Skip to content

Bölüm 7 / 19

Alt sorgular

Alt sorgu, CTE, window fonksiyonları.

17 dakikalık okuma

Bu ders SQL'in "ileri" sayılan üç aracını kapsıyor ve üçünde de asıl bilgi sözdiziminde değil motorun ne yaptığında: SQL Server'da bir CTE'yi iki kez referans etmek onu iki kez çalıştırır; PostgreSQL 12'ye kadar her CTE bir iyileştirme bariyeriydi; pencere fonksiyonlarının varsayılan çerçevesi ROWS değil RANGE'dir ve eşit değerli satırlarda farklı sonuç verir. Bunları bilmeyen biri doğru görünen ama yanlış olan sorgular yazar.

01Ne öğreneceksin

  • Alt sorgu türleri: skaler, çok satırlı, korelasyonlu, türetilmiş tablo
  • Korelasyonlu alt sorgunun "her satır için çalışır" sanılması — ve iyileştiricinin onu join'e çevirmesi
  • Skaler kullanıcı fonksiyonlarının bunu neden engellediği
  • CTE: okunabilirlik kazancı ve SQL Server'da iki kez referansın iki kez çalışması
  • PostgreSQL 12 öncesi/sonrası CTE davranışı ve MATERIALIZED anahtar kelimesi
  • Geçici tablo ile tablo değişkeni arasındaki istatistik farkı
  • Özyinelemeli CTE, MAXRECURSION ve sonsuz döngüden korunma
  • ROW_NUMBER/RANK/DENSE_RANK, LAG/LEAD, yürüyen toplam
  • RANGE ile ROWS çerçeve farkı ve LAST_VALUE tuzağı
  • POC indeksiyle pencere fonksiyonunun sıralamasını bedavaya getirmek

02Alt sorgular

Alt sorgu, başka bir sorgunun içine yerleştirilen sorgudur. Dört yerde kullanılabilir ve her yerin kendi kuralı vardır:

-- 1) SELECT listesinde: SKALER alt sorgu — tek satır, tek sütun döndürmek ZORUNDA
SELECT m.Ad,
       (SELECT COUNT(*) FROM Siparis s WHERE s.MusteriId = m.Id) AS SiparisSayisi
FROM   Musteri m;

-- 2) WHERE içinde: çok satırlı alt sorgu
SELECT * FROM Musteri WHERE Id IN (SELECT MusteriId FROM Siparis WHERE Tutar > 1000);
SELECT * FROM Siparis WHERE Tutar > (SELECT AVG(Tutar) FROM Siparis);      -- skaler

-- 3) FROM içinde: TÜRETİLMİŞ TABLO — takma ad zorunludur
SELECT t.Sehir, t.Toplam
FROM  (SELECT Sehir, SUM(Tutar) AS Toplam FROM Siparis GROUP BY Sehir) t
WHERE t.Toplam > 10000;

-- 4) EXISTS ile: varlık kontrolü — ne döndürdüğü önemsizdir
SELECT * FROM Musteri m WHERE EXISTS (SELECT 1 FROM Siparis s WHERE s.MusteriId = m.Id);

Skaler alt sorgunun sıkı kuralı: birden çok satır döndürürse deyim çalışma anında hata verir ("alt sorgu birden fazla değer döndürdü"). Hiç satır döndürmezse NULL üretir — hata değil. Bu ikinci durum sinsidir: rapor sessizce boş hücre gösterir.

Korelasyonlu alt sorgu, dış sorgunun sütununa başvuran alt sorgudur (yukarıdaki 1 ve 4 numaralı örnekler). Yaygın inanış, bunun "dış sorgudaki her satır için bir kez çalıştığı"dır. Doğru değildir: iyileştirici korelasyonu çözer (decorrelation) ve genellikle bunu bir semi join ya da gruplu bir join'e çevirir; yani 1 milyon satır için 1 milyon sorgu çalışmaz. Bunu planda Nested Loops (Left Semi Join) ya da Hash Semi Join olarak görürsün.

