SQLite'ta WITH ad AS (SELECT ...) yazarak bir sorgunun içinde geçici, adlandırılmış bir sonuç kümesi tanımlarsınız; buna ortak tablo ifadesi (Common Table Expression, CTE) denir ve yalnızca o tek ifade çalıştığı sürece var olan bir görünüm gibi davranır. WITH RECURSIVE ile tanımlanan özyinelemeli CTE ise kendi sonucuna yeniden başvurarak ağaçları, hiyerarşileri, grafları ve sayı ya da tarih dizilerini tek bir SQL ifadesiyle dolaşmanızı sağlar.
Bu yazıda önce sıradan CTE'lerle karmaşık sorguları okunur parçalara ayırmayı, ardından özyinelemeli CTE'nin SQLite belgelerinde anlatılan kuyruk algoritmasını ve bu algoritmanın UNION, LIMIT ve ORDER BY ile nasıl kontrol edildiğini adım adım inceliyoruz. Son bölümde MATERIALIZED ipuçlarını ve CTE'nin kısıtlarını bulacaksınız. Temel sorgu yapısı için SQLite SELECT, birleştirmeler için SQLite JOIN türleri yazılarına bakabilirsiniz.
Örnek tablo
CREATE TABLE siparis(id INTEGER PRIMARY KEY, musteri TEXT, tutar INTEGER, tarih TEXT);
INSERT INTO siparis(musteri, tutar, tarih) VALUES
('Ayşe', 1200, '2026-09-03'),
('Mehmet', 300, '2026-09-10'),
('Ayşe', 800, '2026-10-01'),
('Zeynep', 2500, '2026-10-15'),
('Mehmet', 450, '2026-10-20');Sıradan CTE: alt sorguyu adlandırmak
Sıradan bir CTE, FROM bölümüne yazacağınız bir alt sorguyu ifadenin başına taşır ve ona bir ad verir. Sonuç değişmez, ama sorgu yukarıdan aşağıya okunabilir hâle gelir: önce ara sonucun ne olduğu, sonra onunla ne yapıldığı görülür. Belgeler sıradan CTE'leri tam olarak bu amaçla, alt sorguları ayrıştırıp ifadeyi anlaşılır kılmak için önerir.
WITH musteri_toplam AS (
SELECT musteri, sum(tutar) AS toplam
FROM siparis
GROUP BY musteri
)
SELECT musteri, toplam
FROM musteri_toplam
WHERE toplam > 1000
ORDER BY toplam DESC;
-- çıktı:
-- Zeynep|2500
-- Ayşe|2000Bir WITH bölümünde virgülle ayrılmış birden fazla CTE tanımlayabilirsiniz ve sonraki CTE önceki CTE'leri kullanabilir. CTE adından sonra parantez içinde sütun adları vermek de mümkündür; bu durumda içteki SELECT ifadesinin sütunları bu adları alır ve takma ad yazmanıza gerek kalmaz.
WITH
aylik(ay, ciro) AS (
SELECT substr(tarih, 1, 7), sum(tutar) FROM siparis GROUP BY 1
),
ortalama(deger) AS (
SELECT avg(ciro) FROM aylik
)
SELECT ay, ciro, ciro > deger AS ortalama_ustu
FROM aylik, ortalama
ORDER BY ay;
-- çıktı:
-- 2026-09|1500|0
-- 2026-10|3750|1Burada aylik CTE'si aylık ciroyu hesaplıyor, ortalama ise bu ara sonucun ortalamasını alıyor. Son sorgu iki CTE'yi virgülle birleştiriyor; ortalama tek satır olduğu için bu birleştirme her aya ortalama değeri ekliyor. Ortalamanın 2625 olduğunu ekrana yazdırmadık, ama karşılaştırma sonucunu 0 ve 1 olarak görüyoruz. CTE'ler yalnızca SELECT önünde değil, INSERT, UPDATE ve DELETE ifadelerinin önünde de kullanılabilir; bunu yazının sonunda göreceğiz.
Özyinelemeli CTE nasıl çalışır?

