SQLite'ta pencere (window) fonksiyonu, arkasına OVER (...) yazılan bir fonksiyondur ve her satır için, o satırla ilişkili bir satır kümesi (pencere) üzerinden hesap yapar. GROUP BY'dan farkı satır sayısını azaltmamasıdır: sum(tutar) OVER (ORDER BY ay) her satırın yanına o satıra kadarki kümülatif toplamı yazar. Sıra numarası, sıralama, önceki ve sonraki satırla karşılaştırma, hareketli ortalama ve grup içinde ilk N kayıt gibi işlerin hepsi pencere fonksiyonlarıyla yapılır.
Pencere fonksiyonları SQLite'a SQLite 3.25.0 sürüm notu ile geldi; GROUPS çerçeve türü, EXCLUDE ve pencere zincirleme ise SQLite 3.28.0 sürüm notu ile eklendi. Belgelere göre SQLite geliştiricileri davranış için PostgreSQL belgelerini başvuru olarak aldı ve sonuçları PostgreSQL ile karşılaştırarak test etti. Bu yazıda PARTITION BY, ORDER BY ve çerçeve tanımının birbirini nasıl etkilediğini ve en çok şaşırtan varsayılan çerçeve davranışını örneklerle açıklıyoruz.
Örnek tablo

İki şubenin dört aylık cirosunu kullanacağız. İzmir şubesinde iki ayın cirosu bilerek aynı (90); eşit değerlerin sıralama ve çerçeve hesaplarını nasıl etkilediğini bu satırlar gösterecek.
CREATE TABLE ciro(sube TEXT, ay TEXT, tutar INTEGER);
INSERT INTO ciro VALUES
('Ankara', '2026-07', 100), ('Ankara', '2026-08', 140),
('Ankara', '2026-09', 120), ('Ankara', '2026-10', 180),
('İzmir', '2026-07', 90), ('İzmir', '2026-08', 90),
('İzmir', '2026-09', 150), ('İzmir', '2026-10', 110);İlk pencere fonksiyonu: row_number()
row_number(), pencere tanımındaki ORDER BY sırasına göre her satıra 1'den başlayan ardışık bir numara verir. Pencere içindeki sıralamanın sorgunun sonuç sırasını etkilemediğine dikkat edin: numaralar ciroya göre verildi, ama sonuç dıştaki ORDER BY sube, ay ile sıralandı. Belgeler bu ayrımı özellikle vurgular; iki sıralama birbirinden bağımsızdır.
SELECT sube, ay, tutar,
row_number() OVER (ORDER BY tutar DESC) AS sira
FROM ciro
ORDER BY sube, ay;
-- çıktı:
-- Ankara|2026-07|100|6
-- Ankara|2026-08|140|3
-- Ankara|2026-09|120|4
-- Ankara|2026-10|180|1
-- İzmir|2026-07|90|7
-- İzmir|2026-08|90|8
-- İzmir|2026-09|150|2
-- İzmir|2026-10|110|5İzmir'in iki 90'lık ayı 7 ve 8 numaralarını aldı. Pencere sıralamasında eşit olan iki satırın hangisinin önce numaralanacağı belirsizdir; kararlı bir numaralandırma istiyorsanız pencere içindeki ORDER BY listesine ay gibi ikinci bir sütun ekleyin.
Pencere fonksiyonları yalnızca sonuç listesinde ve sorgunun ORDER BY bölümünde kullanılabilir. Bunun önemli bir sonucu var: WHERE içinde pencere fonksiyonu kullanamazsınız. Bunu aşağıda "grup içinde ilk N" örneğinde göreceğiz.
Kümülatif toplam ve PARTITION BY
Her toplama fonksiyonu (sum, avg, count, min, max, group_concat ve diğerleri) arkasına OVER eklenerek pencere fonksiyonu olarak kullanılabilir. Bu durumda her satır için sonuç, o satırın "çerçevesi" içindeki satırlar üzerinden hesaplanır. Pencerede ORDER BY varsa varsayılan çerçeve bölümün başından geçerli satıra kadardır; bu da kümülatif toplamı verir.
SELECT ay, tutar, sum(tutar) OVER (ORDER BY ay) AS kumulatif
FROM ciro WHERE sube = 'Ankara';
-- çıktı:
-- 2026-07|100|100
-- 2026-08|140|240
-- 2026-09|120|360
-- 2026-10|180|540PARTITION BY, sonuç kümesini bölümlere ayırır ve pencere hesabı her bölüm için ayrı yapılır. Bölüm, PARTITION BY ifadelerinin hepsi aynı olan satırlardan oluşur; PARTITION BY yazılmazsa bütün sonuç tek bir bölümdür. Aşağıdaki ilk sorguda kümülatif toplam her şube için sıfırdan başlıyor.
SELECT sube, ay, tutar,
sum(tutar) OVER (PARTITION BY sube ORDER BY ay) AS kumulatif
FROM ciro ORDER BY sube, ay;
-- çıktı:
-- Ankara|2026-07|100|100
-- Ankara|2026-08|140|240
-- Ankara|2026-09|120|360
-- Ankara|2026-10|180|540
-- İzmir|2026-07|90|90
-- İzmir|2026-08|90|180
-- İzmir|2026-09|150|330
-- İzmir|2026-10|110|440
-- ORDER BY olmadan: bölümün tamamı tek çerçeve
SELECT sube, ay, tutar,
tutar * 100 / sum(tutar) OVER (PARTITION BY sube) AS yuzde
FROM ciro ORDER BY sube, ay;
-- çıktı:
-- Ankara|2026-07|100|18
-- Ankara|2026-08|140|25
-- Ankara|2026-09|120|22
-- Ankara|2026-10|180|33
-- İzmir|2026-07|90|20
-- İzmir|2026-08|90|20
-- İzmir|2026-09|150|34
-- İzmir|2026-10|110|25İkinci sorgu pencerede ORDER BY kullanmıyor. Belgelere göre ORDER BY yoksa bölümdeki bütün satırlar birbirinin "eşi" (peer) sayılır ve hepsi aynı çerçeveye girer; bu yüzden sum her satır için şubenin toplamını verdi. Bu kalıpla bir satırın grubu içindeki payını, gruplama yapmadan ve satırları kaybetmeden hesaplayabilirsiniz. Tamsayı bölmesi yaptığımız için yüzdeler küsuratsız çıktı; ondalıklı sonuç için tutar * 100.0 yazmanız yeterlidir.
Sıralama fonksiyonları: row_number, rank, dense_rank
Üç fonksiyon da pencere sırasına göre numara verir, ama eşit değerlere farklı davranır. row_number() eşitlik gözetmeden ardışık numara verir. rank() eşit satırlara aynı numarayı verir ve sonrasında boşluk bırakır (1, 2, 2, 4 gibi). dense_rank() eşit satırlara aynı numarayı verir ama boşluk bırakmaz (1, 2, 2, 3 gibi). Aşağıdaki örnekte pencere tanımını WINDOW bölümüne bir kez yazıp üç fonksiyonda adıyla kullandık.
SELECT sube, ay, tutar,
row_number() OVER w AS rn,
rank() OVER w AS rnk,
dense_rank() OVER w AS drnk
FROM ciro
WINDOW w AS (ORDER BY tutar DESC)
ORDER BY rn;
-- çıktı:
-- Ankara|2026-10|180|1|1|1
-- İzmir|2026-09|150|2|2|2
-- Ankara|2026-08|140|3|3|3
-- Ankara|2026-09|120|4|4|4
-- İzmir|2026-10|110|5|5|5
-- Ankara|2026-07|100|6|6|6
-- İzmir|2026-07|90|7|7|7
-- İzmir|2026-08|90|8|7|7Son iki satırda fark görünüyor: iki 90'lık satırın row_number değeri 7 ve 8, rank ve dense_rank değeri ise ikisinde de 7. Eşitlikten sonra başka satır olsaydı rank 9'dan, dense_rank 8'den devam ederdi. Belgelerdeki tabloya göre percent_rank() ve cume_dist() da aynı eş grubu mantığını kullanır; pencerede ORDER BY yoksa rank ve dense_rank her satır için 1 döndürür.
WINDOW bölümü, varsa HAVING bölümünden sonra ve ORDER BY bölümünden önce yazılır. Aynı pencereyi birden çok fonksiyonda kullanırken hem sorguyu kısaltır hem de tanımların birbirinden kaymasını önler.
Önceki ve sonraki satır: lag ve lead
lag(ifade) bölüm içindeki bir önceki satırın, lead(ifade) bir sonraki satırın değerini getirir. İkinci argüman kaç satır geri veya ileri gidileceğini, üçüncü argüman ise o satır yoksa NULL yerine döndürülecek varsayılan değeri belirtir. Bu iki fonksiyon aylık değişim, bir önceki ölçümle fark veya iki olay arasındaki süre gibi hesapların standart yoludur.
SELECT ay, tutar,
lag(tutar) OVER (ORDER BY ay) AS onceki,
tutar - lag(tutar) OVER (ORDER BY ay) AS fark,
lead(tutar, 1, 0) OVER (ORDER BY ay) AS sonraki
FROM ciro WHERE sube = 'Ankara';
-- çıktı:
-- 2026-07|100|||140
-- 2026-08|140|100|40|120
-- 2026-09|120|140|-20|180
-- 2026-10|180|120|60|0İlk ayın öncesi olmadığı için lag NULL döndürdü ve NULL ile yapılan çıkarma da NULL verdi. Son satırdaki lead ise varsayılan değer olarak verdiğimiz 0'ı döndürdü. Belgelere göre lag ve lead çerçeve tanımını dikkate almaz; her zaman bölümdeki konuma bakar.
Çerçeve tanımı: ROWS, RANGE ve GROUPS