Ama bir istisna vardır ve pahalıdır: alt sorgu bir skaler kullanıcı fonksiyonunun (scalar UDF) içindeyse iyileştirici içeriye bakamaz. SQL Server 2019 öncesinde böyle bir fonksiyon satır satır, yorumlanarak çalışır ve üstelik sorguyu seri plana zorlar — paralellik tamamen kaybolur. Milyon satırlık bir sorguda tek bir skaler UDF, süreyi saniyelerden dakikalara çıkarır. SQL Server 2019'un "scalar UDF inlining" özelliği çoğu durumda bunu otomatik çözer ama her fonksiyon uygun değildir. Pratik kural: skaler UDF'yi SELECT listesinde ya da WHERE'de kullanma; yerine satır döndüren satır içi tablo değerli fonksiyon (CREATE FUNCTION ... RETURNS TABLE AS RETURN SELECT ...) yaz ve CROSS APPLY ile bağla. PostgreSQL tarafında karşılığı, fonksiyonun VOLATILE yerine STABLE/IMMUTABLE işaretlenmesi ve mümkünse LANGUAGE sql ile yazılıp satır içine alınabilmesidir.

IN, EXISTS, JOIN — hangisi?

AmaçYazımNot
Eşleşme var mıEXISTSİlk eşleşmede durur, satırı çoğaltmaz
Eşleşme yok muNOT EXISTSNULL'a dayanıklı — NOT IN değil (3. ders)
Sağ taraftan sütun da lazımJOINÇoğaltma riskine dikkat (fan-out)
Küçük sabit listeIN (1,2,3)Uzun listelerde plan çeşitlenmesi (2. ders)

ANY/SOME ve ALL operatörleri de vardır (> ALL (alt sorgu) = "hepsinden büyük") ama okunması zordur ve ALL boş kümede TRUE döndürür — genelde MAX/MIN ile yeniden yazılır.

03CTE: okunabilirlik ve motorun farklı davranışı

Ortak tablo ifadesi (Common Table Expression), sorgunun başına yazılan adlandırılmış bir alt sorgudur. İç içe geçmiş türetilmiş tabloları düz, okunur adımlara çevirir:

WITH SehirToplam AS (
    SELECT Sehir, SUM(Tutar) AS Toplam, COUNT(*) AS Adet
    FROM   Siparis
    WHERE  Olusturma >= '2026-01-01'
    GROUP  BY Sehir
),
Ortalama AS (
    SELECT AVG(Toplam) AS GenelOrtalama FROM SehirToplam
)
SELECT s.Sehir, s.Toplam, s.Adet
FROM   SehirToplam s CROSS JOIN Ortalama o
WHERE  s.Toplam > o.GenelOrtalama
ORDER  BY s.Toplam DESC;

CTE'nin faydası okunabilirliktir: adım adım düşünürsün, her adımı ayrı ayrı test edebilirsin. Ama performans aracı değildir ve iki motor onu bambaşka ele alır:

MotorCTE ne yapar
SQL ServerAsla materyalize edilmez. Sorgu metnine yerleştirilir (makro gibi). CTE'ye iki kez başvurursan alttaki sorgu iki kez çalışır.
PostgreSQL ≤ 11Her zaman materyalize edilir. CTE bir "iyileştirme bariyeriydi": dıştaki WHERE koşulu içeri itilemezdi.
PostgreSQL 12+Yan etkisiz, özyinelemesiz ve bir kez başvurulan CTE varsayılan olarak satır içine alınır; MATERIALIZED / NOT MATERIALIZED ile davranış zorlanabilir.

SQL Server'ın davranışını kanıtlamak kolaydır:

WITH x AS (SELECT NEWID() AS g)
SELECT * FROM x
UNION ALL
SELECT * FROM x;
-- İki satırda FARKLI GUID gelir → CTE iki kez çalıştı.

Sonucu şudur: pahalı bir hesabı CTE'ye alıp birden çok yerde kullanmak hızlandırmaz, yavaşlatır. Gerçekten bir kez hesaplanmasını istiyorsan sonucu bir yere yazmalısın:

