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

sql
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.

sql
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|2000

Bir 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.

sql
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|1

Burada 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 CTE nasıl çalışır? Başlangıç sorgusunu çalıştır ve sonuçlarını bir kuyruğa ekle., Kuyruk boşalana kadar, Az önce çıkarılan satırı CTE'nin tek satırıymış gibi kabul...

Ö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.
sql
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

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.

sql
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|1

Tarihleri 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.

sql
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.

sql
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.

sql
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,2

UNION 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.

sql
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.

sql
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|Zeynep

Bu 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.

sql
WITH kucuk AS (SELECT id FROM siparis WHERE tutar < 500)
DELETE FROM siparis WHERE id IN kucuk;

SELECT count(*) FROM siparis;
-- çıktı: 3

Veri 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

Kısıtlar ve dikkat edilecekler: Tetikleyici içinde kullanılamaz, Yalnızca en başta, RECURSIVE sözcüğü zorunlu değil, Sonsuz döngü
  • Tetikleyici içinde kullanılamaz: belgelere göre WITH bölümü CREATE TRIGGER gövdesinde yer alamaz.
  • Yalnızca en başta: WITH üst düzey bir SELECT ifadesinin veya bir alt sorgunun başında olmalıdır; birleşik bir sorgunun ikinci parçasının önüne yazılamaz.
  • RECURSIVE sö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. RECURSIVE yazmak da CTE'leri özyinelemeli olmaya zorlamaz.
  • Sonsuz döngü: döngü içerebilecek yapılarda UNION kullanın veya bir LIMIT ekleyin.

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ı