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
MATERIALIZEDanahtar kelimesi - Geçici tablo ile tablo değişkeni arasındaki istatistik farkı
- Özyinelemeli CTE,
MAXRECURSIONve sonsuz döngüden korunma ROW_NUMBER/RANK/DENSE_RANK,LAG/LEAD, yürüyen toplamRANGEileROWSçerçeve farkı veLAST_VALUEtuzağı- 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ım | Not |
|---|---|---|
| Eşleşme var mı | EXISTS | İlk eşleşmede durur, satırı çoğaltmaz |
| Eşleşme yok mu | NOT EXISTS | NULL'a dayanıklı — NOT IN değil (3. ders) |
| Sağ taraftan sütun da lazım | JOIN | Çoğaltma riskine dikkat (fan-out) |
| Küçük sabit liste | IN (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:
| Motor | CTE ne yapar |
|---|---|
| SQL Server | Asla materyalize edilmez. Sorgu metnine yerleştirilir (makro gibi). CTE'ye iki kez başvurursan alttaki sorgu iki kez çalışır. |
| PostgreSQL ≤ 11 | Her 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çüt | Geçici tablo #t | Tablo değişkeni @t |
|---|---|---|
| İstatistik | Var — iyileştirici gerçekçi tahmin yapar | Yok — tahmin genelde 1 satır |
| İndeks | Sonradan eklenebilir | Yalnız tanımda (birincil/benzersiz kısıt) |
| Kapsam | Oturum/toplu iş; iç içe yordamlarda görünür | Yalnız tanımlandığı toplu iş |
| İşlem davranışı | Geri alınır | ROLLBACK'ten etkilenmez |
| Yeniden derleme | Tetikler | Tetiklemez |
"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;
| Fonksiyon | Eşit değerlerde davranışı | Örnek çıktı (100, 90, 90, 80) |
|---|---|---|
ROW_NUMBER() | Eşitlik tanımaz, her satıra ayrı numara | 1, 2, 3, 4 |
RANK() | Eşitlere aynı sıra, sonra boşluk bırakır | 1, 2, 2, 4 |
DENSE_RANK() | Eşitlere aynı sıra, boşluk bırakmaz | 1, 2, 2, 3 |
NTILE(4) | Satırları eşit büyüklükte kovalara böler | 1, 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.
| Tarih | Tutar | ROWS ile yürüyen toplam | RANGE ile yürüyen toplam |
|---|---|---|---|
| 01.01 | 100 | 100 | 100 |
| 02.01 | 50 | 150 | 250 |
| 02.01 | 100 | 250 | 250 |
| 03.01 | 30 | 280 | 280 |
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 BYsütunları - Ordering — sonra
ORDER BYsütunları, aynı yönde - Covering — sorgunun kullandığı diğer sütunlar
INCLUDEile 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
- 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. - Skaler alt sorgu: her müşterinin sipariş sayısını skaler alt sorguyla getir,
sonra aynı raporu
LEFT JOIN+GROUP BYile yaz. Planları karşılaştır — motor korelasyonu çözdü mü? - 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ı? - 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ç. - 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ı
#temptablosuyla tekrarla. - Ö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ı?
- Döngü koruması: PostgreSQL'de yol dizisi kontrolünü ekleyip tekrarla.
- Sıralama fonksiyonları: aynı tutara sahip birkaç sipariş üzerinde
ROW_NUMBER,RANKveDENSE_RANKsonuçlarını yan yana getir. - Ç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 ROWile hesapla. Değerler farklı mı? LAST_VALUE: çerçeve yazmadanLAST_VALUEkullan — her satırda ne döndü? ÇerçeveyiUNBOUNDED FOLLOWING'e açıp tekrarla.- POC indeksi:
ROW_NUMBER() OVER (PARTITION BY MusteriId ORDER BY Olusturma DESC)sorgusunun planına bak,Sortvar 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."