-- SQL Server: geçici tabloya al (istatistik de üretilir)
SELECT Sehir, SUM(Tutar) AS Toplam INTO #SehirToplam FROM Siparis GROUP BY Sehir;
CREATE INDEX IX_tmp ON #SehirToplam(Sehir);
-- ... #SehirToplam'ı istediğin kadar kullan

-- PostgreSQL 12+: CTE'yi materyalize etmeye zorla
WITH sehir_toplam AS MATERIALIZED (
    SELECT sehir, sum(tutar) AS toplam FROM siparis GROUP BY sehir
) SELECT ... ;

Geçici tablo mu, tablo değişkeni mi (SQL Server)? Bu, mülakatın klasik derinlik sorusudur:

ÖlçütGeçici tablo #tTablo değişkeni @t
İstatistikVar — iyileştirici gerçekçi tahmin yaparYok — tahmin genelde 1 satır
İndeksSonradan eklenebilirYalnız tanımda (birincil/benzersiz kısıt)
KapsamOturum/toplu iş; iç içe yordamlarda görünürYalnız tanımlandığı toplu iş
İşlem davranışıGeri alınırROLLBACK'ten etkilenmez
Yeniden derlemeTetiklerTetiklemez

"1 satır tahmini" tek başına yavaş sorguların büyük bir kaynağıdır: iyileştirici tablo değişkeninde 5 satır olduğunu sanıp nested loops seçer, gerçekte 500 000 satır vardır ve sorgu durur. SQL Server 2019'un tablo değişkeni ertelenmiş derlemesi bu tahmini gerçek satır sayısıyla yapar ve sorunun büyük kısmını çözer; daha eski sürümlerde çözüm OPTION (RECOMPILE) eklemek ya da geçici tabloya geçmektir. Pratik kural: birkaç yüz satıra kadar tablo değişkeni, üstünde geçici tablo. PostgreSQL'de tablo değişkeni kavramı yoktur; geçici tablolar (CREATE TEMP TABLE) kullanılır ve onların da ANALYZE edilmesi gerekebilir.

04Özyinelemeli CTE

Hiyerarşi (organizasyon şeması, kategori ağacı, malzeme listesi) SQL'de özyinelemeli CTE ile gezilir. Yapı her zaman aynıdır: çapa sorgusu + UNION ALL + kendine başvuran özyineleme adımı.

-- SQL Server
WITH Hiyerarsi AS (
    -- Çapa: kökler
    SELECT Id, Ad, YoneticiId, 0 AS Seviye,
           CAST(Ad AS nvarchar(4000)) AS Yol
    FROM   dbo.Calisan WHERE YoneticiId IS NULL

    UNION ALL

    -- Özyineleme: bir önceki seviyenin altları
    SELECT c.Id, c.Ad, c.YoneticiId, h.Seviye + 1,
           CAST(h.Yol + N' > ' + c.Ad AS nvarchar(4000))
    FROM   dbo.Calisan c
    JOIN   Hiyerarsi h ON h.Id = c.YoneticiId
)
SELECT Seviye, Yol, Ad FROM Hiyerarsi
ORDER BY Yol
OPTION (MAXRECURSION 200);            -- varsayılan 100'dür, 0 = sınırsız
-- PostgreSQL: RECURSIVE anahtar kelimesi ZORUNLUDUR
WITH RECURSIVE hiyerarsi AS (
    SELECT id, ad, yonetici_id, 0 AS seviye, ARRAY[id] AS yol
    FROM   calisan WHERE yonetici_id IS NULL
  UNION ALL
    SELECT c.id, c.ad, c.yonetici_id, h.seviye + 1, h.yol || c.id
    FROM   calisan c JOIN hiyerarsi h ON h.id = c.yonetici_id
    WHERE  NOT c.id = ANY(h.yol)      -- DÖNGÜ KORUMASI: kendi yolunda tekrar geçme
)
SELECT seviye, yol, ad FROM hiyerarsi ORDER BY yol;