Özyinelemeli bir CTE iki parçadan oluşan birleşik bir sorgudur. İlk parça (başlangıç sorgusu) CTE'ye başvurmaz ve başlangıç satırlarını üretir. İkinci parça (özyinelemeli sorgu) FROM bölümünde CTE'nin kendisine tam olarak bir kez başvurur. İki parça UNION ALL veya UNION ile bağlanır. SQLite belgeleri sonucu şu algoritmayla tarif eder:
- Başlangıç sorgusunu çalıştır ve sonuçlarını bir kuyruğa ekle.
- Kuyruk boşalana kadar: kuyruktan bir satır çıkar, bu satırı CTE sonucuna ekle.
- Az önce çıkarılan satırı CTE'nin tek satırıymış gibi kabul ederek özyinelemeli sorguyu çalıştır ve ürettiği satırları kuyruğa ekle.
WITH RECURSIVE sayac(x) AS (
SELECT 1 -- başlangıç sorgusu
UNION ALL
SELECT x + 1 FROM sayac WHERE x < 5 -- özyinelemeli sorgu
)
SELECT x FROM sayac;
-- çıktı:
-- 1
-- 2
-- 3
-- 4
-- 5
-- WHERE yerine LIMIT ile durdurmak
WITH RECURSIVE sayac(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM sayac LIMIT 3)
SELECT group_concat(x, ',') FROM sayac;
-- çıktı: 1,2,3İlk örnekte kuyruğa önce 1 girer. 1 çıkarılıp sonuca eklenince özyinelemeli sorgu 2'yi üretir, sonra 3'ü ve bu böyle sürer. 5 çıkarıldığında WHERE x < 5 koşulu hiçbir satır üretmez, kuyruk boşalır ve özyineleme biter. Belgeler burada önemli bir ayrıntıya dikkat çeker: "sonuca ekle" ifadesi kavramsaldır. SQLite her satırın yalnızca bir kez kullanıldığını fark ettiğinde satırı üretir üretmez dış sorguya verir ve saklamaz; bu sayede bir milyon sayı üreten bir sorgu bile çok az bellek kullanır.
LIMIT ile güvenli durdurma
İkinci örnekte koşul yerine LIMIT kullanıldı. Özyinelemeli sorgudaki LIMIT, CTE sonucuna eklenecek en fazla satır sayısını belirler ve sınıra ulaşıldığında kuyrukta satır kalsa bile özyineleme hemen durur. Karmaşık sorgularda WHERE koşulunun kuyruğu gerçekten boşaltacağından emin olmak zor olabilir; belgeler bu yüzden üst sınırı bildiğiniz durumlarda güvenlik önlemi olarak LIMIT eklemeyi iyi bir alışkanlık olarak görür. OFFSET de kullanılabilir; ilk N satır işlenir ama sonuca eklenmez.
Uygulama: eksik günleri doldurmak