Toplama türündeki pencere fonksiyonlarının hangi satırları okuyacağını çerçeve (frame) tanımı belirler. Çerçeve dört parçadan oluşur: tür (ROWS, RANGE veya GROUPS), başlangıç sınırı, bitiş sınırı ve isteğe bağlı EXCLUDE bölümü. Sınırlar UNBOUNDED PRECEDING (bölümün ilk satırı), N PRECEDING, CURRENT ROW, N FOLLOWING ve UNBOUNDED FOLLOWING (bölümün son satırı) olabilir.
| Çerçeve türü | Sınırı ne ile ölçer | Not |
|---|---|---|
| ROWS | Tek tek satır sayısıyla | N PRECEDING tam N satır geri gider |
| GROUPS | Eş grup sayısıyla | Pencere ORDER BY değerleri aynı olan satırlar bir gruptur |
| RANGE | ORDER BY değerinin aralığıyla | N PRECEDING için ORDER BY tek terimli olmalı |
Hareketli toplam veya ortalama için ROWS kullanılır. Aşağıdaki sorgu her ay için o ay ve önceki iki ayın toplamını hesaplıyor. İlk iki ayda geriye yeterince satır olmadığı için çerçeve daha kısa; bunu yanındaki sayım sütunuyla görebilirsiniz. Ortalama alırken bu kısa çerçeveleri hesaba katmak gerekir.
SELECT ay, tutar,
sum(tutar) OVER (ORDER BY ay ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS son3_toplam,
count(*) OVER (ORDER BY ay ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS son3_adet
FROM ciro WHERE sube = 'İzmir';
-- çıktı:
-- 2026-07|90|90|1
-- 2026-08|90|180|2
-- 2026-09|150|330|3
-- 2026-10|110|350|3Varsayılan çerçevenin tuzağı
Çerçeve belirtmezseniz SQLite RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS kullanır. Buradaki RANGE ayrıntısı önemlidir: RANGE ve GROUPS türlerinde CURRENT ROW, geçerli satırın bütün eşlerini de çerçeveye katar. Pencere ORDER BY değerinde eşitlik varsa, eşit satırların hepsi aynı sonucu alır.
SELECT ay, tutar,
sum(tutar) OVER (ORDER BY tutar) AS range_varsayilan,
sum(tutar) OVER (ORDER BY tutar ROWS UNBOUNDED PRECEDING) AS rows_cercevesi
FROM ciro WHERE sube = 'İzmir'
ORDER BY tutar, ay;
-- çıktı:
-- 2026-07|90|180|90
-- 2026-08|90|180|180
-- 2026-10|110|290|290
-- 2026-09|150|440|440İlk 90'lık satırda varsayılan çerçeve 180 verdi, çünkü diğer 90'lık satır onun eşi ve çerçeveye dahil. ROWS UNBOUNDED PRECEDING ile satır satır ilerleyen gerçek bir kümülatif toplam elde ettik. Kural olarak pencere sıralama sütununuz benzersiz değilse ve satır bazında birikim istiyorsanız çerçeveyi ROWS ile açıkça yazın.
Aynı tuzak last_value() fonksiyonunda daha da belirgindir. Varsayılan çerçeve geçerli satırda bittiği için last_value bölümün son değerini değil, geçerli satırın değerini (ve eşlerini) görür. Bölümün gerçek son değerini almak için çerçeveyi UNBOUNDED FOLLOWING sınırına kadar genişletmeniz gerekir.
SELECT ay, tutar,
last_value(tutar) OVER (ORDER BY ay) AS varsayilan,
last_value(tutar) OVER (ORDER BY ay
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS gercek_son
FROM ciro WHERE sube = 'Ankara';
-- çıktı:
-- 2026-07|100|100|180
-- 2026-08|140|140|180
-- 2026-09|120|120|180
-- 2026-10|180|180|180Belgelerdeki listeye göre yerleşik pencere fonksiyonlarının çoğu çerçeveyi yok sayar; çerçeveyi dikkate alan üç istisna first_value(), last_value() ve nth_value()'dur. Toplama fonksiyonları ise her zaman çerçeveye göre çalışır.
EXCLUDE bölümü
EXCLUDE çerçeveden bazı satırları çıkarır. EXCLUDE NO OTHERS varsayılandır ve hiçbir satırı çıkarmaz. EXCLUDE CURRENT ROW geçerli satırı, EXCLUDE GROUP geçerli satırı ve bütün eşlerini, EXCLUDE TIES ise geçerli satırı bırakıp yalnızca eşlerini çıkarır. Örneğin bir değerin, kendisi hariç grubun geri kalanının ortalamasıyla karşılaştırılması gerektiğinde EXCLUDE CURRENT ROW işe yarar.
Grup içinde ilk N kayıt
"Her şubenin en yüksek iki ayı" gibi sorular pencere fonksiyonlarının en yaygın kullanım alanıdır. İlk akla gelen yazım hata verir, çünkü pencere fonksiyonları WHERE bölümünde kullanılamaz. Çözüm, numaralandırmayı bir alt sorguda (veya CTE'de) yapıp süzmeyi dıştaki sorguda uygulamaktır.
SELECT sube, ay, tutar FROM ciro
WHERE row_number() OVER (PARTITION BY sube ORDER BY tutar DESC) = 1;
-- hata: misuse of window function row_number()
SELECT sube, ay, tutar FROM (
SELECT sube, ay, tutar,
row_number() OVER (PARTITION BY sube ORDER BY tutar DESC) AS rn
FROM ciro
)
WHERE rn <= 2
ORDER BY sube, tutar DESC;
-- çıktı:
-- Ankara|2026-10|180
-- Ankara|2026-08|140
-- İzmir|2026-09|150
-- İzmir|2026-10|110Eşitlik durumunda kaç satır istediğinize göre fonksiyonu seçin: tam N satır için row_number(), eşitleri birlikte getirmek için rank() veya dense_rank(). Bu kalıp, GROUP BY yazısında anlatılan SQLite'a özgü çıplak sütun davranışının taşınabilir bir alternatifidir. Alt sorgunun okunabilirliği için CTE kullanımına SQLite WITH ve recursive CTE yazısında bakabilirsiniz.
ntile ile dilimlere ayırma
ntile(N) bölümü olabildiğince eşit N gruba ayırır ve her satıra grup numarasını verir. Satır sayısı N'ye tam bölünmüyorsa belgelere göre büyük gruplar önce gelir. Sekiz satırı üç dilime böldüğümüzde 3, 3 ve 2 satırlık dilimler oluştu.
SELECT sube, ay, tutar, ntile(3) OVER (ORDER BY tutar DESC) AS dilim
FROM ciro ORDER BY dilim, tutar DESC;
-- çıktı:
-- Ankara|2026-10|180|1
-- İzmir|2026-09|150|1
-- Ankara|2026-08|140|1
-- Ankara|2026-09|120|2
-- İzmir|2026-10|110|2
-- Ankara|2026-07|100|2
-- İzmir|2026-07|90|3
-- İzmir|2026-08|90|3FILTER, adlandırılmış pencereler ve zincirleme
Toplama türündeki pencere fonksiyonlarında FILTER (WHERE koşul) kullanılabilir. Bu durumda koşulu sağlamayan satırlar hiçbir satırın çerçevesine girmez, ama fonksiyon yine her satır için bir değer döndürür. Pencere zincirleme ise adlandırılmış bir pencereyi temel alıp ona yalnızca çerçeve eklemenizi sağlar: OVER (w ROWS ...) yazımı, w penceresinin PARTITION BY ve ORDER BY bölümlerini kopyalar.
SELECT ay, tutar,
sum(tutar) FILTER (WHERE tutar >= 120) OVER (ORDER BY ay) AS buyukler
FROM ciro WHERE sube = 'Ankara';
-- çıktı:
-- 2026-07|100|
-- 2026-08|140|140
-- 2026-09|120|260
-- 2026-10|180|440
SELECT sube, ay, sum(tutar) OVER (w ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) AS iki_ay
FROM ciro
WINDOW w AS (PARTITION BY sube ORDER BY ay)
ORDER BY sube, ay;
-- çıktı:
-- Ankara|2026-07|100
-- Ankara|2026-08|240
-- Ankara|2026-09|260
-- Ankara|2026-10|300
-- İzmir|2026-07|90
-- İzmir|2026-08|180
-- İzmir|2026-09|240
-- İzmir|2026-10|260Zincirlemenin belgelerde belirtilen kuralları vardır: yeni pencere kendi PARTITION BY bölümünü içeremez; temel pencerede ORDER BY varsa yeni pencere ayrıca ORDER BY yazamaz; temel pencere çerçeve tanımı içeremez. Bu yüzden OVER w ile OVER (w) neredeyse aynıdır, ancak ikincisi w bir çerçeve içeriyorsa hata verir.
Kısıtlar ve hata mesajları
Pencere fonksiyonları gruplama sonrasında hesaplanır, bu yüzden gruplanmış bir sorguda özet sonuçlarını sıralamak için de kullanılabilir. Buna karşılık iki önemli kısıt vardır: pencere fonksiyonlarında DISTINCT kullanılamaz ve FILTER yalnızca toplama türündeki pencere fonksiyonlarında geçerlidir; rank() gibi yerleşik pencere fonksiyonlarına FILTER eklemek hata verir.
SELECT sube, sum(tutar), rank() OVER (ORDER BY sum(tutar) DESC) FROM ciro GROUP BY sube;
-- çıktı:
-- Ankara|540|1
-- İzmir|440|2
SELECT rank() FILTER (WHERE tutar > 100) OVER (ORDER BY tutar) FROM ciro;
-- hata: FILTER clause may only be used with aggregate window functions
SELECT count(DISTINCT sube) OVER () FROM ciro;
-- hata: DISTINCT is not supported for window functionsYerleşik pencere fonksiyonları

| Fonksiyon | Döndürdüğü | Çerçeveye bakar mı |
|---|---|---|
| row_number() | Bölüm içindeki sıra numarası | Hayır |
| rank() | Boşluklu sıra (eşler aynı) | Hayır |
| dense_rank() | Boşluksuz sıra (eşler aynı) | Hayır |
| percent_rank() | (rank - 1) / (satır sayısı - 1) | Hayır |
| cume_dist() | Kümülatif dağılım | Hayır |
| ntile(N) | N dilimden hangisinde olduğu | Hayır |
| lag / lead | Önceki / sonraki satırın değeri | Hayır |
| first_value / last_value | Çerçevenin ilk / son değeri | Evet |
| nth_value(x, N) | Çerçevenin N. satırındaki değer | Evet |
Sık Sorulan Sorular
Pencere fonksiyonları eski SQLite sürümlerinde çalışır mı?
Hayır. İlk destek 3.25.0 sürümüyle geldi; GROUPS, EXCLUDE, pencere zincirleme ve RANGE çerçevelerinde N PRECEDING/N FOLLOWING sınırları ise 3.28.0 ile eklendi. Uygulamanızın kullandığı kütüphanenin sürümünü sqlite_version() ile kontrol edin.
Pencere fonksiyonu ile GROUP BY birlikte kullanılabilir mi?
Evet. Gruplama önce yapılır, pencere fonksiyonları gruplanmış satırlar üzerinde çalışır. Yukarıdaki son örnekte şubeleri toplam ciroya göre rank() ile sıraladık.
Hareketli ortalamada ilk satırlar neden farklı?
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ilk satırda yalnızca bir, ikinci satırda iki satır bulur; avg() bu kısa çerçevelerin ortalamasını verir. Tam çerçeve istiyorsanız count(*) OVER (...) ile çerçevedeki satır sayısını hesaplayıp eksik olanları dışarıda süzebilirsiniz.
Kendi pencere fonksiyonumu yazabilir miyim?
Toplama türünde olanları yazabilirsiniz; belgeler bunun C arayüzündeki sqlite3_create_window_function() ile yapıldığını belirtir. Dil bağlayıcılarının bu arayüzü sunup sunmadığı bağlayıcıya göre değişir; örneğin Python sqlite3 yazısına bakabilirsiniz. rank() gibi planlayıcıda özel işlem gerektiren yerleşik pencere fonksiyonlarının benzerleri uygulama tarafından eklenemez.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: SQLite öğrenme rehberi · GROUP BY ve aggregate · SQLite SELECT · WITH ve recursive CTE · Tarih ve saat fonksiyonları · EXPLAIN QUERY PLAN