İki motor arasındaki en önemli fark güvenliktir. SQL Server'da özyineleme varsayılan olarak 100 adımda durur ve hata verir — istemeden yazdığın sonsuz döngü seni kilitlemez, sadece uyarır. PostgreSQL'de böyle bir varsayılan sınır yoktur: veride bir döngü varsa (A'nın yöneticisi B, B'nin yöneticisi A) sorgu sonsuza kadar çalışır ve belleği tüketir. Korunma yolları: yukarıdaki gibi ziyaret edilen kimlikleri bir dizide taşımak, UNION ALL yerine UNION kullanmak (yinelenenleri eleyerek döngüyü kırar, ama daha pahalıdır) ya da PostgreSQL 14'ten beri var olan CYCLE yan tümcesini kullanmaktır.

Özyinelemeli CTE'nin ikinci kullanımı seri üretmektir (tarih listesi, sayı listesi):

-- PostgreSQL: yerleşik ve çok daha hızlı
SELECT generate_series('2026-01-01'::date, '2026-12-31'::date, '1 day') AS gun;

-- SQL Server 2022+
SELECT DATEADD(day, value, '2026-01-01') FROM GENERATE_SERIES(0, 364);

-- Daha eski SQL Server: özyinelemeli CTE ya da sayı tablosu
WITH Sayilar AS (
    SELECT 0 AS n UNION ALL SELECT n + 1 FROM Sayilar WHERE n < 364
) SELECT DATEADD(day, n, '2026-01-01') FROM Sayilar OPTION (MAXRECURSION 400);

Üretimde sık kullanılan bir sayı/takvim tablosunu kalıcı tutmak, her seferinde üretmekten hem hızlı hem de plan açısından öngörülebilirdir — 4. dersteki "boş ay raporda görünmüyor" sorununun standart çözümü de budur.

05Pencere fonksiyonları

Pencere fonksiyonu, satırları gruplamadan her satıra o satırın ait olduğu küme hakkında bilgi ekler. GROUP BY satır sayısını azaltır; OVER azaltmaz.

SELECT s.Id, s.MusteriId, s.Tutar, s.Olusturma,
       ROW_NUMBER() OVER (PARTITION BY s.MusteriId ORDER BY s.Olusturma DESC) AS Sira,
       SUM(s.Tutar) OVER (PARTITION BY s.MusteriId)                          AS MusteriToplami,
       s.Tutar - LAG(s.Tutar) OVER (PARTITION BY s.MusteriId ORDER BY s.Olusturma) AS Fark,
       RANK()       OVER (ORDER BY s.Tutar DESC) AS TutarSirasi,
       DENSE_RANK() OVER (ORDER BY s.Tutar DESC) AS TutarSirasiBosluksuz
FROM   Siparis s;
FonksiyonEşit değerlerde davranışıÖrnek çıktı (100, 90, 90, 80)
ROW_NUMBER()Eşitlik tanımaz, her satıra ayrı numara1, 2, 3, 4
RANK()Eşitlere aynı sıra, sonra boşluk bırakır1, 2, 2, 4
DENSE_RANK()Eşitlere aynı sıra, boşluk bırakmaz1, 2, 2, 3
NTILE(4)Satırları eşit büyüklükte kovalara böler1, 2, 3, 4

ROW_NUMBER'ın ORDER BY'ı kararlı değilse (eşit değerli satırlar varsa ve ayırt edici bir sütun eklenmemişse) numaralar sorgudan sorguya değişir — sayfalamada ve "grup başına ilk kayıt" sorgularında bu, kayıt kaybı demektir. 2. dersteki kararlı sıralama kuralı burada da geçerlidir.

Pencere fonksiyonu WHERE'de kullanılamaz. Mantıksal sırada SELECT aşamasında hesaplanır, yani WHERE, GROUP BY ve HAVING çalışırken henüz yoktur. Filtrelemek için bir katman sarmalarsın — "grup başına ilk N" kalıbının standart yazımı budur:

WITH Sirali AS (
    SELECT s.*, ROW_NUMBER() OVER (PARTITION BY s.MusteriId
                                   ORDER BY s.Olusturma DESC, s.Id DESC) AS rn
    FROM Siparis s
)
SELECT * FROM Sirali WHERE rn <= 3;      -- her müşterinin son 3 siparişi