Raporlarda sık karşılaşılan bir sorun, hiç kaydı olmayan günlerin sonuçta hiç görünmemesidir. Özyinelemeli CTE ile önce bir tarih dizisi üretip ardından gerçek verilerle LEFT JOIN yaparsanız sıfır değerli günler de listede yer alır. Tarih ilerletmek için date(t, '+1 day') kullandık; tarih fonksiyonları SQLite tarih ve saat fonksiyonları yazısında anlatılıyor.
WITH RECURSIVE gun(t) AS (
SELECT '2026-10-14'
UNION ALL
SELECT date(t, '+1 day') FROM gun WHERE t < '2026-10-20'
)
SELECT g.t, count(s.id) AS siparis_sayisi
FROM gun g LEFT JOIN siparis s ON s.tarih = g.t
GROUP BY g.t
ORDER BY g.t;
-- çıktı:
-- 2026-10-14|0
-- 2026-10-15|1
-- 2026-10-16|0
-- 2026-10-17|0
-- 2026-10-18|0
-- 2026-10-19|0
-- 2026-10-20|1Tarihleri YYYY-AA-GG biçiminde metin olarak sakladığımız için t < '2026-10-20' karşılaştırması doğru çalışıyor: bu biçimde metin sıralaması ile tarih sıralaması aynıdır. Saymak için count(*) değil count(s.id) kullandık; aksi hâlde siparişi olmayan günler 1 olarak görünürdü.
Hiyerarşi ve ağaç sorguları
Her satırın üst kaydını gösteren bir sütun içeren tablolar (kategori ağacı, organizasyon şeması, yorum yanıtları) özyinelemeli CTE'nin en tipik kullanım alanıdır. Başlangıç sorgusu kökü seçer, özyinelemeli sorgu da tabloyu CTE ile birleştirerek bir alt seviyeye iner. Seviye bilgisini bir sütunda taşırsanız girinti veya derinlik sınırı için kullanabilirsiniz.
CREATE TABLE kategori(id INTEGER PRIMARY KEY, ad TEXT, ust_id INTEGER REFERENCES kategori);
INSERT INTO kategori VALUES
(1, 'Bilgisayar', NULL), (2, 'Dizüstü', 1), (3, 'Masaüstü', 1),
(4, 'Oyun dizüstü', 2), (5, 'İş dizüstü', 2), (6, 'Mini PC', 3), (7, 'Telefon', NULL);
-- Genişlik öncelikli: önce sığ seviyeler
WITH RECURSIVE alt(id, ad, seviye) AS (
SELECT id, ad, 0 FROM kategori WHERE id = 1
UNION ALL
SELECT k.id, k.ad, alt.seviye + 1
FROM kategori k JOIN alt ON k.ust_id = alt.id
ORDER BY 3
)
SELECT substr('..........', 1, seviye * 2) || ad FROM alt;
-- çıktı:
-- Bilgisayar
-- ..Dizüstü
-- ..Masaüstü
-- ....Oyun dizüstü
-- ....İş dizüstü
-- ....Mini PC
-- Derinlik öncelikli: ORDER BY 3 DESC
WITH RECURSIVE alt(id, ad, seviye) AS (
SELECT id, ad, 0 FROM kategori WHERE id = 1
UNION ALL
SELECT k.id, k.ad, alt.seviye + 1
FROM kategori k JOIN alt ON k.ust_id = alt.id
ORDER BY 3 DESC
)
SELECT substr('..........', 1, seviye * 2) || ad FROM alt;
-- çıktı:
-- Bilgisayar
-- ..Dizüstü
-- ....Oyun dizüstü
-- ....İş dizüstü
-- ..Masaüstü
-- ....Mini PCİki sorgu arasındaki tek fark özyinelemeli sorgudaki ORDER BY yönüdür. Belgelere göre özyinelemeli sorgudaki ORDER BY, kuyruktan bir sonraki satırın hangi sırayla çıkarılacağını belirler. ORDER BY 3 en küçük seviyeli satırları önce çıkarır ve ağaç seviye seviye, yani genişlik öncelikli dolaşılır. DESC eklendiğinde en derin satır önce çıkar ve her dal sonuna kadar izlenir; sonuç derinlik öncelikli dolaşmadır ve bir ağaç görünümü için genellikle istenen budur. ORDER BY yazılmazsa kuyruk şu anki uygulamada ilk giren ilk çıkar mantığıyla çalışır, ancak belgeler buna güvenilmemesini, çünkü ileride değişebileceğini söyler.
Yol oluşturma ve yukarı doğru dolaşma
Özyinelemeli sorguda her adımda bir metin sütununu büyüterek kökten yaprağa giden yolu oluşturabilirsiniz. Ters yönde gitmek de aynı kadar kolaydır: başlangıç sorgusu bir yaprağı seçer, özyinelemeli sorgu ise ust_id üzerinden bir üst seviyeye çıkar.
WITH RECURSIVE yol(id, iz) AS (
SELECT id, ad FROM kategori WHERE ust_id IS NULL
UNION ALL
SELECT k.id, yol.iz || ' > ' || k.ad
FROM kategori k JOIN yol ON k.ust_id = yol.id
)
SELECT iz FROM yol ORDER BY iz;
-- çıktı:
-- Bilgisayar
-- Bilgisayar > Dizüstü
-- Bilgisayar > Dizüstü > Oyun dizüstü
-- Bilgisayar > Dizüstü > İş dizüstü
-- Bilgisayar > Masaüstü
-- Bilgisayar > Masaüstü > Mini PC
-- Telefon
-- Aşağıdan yukarı: bir kategorinin bütün üstleri
WITH RECURSIVE ata(id, ad, ust_id) AS (
SELECT id, ad, ust_id FROM kategori WHERE id = 4
UNION ALL
SELECT k.id, k.ad, k.ust_id FROM kategori k JOIN ata ON k.id = ata.ust_id
)
SELECT group_concat(ad, ' < ') FROM ata;
-- çıktı: Oyun dizüstü < Dizüstü < Bilgisayarİlk sorgunun sonucundaki sıralama dikkat çekebilir: "İş dizüstü", "Oyun dizüstü" satırından sonra geliyor. Sıralama BINARY harmanlamayla, yani bayt değerlerine göre yapıldığı için Türkçe büyük İ harfi Latin harflerinin arkasına düşer. Türkçe sıralama gerekiyorsa COLLATE ve NOCASE yazısındaki seçeneklere bakın. İkinci sorgudaki group_concat sonucu, satırların üretildiği sırayı yansıtıyor; sıranın garanti olmasını istiyorsanız seviye sütunu tutup birleştirmede ORDER BY kullanın.
Graflar ve döngüler: UNION ile UNION ALL
Ağaçta her düğümün tek bir üstü vardır ve dolaşma kendiliğinden biter. Graflarda ise döngü olabilir: 1'den 2'ye, 2'den 3'e, 3'ten tekrar 1'e giden bağlantılar sonsuz bir dolaşmaya yol açar. Belgelere göre başlangıç ve özyinelemeli sorgu UNION ile bağlandığında bir satır, daha önce kuyruğa eklenmiş bir satırın aynısıysa kuyruğa eklenmez; o satır kuyruktan çoktan çıkarılmış olsa bile. Bu kural döngüleri doğal olarak keser.
CREATE TABLE baglanti(a INTEGER, b INTEGER);
INSERT INTO baglanti VALUES (1, 2), (2, 3), (3, 1), (3, 4), (5, 6);
-- UNION: daha önce görülen satır kuyruğa yeniden girmez, döngü biter
WITH RECURSIVE ulasilan(x) AS (
SELECT 1
UNION
SELECT b FROM baglanti JOIN ulasilan ON a = x
)
SELECT group_concat(x, ',') FROM ulasilan;
-- çıktı: 1,2,3,4
-- UNION ALL: döngüde dönmeye devam eder, yalnızca LIMIT durdurur
WITH RECURSIVE ulasilan(x) AS (
SELECT 1
UNION ALL
SELECT b FROM baglanti JOIN ulasilan ON a = x
LIMIT 10
)
SELECT group_concat(x, ',') FROM ulasilan;
-- çıktı: 1,2,3,1,4,2,3,1,4,2UNION ALL sürümü yinelenenleri ayıklamadığı için 1, 2, 3 döngüsünde dönmeye devam etti ve yalnızca LIMIT 10 sayesinde durdu. Öte yandan UNION bedelsiz değildir: yinelenenleri tespit edebilmek için SQLite'ın daha önce ürettiği bütün satırları saklaması gerekir. Belgeler bu nedenle mümkün olan her yerde UNION ALL kullanılmasını önerir; graf gibi döngü riski olan yapılarda ise UNION doğru seçimdir.
Özyinelemeli CTE'nin kuralları
Belgeler özyinelemeli bir CTE'nin uyması gereken kuralları açıkça sıralar. Sorgu en az iki parçalı birleşik bir sorgu olmalıdır; özyinelemeli olmayan parçalar özyinelemeli parçalardan önce gelmelidir; her özyinelemeli parçanın FROM bölümünde CTE tam olarak bir kez geçmeli ve başka hiçbir yerde, alt sorgular dahil, geçmemelidir. Özyinelemeli parça özet fonksiyonu ve pencere fonksiyonu kullanamaz. Başlangıç sorgusu ORDER BY, LIMIT veya OFFSET içeremez; özyinelemeli sorgu içerebilir.
WITH RECURSIVE sayac(x) AS (SELECT 1 UNION ALL SELECT sum(x) FROM sayac)
SELECT x FROM sayac;
-- hata: recursive aggregate queries not supported
WITH RECURSIVE sayac(x) AS (
SELECT 1 UNION ALL SELECT x + 1 FROM sayac, sayac AS s2 WHERE x < 3
)
SELECT x FROM sayac;
-- hata: multiple references to recursive table: sayacÖzyinelemeli parçanın kendisi de birleşik bir sorgu olabilir; yukarıdaki graf örneğinde her iki yönde dolaşmak için iki ayrı özyinelemeli SELECT yazılabilir. Bu olanak 3.34.0 sürümüyle geldi; belgelere göre daha eski sürümlerde özyinelemeli kısım tek bir basit SELECT olmak zorundaydı.
MATERIALIZED ve NOT MATERIALIZED
Bir CTE'nin nasıl uygulanacağına normalde sorgu planlayıcı karar verir: CTE alt sorgu olarak dış sorguya gömülebilir ya da bir kez hesaplanıp geçici bir tabloda tutulabilir. AS MATERIALIZED yazdığınızda CTE geçici bir tabloya yazılır ve adı geçtiği her yerde o tablo kullanılır. Bu durumda sorgu düzleştirme (flattening) veya koşulu alt sorguya itme (push-down) gibi iyileştirmeler uygulanamaz; belgeler bunun bir hata değil, planlayıcıyı bilinçli olarak sınırlamak için kullanılan bir "iyileştirme bariyeri" olduğunu belirtir. AS NOT MATERIALIZED ise CTE'nin sıradan bir görünüm veya alt sorgu gibi ele alınmasını ister ama geçici tablo kullanımını tamamen yasaklamaz.
WITH m AS MATERIALIZED (
SELECT musteri, sum(tutar) AS t FROM siparis GROUP BY musteri
)
SELECT a.musteri, b.musteri
FROM m a JOIN m b ON a.t < b.t
WHERE a.musteri = 'Mehmet'
ORDER BY 2;
-- çıktı:
-- Mehmet|Ayşe
-- Mehmet|ZeynepBu ipuçları PostgreSQL'den alınmış standart dışı bir sözdizimidir ve SQLite 3.35.0 sürüm notu ile eklendi. Aynı sürüm notu, birden fazla kez kullanılan CTE'lerin varsayılan davranışının MATERIALIZED olarak değiştiğini de kaydeder. Belgelerin önerisi nettir: geçerli bir nedeniniz yoksa bu anahtar sözcükleri kullanmayın ve kararı planlayıcıya bırakın. Bir sorgunun planını görmek için EXPLAIN QUERY PLAN yazısına bakın.
CTE ile veri değiştirmek
WITH bölümü INSERT, UPDATE ve DELETE ifadelerinin önüne de yazılabilir. Aşağıdaki örnekte silinecek satırları önce bir CTE ile belirledik. IN işlecinin sağ tarafına yalnızca CTE adını yazmak, belgelere göre (SELECT * FROM kucuk) yazmakla aynıdır.
WITH kucuk AS (SELECT id FROM siparis WHERE tutar < 500)
DELETE FROM siparis WHERE id IN kucuk;
SELECT count(*) FROM siparis;
-- çıktı: 3Veri değiştiren sorguları denemeden önce aynı CTE'yi bir SELECT ile çalıştırıp hangi satırların etkileneceğini görmek iyi bir alışkanlıktır. Toplu silme ve güncelleme işlemleri için SQLite INSERT, UPDATE ve DELETE ve SQLite transaction yazılarına da bakın.
Kısıtlar ve dikkat edilecekler

- Tetikleyici içinde kullanılamaz: belgelere göre
WITHbölümüCREATE TRIGGERgövdesinde yer alamaz. - Yalnızca en başta:
WITHüst düzey birSELECTifadesinin veya bir alt sorgunun başında olmalıdır; birleşik bir sorgunun ikinci parçasının önüne yazılamaz. RECURSIVEsözcüğü zorunlu değil: SQL standardı bunu ister, ancak SQLite SQL Server ve Oracle ile uyum için zorlamaz. Okunabilirlik için yine de yazmanızı öneririz.RECURSIVEyazmak da CTE'leri özyinelemeli olmaya zorlamaz.- Sonsuz döngü: döngü içerebilecek yapılarda
UNIONkullanın veya birLIMITekleyin.
Sık Sorulan Sorular
CTE ile görünüm (VIEW) arasındaki fark nedir?
CTE yalnızca tanımlandığı tek ifade boyunca vardır ve veritabanına kaydedilmez. Görünüm ise şemanın bir parçasıdır ve her sorguda yeniden kullanılabilir. Aynı ara sonucu birçok sorguda kullanıyorsanız görünüm daha uygundur; bkz. SQLite CREATE VIEW.
CTE sorguyu hızlandırır mı?
Kendi başına hayır; çoğu durumda planlayıcı CTE'yi alt sorgu gibi ele alır ve aynı planı üretir. CTE'nin asıl katkısı okunabilirliktir. Birden çok kez kullanılan bir CTE'nin bir kez hesaplanması bazı sorgularda fark yaratabilir, ama bunu varsaymak yerine planı ölçerek kontrol edin.
Özyinelemeli CTE ile kaç seviye inebilirim?
Algoritma satır satır ilerlediği için seviye sayısının belgelerde ayrıca bir sınırı yoktur; sınırı verinizin büyüklüğü ve sizin koyduğunuz WHERE ya da LIMIT koşulu belirler. Belgelerdeki örnekler bir milyon satırlık sayı dizilerini ve Sudoku çözümünü bile tek bir sorguyla gösterir.
Sayı dizisi üretmenin daha kısa bir yolu var mı?
generate_series(başlangıç, bitiş, adım) adlı tablo değerli fonksiyon bunu tek satırda yapar: SELECT value FROM generate_series(1, 5). Belgelere göre bu fonksiyon SQLite kaynak ağacındaki yüklenebilir bir eklentidir ve komut satırı aracına derlenmiş olarak gelir; uygulamanızın kullandığı kütüphanede bulunmayabilir. Belgeler aynı işi gören özyinelemeli CTE karşılığını da verir; her SQLite kurulumunda çalışan taşınabilir yöntem budur.
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 · SQLite SELECT · SQLite JOIN türleri · Window fonksiyonları · SQLite CREATE VIEW · Tarih ve saat fonksiyonları