Aynı sonucu 3. derste APPLY/LATERAL ile de üretmiştik; seçim ölçütü grup sayısıydı. PostgreSQL'de grup başına tek satır isteniyorsa DISTINCT ON en kısa yoldur (4. ders). Bazı veritabanlarında bulunan QUALIFY yan tümcesi ne SQL Server'da ne PostgreSQL'de vardır — sarmalama zorunludur.

Yürüyen toplam (running total) pencere fonksiyonlarının en yaygın kullanımıdır ve bir sonraki bölümdeki tuzağın da sahnesidir:

SELECT Tarih, Tutar,
       SUM(Tutar) OVER (ORDER BY Tarih
                        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS YuruyenToplam
FROM   GunlukSatis;

06Çerçeve (frame) ve performans

Bu bölüm, bu dersin en çok işe yarayan bilgisidir. Bir pencere fonksiyonunda ORDER BY yazıp çerçeveyi belirtmezsen, varsayılan çerçeve şudur:

RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW

ROWS değil, RANGE. Aradaki fark eşit değerli satırlarda ortaya çıkar: ROWS fiziksel satır sayar, RANGE ise ORDER BY değeri aynı olan satırların hepsini (peers) tek bir birim sayar ve o grubun tamamını dâhil eder.

TarihTutarROWS ile yürüyen toplamRANGE ile yürüyen toplam
01.01100100100
02.0150150250
02.01100250250
03.0130280280

Aynı tarihe ait iki satır RANGE ile aynı yürüyen toplamı gösterir — satır bazında ilerleyen bir rapor beklerken bu yanlış sonuçtur. Üstelik RANGE daha pahalıdır: SQL Server bunu bir çalışma tablosuyla (spool) çözer ve diske dokunur. Bu yüzden kural nettir: yürüyen toplamda her zaman ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW yaz. ORDER BY hiç yazmazsan çerçeve tüm bölüm olur — PARTITION BY'lı toplamlarda istenen davranış da budur.

LAST_VALUE tuzağı aynı varsayılandan doğar:

-- Beklenen: bölümdeki SON değer. Gerçekte: HER SATIRDA KENDİSİ.
LAST_VALUE(Tutar) OVER (PARTITION BY MusteriId ORDER BY Olusturma)

-- Doğrusu: çerçeveyi bölümün sonuna kadar aç
LAST_VALUE(Tutar) OVER (PARTITION BY MusteriId ORDER BY Olusturma
                        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)

FIRST_VALUE aynı hatayı yapmaz (varsayılan çerçeve zaten başı kapsar), bu yüzden sorun yalnız LAST_VALUE'da fark edilir ve genelde geç fark edilir.

Performans: POC indeksi. Pencere fonksiyonu, bölüm ve sıralama gereksinimi nedeniyle planda bir Sort doğurur. Bu sıralamayı ortadan kaldırmanın yolu, indeksin şu üç şeyi karşılamasıdır — kısaltması POC'dur:

  • Partitioning — indeksin ilk sütunları PARTITION BY sütunları
  • Ordering — sonra ORDER BY sütunları, aynı yönde
  • Covering — sorgunun kullandığı diğer sütunlar INCLUDE ile eklenir
-- ROW_NUMBER() OVER (PARTITION BY MusteriId ORDER BY Olusturma DESC) için
CREATE INDEX IX_Siparis_POC
    ON dbo.Siparis (MusteriId, Olusturma DESC)
    INCLUDE (Tutar, Durum);
-- PostgreSQL karşılığı
CREATE INDEX ix_siparis_poc ON siparis (musteri_id, olusturma DESC) INCLUDE (tutar, durum);

Bu indeks eklendiğinde plandaki Sort işlemcisi kaybolur ve büyük tablolarda süre kat kat düşer. INCLUDE sütunları indeksin anahtarına girmez, yalnız yaprak seviyesinde taşınır — böylece anahtar dar kalır. Ayrıntısı 9. dersin konusudur.

Son bir kıyas: pencere fonksiyonu mu GROUP BY mı? İkisi rakip değildir. GROUP BY satırları azaltır; pencere fonksiyonu detayı korurken özet ekler. "Her siparişi göster, yanında müşterinin toplamını da yaz" istendiğinde GROUP BY ile bunu yapmanın yolu tabloyu ikinci kez okuyup join'lemektir; pencere fonksiyonu bunu tek geçişte yapar. Tersine, yalnız özet isteniyorsa GROUP BY daha ucuzdur çünkü daha az satır üretir. Pencere fonksiyonları mantıksal sırada GROUP BY'dan sonra çalıştığı için ikisi birlikte de kullanılabilir: gruplanmış sonuç üzerinde sıralama numarası vermek gibi.

07Mini alıştırma — Üç aracı da sınır durumunda dene

Yaklaşık 20 dakika · 1. dersteki şema üzerinde

  1. Veri kur: 5000 sipariş üret; bazı müşterilere aynı tarihte birden çok sipariş ver (eşit değerli satırlar lazım olacak). Ayrıca Calisan(Id, Ad, YoneticiId) tablosu kurup 4 seviyeli bir hiyerarşi oluştur.
  2. Skaler alt sorgu: her müşterinin sipariş sayısını skaler alt sorguyla getir, sonra aynı raporu LEFT JOIN + GROUP BY ile yaz. Planları karşılaştır — motor korelasyonu çözdü mü?
  3. CTE iki kez (SQL Server): WITH x AS (SELECT NEWID() g) SELECT * FROM x UNION ALL SELECT * FROM x; çalıştır. GUID'ler aynı mı?
  4. CTE maliyeti: pahalı bir toplama sorgusunu CTE'ye alıp iki kez kullan ve süreyi ölç. Aynı işi geçici tabloya (SQL Server) ya da AS MATERIALIZED (PostgreSQL 12+) ile yapıp tekrar ölç.
  5. Tablo değişkeni (SQL Server): 200 000 satırı bir tablo değişkenine doldurup join'le ve planda tahmini satır sayısına bak. Aynısını #temp tablosuyla tekrarla.
  6. Özyineleme: hiyerarşiyi özyinelemeli CTE ile listele; seviye ve yol sütunu üret. Sonra bir çalışanın yöneticisini kendi astı yaparak döngü oluştur ve sorguyu tekrar çalıştır. SQL Server ne dedi? PostgreSQL ne yaptı?
  7. Döngü koruması: PostgreSQL'de yol dizisi kontrolünü ekleyip tekrarla.
  8. Sıralama fonksiyonları: aynı tutara sahip birkaç sipariş üzerinde ROW_NUMBER, RANK ve DENSE_RANK sonuçlarını yan yana getir.
  9. Çerçeve farkı: aynı tarihe ait iki satırın bulunduğu veri üzerinde yürüyen toplamı önce çerçeve yazmadan, sonra ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ile hesapla. Değerler farklı mı?
  10. LAST_VALUE: çerçeve yazmadan LAST_VALUE kullan — her satırda ne döndü? Çerçeveyi UNBOUNDED FOLLOWING'e açıp tekrarla.
  11. POC indeksi: ROW_NUMBER() OVER (PARTITION BY MusteriId ORDER BY Olusturma DESC) sorgusunun planına bak, Sort var mı? POC indeksini açıp tekrar bak.

08Kendini sına

Korelasyonlu alt sorgu her satır için bir kez mi çalışır?

Genellikle hayır. Korelasyonlu alt sorgu, dış sorgunun sütununa başvuran alt sorgudur ve mantıksal anlamı "her satır için değerlendir"dir; ancak iyileştirici bu korelasyonu çözer (decorrelation) ve çoğu durumda semi join, anti join ya da gruplu bir join'e dönüştürür — planda Left Semi Join olarak görünür. Yani EXISTS ya da skaler alt sorgu yazmak, otomatik olarak yavaş demek değildir; okunabilirlik açısından çoğu zaman tercih edilir. Bu dönüşümün yapılamadığı önemli bir durum vardır: alt sorgu bir skaler kullanıcı fonksiyonunun içindeyse iyileştirici içeriye bakamaz. SQL Server 2019 öncesinde böyle bir fonksiyon satır satır, yorumlanarak çalışır ve sorguyu seri plana zorlar; milyon satırlık bir sorguda tek bir skaler UDF süreyi dakikalara çıkarır. 2019'un "scalar UDF inlining" özelliği çoğu durumu çözer ama her fonksiyon uygun değildir; sağlam yol, satır içi tablo değerli fonksiyon yazıp CROSS APPLY ile bağlamaktır.

CTE bir performans aracı mıdır? İki motorda nasıl davranır?

CTE öncelikle bir okunabilirlik aracıdır; performans aracı değildir ve motorlar onu farklı ele alır. SQL Server CTE'yi asla materyalize etmez: sorgu metnine makro gibi yerleştirir, dolayısıyla CTE'ye iki kez başvurursan alttaki sorgu iki kez çalışır. Bunu WITH x AS (SELECT NEWID() g) SELECT * FROM x UNION ALL SELECT * FROM x; ile kanıtlayabilirsin — iki farklı GUID gelir. PostgreSQL 11 ve öncesinde tam tersini yapardı: her CTE materyalize edilir ve bir "iyileştirme bariyeri" oluştururdu, dıştaki koşullar içeri itilemezdi. 12'den itibaren yan etkisiz, özyinelemesiz ve bir kez başvurulan CTE'ler varsayılan olarak satır içine alınır; MATERIALIZED / NOT MATERIALIZED ile davranış zorlanabilir. Pahalı bir ara sonucun gerçekten bir kez hesaplanmasını istiyorsan SQL Server'da geçici tabloya yazarsın (üstelik istatistik de üretilir), PostgreSQL'de AS MATERIALIZED dersin.

Özyinelemeli CTE nasıl yazılır, hangi risk vardır?

Yapı üç parçadır: kökleri seçen çapa sorgusu, UNION ALL, ve CTE'nin kendisine başvuran özyineleme adımı. PostgreSQL'de WITH RECURSIVE yazmak zorunludur, SQL Server'da ayrı bir anahtar kelime yoktur. Pratikte seviye sayacı ve yol sütunu da taşınır; yol sütunu hem sıralama hem döngü tespiti için kullanılır. Risk sonsuz döngüdür: veride çevrim varsa (A'nın yöneticisi B, B'nin yöneticisi A) özyineleme durmaz. SQL Server bu konuda korumalıdır — varsayılan MAXRECURSION 100'dür ve aşılınca hata verir (0 yazılırsa sınırsız olur). PostgreSQL'de varsayılan bir sınır yoktur: sorgu belleği tüketene kadar çalışır. Korunma yolları, ziyaret edilen kimlikleri bir dizide taşıyıp WHERE NOT id = ANY(yol) koşulu yazmak, UNION ALL yerine UNION kullanmak ya da PostgreSQL 14 ile gelen CYCLE yan tümcesini kullanmaktır.

ROW_NUMBER, RANK ve DENSE_RANK farkı nedir? Pencere fonksiyonu WHERE'de kullanılabilir mi?

Üçü de sıralama numarası üretir, farkları eşit değerlerde ortaya çıkar. 100, 90, 90, 80 değerleri için: ROW_NUMBER eşitliği tanımaz ve 1, 2, 3, 4 verir; RANK eşitlere aynı sırayı verip ardından boşluk bırakır (1, 2, 2, 4); DENSE_RANK aynı sırayı verir ama boşluk bırakmaz (1, 2, 2, 3). ROW_NUMBER kullanırken ORDER BY'ın kararlı olması gerekir — ayırt edici bir sütun eklenmezse numaralar sorgudan sorguya değişir ve "grup başına ilk N" sorguları kayıt kaçırır. Pencere fonksiyonları WHERE'de kullanılamaz: mantıksal sırada SELECT aşamasında hesaplanırlar, yani WHERE, GROUP BY ve HAVING çalışırken henüz yokturlar. Filtrelemek için sorguyu bir CTE ya da türetilmiş tabloyla sarmalayıp dış katmanda WHERE rn <= 3 yazmak gerekir; bazı veritabanlarındaki QUALIFY yan tümcesi ne SQL Server'da ne PostgreSQL'de vardır. Alternatifler: APPLY/LATERAL (grup sayısı azsa daha hızlı) ve PostgreSQL'de grup başına tek satır için DISTINCT ON.

Pencere çerçevesinde RANGE ile ROWS farkı nedir?

OVER içinde ORDER BY yazıp çerçeve belirtmezsen varsayılan RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW'dur — ROWS değil. ROWS fiziksel satır sayar; RANGE ise ORDER BY değeri aynı olan satırların hepsini tek bir birim (peer grubu) sayar ve o grubun tamamını çerçeveye dâhil eder. Sonuç: aynı tarihe ait iki satır varsa yürüyen toplam ikisinde de aynı, "atlamış" bir değer gösterir — satır bazında ilerleyen bir rapor beklerken bu yanlıştır. RANGE ayrıca daha pahalıdır; SQL Server bunu bir çalışma tablosuyla çözer. Bu yüzden yürüyen toplamda her zaman ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW yazılır. Aynı varsayılandan doğan ikinci tuzak LAST_VALUE'dur: çerçeve geçerli satırda bittiği için her satırda kendisini döndürür; bölümün son değerini almak için çerçeveyi ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ile açmak gerekir. Performans tarafında ise PARTITION BY + ORDER BY sütunlarını sırasıyla içeren ve diğer sütunları INCLUDE eden bir POC indeksi, plandaki Sort işlemcisini tamamen kaldırır.

09Sınav köşesi

Yazılıda nasıl sorulur

(1) Alt sorgu türleri ve nerede kullanılabildikleri. (2) Korelasyonlu alt sorgu nedir, örnek veriniz. (3) IN, EXISTS ve JOIN karşılaştırması. (4) CTE nedir, türetilmiş tablodan farkı. (5) Özyinelemeli CTE ile hiyerarşi listeleyen sorgu yazınız — klasik. (6) İkinci en yüksek maaşı bulan sorguyu yazınız (üç farklı yöntemle). (7) ROW_NUMBER, RANK, DENSE_RANK farkları. (8) Her gruptaki ilk N kaydı getiren sorgu. (9) Yürüyen toplam hesaplayan sorgu. (10) Yinelenen kayıtları ROW_NUMBER ile silen sorgu.

Sözlüde nasıl anlatılır — 2 dakikalık taslak

Düzen: (1) Sıralama — "alt sorgu okunabilirlik için iç içe yazılan sorgudur; CTE aynı şeyi adlandırılmış adımlara böler ve karmaşık raporları okunur hâle getirir; pencere fonksiyonu ise satırları azaltmadan her satıra küme bilgisi ekler." (2) Kullanım — "grup başına ilk N için ROW_NUMBER ile numaralandırıp CTE'de sarmalarım, çünkü pencere fonksiyonu WHERE'de kullanılamaz." (3) Motor bilgisi — "CTE'yi performans aracı sanmam: SQL Server CTE'yi materyalize etmez, iki kez referans verirsem alttaki sorgu iki kez çalışır; gerçekten bir kez hesaplanmasını istersem geçici tabloya alırım. PostgreSQL'de 12 öncesi tam tersiydi, her CTE bir iyileştirme bariyeriydi." (4) Kıdem vuruşu — "pencere fonksiyonlarında varsayılan çerçeve RANGE'dir, ROWS değil; eşit değerli satırlarda yürüyen toplamı yanlış üretir ve daha pahalıdır, bu yüzden çerçeveyi her zaman açıkça yazarım. LAST_VALUE da aynı sebeple çerçeve açılmadan kendisini döndürür." (5) Performans — "pencere sorgularında PARTITION BY + ORDER BY sütunlarını içeren ve gerisini INCLUDE eden bir indeks açarak plandaki sıralamayı tamamen kaldırırım."