MariaDB ve MySQL’de WITH (CTE) ile Ortak Tablo İfadesi Yazma
Veritabanı sorgularında karmaşıklık arttıkça, SQL kodunun okunabilirliği ve yönetilebilirliği ciddi sorun haline gelir. İç içe geçmiş alt sorgular, tekrar eden kod blokları ve uzayan JOIN zincirleri bakımı zorlaştırır. İşte tam bu noktada WITH ifadesiyle oluşturulan Ortak Tablo İfadeleri (CTE – Common Table Expressions) devreye girer. MariaDB 10.2.1 ve MySQL 8.0 ile birlikte standart olarak desteklenen bu özellik, karmaşık sorguları daha okunabilir, modüler ve yönetilebilir hale getirir.
CTE Nedir ve Neden Kullanılır?
CTE, bir sorgu içinde geçici olarak tanımlanmış, isimlendirilmiş bir sonuç kümesidir. Sanki bir görünüm (view) tanımlıyorsunuz ama bunu kalıcı yapmak yerine yalnızca o sorgu için geçici tutuyorsunuz. Alt sorguların aksine, CTE’ler tanımlandıktan sonra ana sorguda birden fazla kez referans alınabilir. Bu büyük avantaj sağlar.
CTE kullanmanın temel gerekçeleri şunlardır:
- Okunabilirlik: Karmaşık sorguları mantıksal bloklara böler
- Tekrar kullanım: Aynı alt sorguyu birden fazla kez yazmak yerine bir kez tanımlarsınız
- Hata ayıklama kolaylığı: Her CTE bloğunu ayrı ayrı test edebilirsiniz
- Özyinelemeli sorgular: Hiyerarşik verileri (organizasyon şeması, kategori ağaçları) işlemek için idealdir
Temel CTE Sözdizimi
CTE’nin temel yapısı oldukça sezgiseldir. WITH anahtar kelimesiyle başlar, CTE’ye bir isim verilir, ardından AS ile sorgu tanımlanır.
WITH cte_adi AS (
SELECT kolon1, kolon2
FROM tablo
WHERE kosul
)
SELECT *
FROM cte_adi;
Basit bir örnekle başlayalım. Diyelim ki bir e-ticaret sisteminde 500 TL üzerinde sipariş veren müşterileri bulmak istiyoruz:
WITH yuksek_degerli_siparisler AS (
SELECT
musteri_id,
SUM(toplam_tutar) AS toplam_harcama
FROM siparisler
WHERE siparis_tarihi >= '2024-01-01'
GROUP BY musteri_id
HAVING SUM(toplam_tutar) > 500
)
SELECT
m.musteri_adi,
m.email,
yds.toplam_harcama
FROM musteriler m
INNER JOIN yuksek_degerli_siparisler yds
ON m.id = yds.musteri_id
ORDER BY yds.toplam_harcama DESC;
Bu sorguyu CTE olmadan yazsaydık, HAVING koşulunu içeren alt sorguyu doğrudan FROM bloğuna gömmek zorunda kalacaktık ve kod hızla karmaşık bir hal alacaktı.
Birden Fazla CTE Kullanımı
Tek bir WITH bloğunda birden fazla CTE tanımlayabilirsiniz. Bu özellikle çok adımlı hesaplamalar için son derece kullanışlıdır. CTE’leri virgülle ayırmanız yeterlidir.
Gerçek dünya senaryosu olarak bir insan kaynakları sistemini ele alalım. Departman bazında ortalama maaşları hesaplayıp, bu ortalamanın üzerinde kazanan çalışanları listelemek isteyelim:
WITH departman_ortalamalari AS (
SELECT
departman_id,
AVG(maas) AS ort_maas,
COUNT(*) AS calisan_sayisi
FROM calisanlar
WHERE aktif = 1
GROUP BY departman_id
),
yuksek_maasli_calisanlar AS (
SELECT
c.id,
c.ad,
c.soyad,
c.maas,
c.departman_id
FROM calisanlar c
INNER JOIN departman_ortalamalari da
ON c.departman_id = da.departman_id
WHERE c.maas > da.ort_maas
)
SELECT
d.departman_adi,
ymc.ad,
ymc.soyad,
ymc.maas,
doa.ort_maas AS departman_ortalamasi,
ROUND((ymc.maas - doa.ort_maas), 2) AS ortalama_farki
FROM yuksek_maasli_calisanlar ymc
INNER JOIN departmanlar d ON ymc.departman_id = d.id
INNER JOIN departman_ortalamalari doa ON ymc.departman_id = doa.departman_id
ORDER BY d.departman_adi, ymc.maas DESC;
Dikkat edin: ikinci CTE olan yuksek_maasli_calisanlar, birinci CTE olan departman_ortalamalari‘nı referans alıyor. CTE’ler sıralı olarak tanımlanır ve sonraki CTE’ler önceki CTE’lere referans verebilir.
Sütun İsimlendirme ile CTE
CTE tanımında sütun isimlerini açıkça belirtmek mümkündür. Bu özellikle hesaplanan sütunlar veya birleştirilmiş sorgularda kullanışlıdır:
WITH aylik_satislar (yil, ay, toplam_satis, siparis_adedi) AS (
SELECT
YEAR(siparis_tarihi),
MONTH(siparis_tarihi),
SUM(tutar),
COUNT(*)
FROM siparisler
GROUP BY
YEAR(siparis_tarihi),
MONTH(siparis_tarihi)
)
SELECT
yil,
ay,
toplam_satis,
siparis_adedi,
ROUND(toplam_satis / siparis_adedi, 2) AS ortalama_siparis_tutari,
LAG(toplam_satis) OVER (ORDER BY yil, ay) AS onceki_ay_satis,
ROUND(
((toplam_satis - LAG(toplam_satis) OVER (ORDER BY yil, ay))
/ LAG(toplam_satis) OVER (ORDER BY yil, ay)) * 100,
2) AS degisim_yuzdesi
FROM aylik_satislar
ORDER BY yil, ay;
Bu sorguda sütun isimlerini CTE tanımında belirtip, ana sorguda pencere fonksiyonlarıyla (LAG) aylık satış değişimini hesaplıyoruz. MariaDB 10.2+ ve MySQL 8.0+ bu tür pencere fonksiyonlarını destekler.
Özyinelemeli CTE (Recursive CTE)
Özyinelemeli CTE’ler, hiyerarşik verileri işlemek için güçlü bir araçtır. Organizasyon şemaları, kategori ağaçları, bill of materials (BOM) yapıları bunların klasik kullanım alanlarıdır.
Bir organizasyon hiyerarşisini düşünelim. Her çalışanın bir yonetici_id alanı var ve kök düğüm için bu alan NULL:
WITH RECURSIVE org_hiyerarsisi AS (
-- Temel durum (anchor member): Kök çalışanlar
SELECT
id,
ad,
soyad,
unvan,
yonetici_id,
0 AS seviye,
CAST(ad AS CHAR(1000)) AS hiyerarsi_yolu
FROM calisanlar
WHERE yonetici_id IS NULL
UNION ALL
-- Özyinelemeli durum: Alt çalışanlar
SELECT
c.id,
c.ad,
c.soyad,
c.unvan,
c.yonetici_id,
oh.seviye + 1,
CONCAT(oh.hiyerarsi_yolu, ' > ', c.ad)
FROM calisanlar c
INNER JOIN org_hiyerarsisi oh ON c.yonetici_id = oh.id
)
SELECT
REPEAT(' ', seviye) AS girinti,
ad,
soyad,
unvan,
seviye,
hiyerarsi_yolu
FROM org_hiyerarsisi
ORDER BY hiyerarsi_yolu;
Özyinelemeli CTE’lerin yapısı iki bölümden oluşur:
- Temel durum (anchor member): Özyinelemenin başladığı ilk sorgu.
UNION ALL‘dan önce gelir. - Özyinelemeli durum (recursive member): CTE’nin kendisine referans veren,
UNION ALL‘dan sonra gelen sorgu.
MariaDB ve MySQL özyinelemeli CTE’lerde sonsuz döngüyü önlemek için @@cte_max_recursion_depth sistem değişkeni kullanır. Varsayılan değer 1000’dir. Gerekirse artırabilirsiniz:
SET SESSION cte_max_recursion_depth = 5000;
Gerçek Dünya Senaryosu: Stok Yönetim Sistemi
Bir stok yönetim sistemi için kapsamlı bir örnek yapalım. Düşük stoklu ürünleri, son 30 günlük satış hızını ve yeniden sipariş önerilerini hesaplayalım:
WITH son_30_gun_satislari AS (
SELECT
urun_id,
SUM(miktar) AS satilan_miktar,
COUNT(DISTINCT DATE(satis_tarihi)) AS satis_gunu_sayisi
FROM satis_detaylari sd
INNER JOIN satislar s ON sd.satis_id = s.id
WHERE s.satis_tarihi >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
GROUP BY urun_id
),
gunluk_satis_hizi AS (
SELECT
urun_id,
satilan_miktar,
satis_gunu_sayisi,
ROUND(satilan_miktar / NULLIF(satis_gunu_sayisi, 0), 2) AS gunluk_hiz
FROM son_30_gun_satislari
),
kritik_stok AS (
SELECT
u.id AS urun_id,
u.urun_adi,
u.stok_miktari,
u.minimum_stok,
u.tedarik_suresi_gun,
gsh.gunluk_hiz,
ROUND(gsh.gunluk_hiz * u.tedarik_suresi_gun, 0) AS tedarik_suresi_tuketim
FROM urunler u
LEFT JOIN gunluk_satis_hizi gsh ON u.id = gsh.urun_id
WHERE u.stok_miktari <= u.minimum_stok * 1.2
)
SELECT
urun_adi,
stok_miktari,
minimum_stok,
COALESCE(gunluk_hiz, 0) AS gunluk_satis_hizi,
tedarik_suresi_gun,
tedarik_suresi_tuketim,
GREATEST(
minimum_stok - stok_miktari + COALESCE(tedarik_suresi_tuketim, 0),
0
) AS onerilens_siparis_miktari,
CASE
WHEN stok_miktari = 0 THEN 'KRİTİK - STOK YOK'
WHEN stok_miktari < minimum_stok THEN 'ACİL SİPARİŞ'
ELSE 'DİKKAT - DÜŞÜK STOK'
END AS durum
FROM kritik_stok
ORDER BY
CASE
WHEN stok_miktari = 0 THEN 1
WHEN stok_miktari < minimum_stok THEN 2
ELSE 3
END,
stok_miktari ASC;
Bu sorgu üç aşamalı bir analiz yapıyor: önce son 30 günün satışlarını topluyor, günlük satış hızını hesaplıyor, ardından kritik stok seviyesindeki ürünleri belirleyip önerilen sipariş miktarını hesaplıyor.
CTE ile Veri Güncelleme
CTE’ler yalnızca SELECT sorgularıyla sınırlı değildir. UPDATE, DELETE ve hatta INSERT işlemlerinde de kullanılabilirler. Bu özellik özellikle karmaşık güncelleme senaryolarında hayat kurtarır.
Örneğin, belirli kriterlere göre müşteri segmentlerini güncelleyelim:
WITH musteri_segmentasyon AS (
SELECT
m.id,
SUM(s.toplam_tutar) AS toplam_harcama,
COUNT(s.id) AS siparis_sayisi,
MAX(s.siparis_tarihi) AS son_siparis_tarihi,
DATEDIFF(CURDATE(), MAX(s.siparis_tarihi)) AS son_siparis_gecen_gun
FROM musteriler m
LEFT JOIN siparisler s ON m.id = s.musteri_id
GROUP BY m.id
)
UPDATE musteriler m
INNER JOIN musteri_segmentasyon ms ON m.id = ms.id
SET m.segment = CASE
WHEN ms.toplam_harcama >= 10000 AND ms.son_siparis_gecen_gun <= 90 THEN 'ALTIN'
WHEN ms.toplam_harcama >= 3000 AND ms.son_siparis_gecen_gun <= 180 THEN 'GUMUS'
WHEN ms.siparis_sayisi >= 1 AND ms.son_siparis_gecen_gun <= 365 THEN 'BRONZ'
ELSE 'PASIF'
END,
m.son_guncelleme = NOW();
Not: MariaDB bu sözdizimini destekler. MySQL 8.0’da da çalışır ancak bazı durumlarda sözdizimi hafifçe farklılaşabilir.
CTE vs Alt Sorgular: Performans Değerlendirmesi
Bir sysadmin olarak “CTE mi yoksa alt sorgu mu kullanmalıyım?” sorusuyla kesinlikle karşılaşacaksınız. Birkaç önemli noktayı bilmek gerekiyor:
Materyalizasyon (Materialization) davranışı:
- MariaDB, CTE’leri varsayılan olarak materyalize eder. Yani CTE sonucu geçici bir tabloya yazılır ve birden fazla referansta tekrar hesaplanmaz. Bu, CTE’nin birden fazla yerde kullanıldığı durumlarda performans avantajı sağlar.
- MySQL 8.0, optimizer’ın kararına göre ya materyalize eder ya da CTE’yi inline (satır içi) olarak çalıştırır.
Ne zaman CTE tercih edilmeli:
- Aynı alt sorguyu birden fazla kez referans almanız gerektiğinde
- Özyinelemeli sorgularda (başka alternatif yok)
- Kodun okunabilirliği öncelikliyken
- Hata ayıklama sürecinde adım adım test ederken
Ne zaman alt sorgu veya view tercih edilmeli:
- Çok basit, tek kullanımlık sorgularda alt sorgu yeterlidir
- Farklı sorgularda tekrar kullanılacak karmaşık sorgular için kalıcı view daha uygun olabilir
EXPLAIN ile CTE Sorgu Analizi
CTE sorgularınızın nasıl çalıştığını anlamak için EXPLAIN kullanmak önemlidir:
EXPLAIN WITH departman_ozeti AS (
SELECT
departman_id,
COUNT(*) AS calisan_sayisi,
AVG(maas) AS ort_maas
FROM calisanlar
GROUP BY departman_id
)
SELECT
d.departman_adi,
dos.calisan_sayisi,
dos.ort_maas
FROM departmanlar d
INNER JOIN departman_ozeti dos ON d.id = dos.departman_id
ORDER BY dos.ort_maas DESC;
MariaDB 10.5+ ve MySQL 8.0’da EXPLAIN ANALYZE kullanarak gerçek çalışma sürelerini ve satır tahminlerini de görebilirsiniz. Bu bilgi sorgu optimizasyonu için son derece değerlidir.
CTE performansını etkileyen birkaç faktöre dikkat edin:
- İndeks kullanımı: CTE içindeki sorgular da indekslerden faydalanır. WHERE koşullarınızda indeksli sütunları kullanın.
- Gereksiz kolonlar: CTE’de yalnızca ihtiyacınız olan kolonları seçin,
SELECT *kullanmaktan kaçının. - Özyinelemeli CTE derinliği: Çok derin özyinelemeler bellek ve performans sorunlarına yol açabilir.
Pratik İpuçları ve Yaygın Hatalar
Üretim ortamında CTE kullanırken dikkat edilmesi gereken noktalar:
Döngü tuzağından kaçınma: Özyinelemeli CTE’lerde sonsuz döngü riski vardır. Hiyerarşik verinizde döngüsel referanslar varsa (A -> B -> C -> A gibi) sorgunuz çöker. Bunu önlemek için derinlik sınırı veya ziyaret edildi kontrolü ekleyin:
WITH RECURSIVE kategori_agaci AS (
SELECT
id,
kat_adi,
ust_kategori_id,
1 AS derinlik,
CAST(id AS CHAR(200)) AS ziyaret_edilen
FROM kategoriler
WHERE ust_kategori_id IS NULL
UNION ALL
SELECT
k.id,
k.kat_adi,
k.ust_kategori_id,
ka.derinlik + 1,
CONCAT(ka.ziyaret_edilen, ',', k.id)
FROM kategoriler k
INNER JOIN kategori_agaci ka ON k.ust_kategori_id = ka.id
WHERE ka.derinlik < 10
AND FIND_IN_SET(k.id, ka.ziyaret_edilen) = 0
)
SELECT * FROM kategori_agaci ORDER BY derinlik, kat_adi;
Diğer önemli ipuçları:
- CTE isimlerini anlamlı seçin:
cte1,cte2gibi isimler kodu anlaşılmaz yapar.aylik_satis_ozeti,aktif_musterilergibi açıklayıcı isimler kullanın. - CTE sayısını makul tutun: 10-15 CTE’yi aşan sorgular bakımı zorlaştırır. Bu noktada stored procedure veya view kullanmayı değerlendirin.
- Geçici tablolarla kıyaslama: Çok karmaşık CTE zincirlerinde ara sonuçları geçici tablolara yazmak performans açısından daha iyi sonuç verebilir. Üretimde her ikisini de test edin.
- MariaDB versiyon kontrolü: CTE’ler MariaDB 10.2.2’den itibaren desteklenir. Özyinelemeli CTE için de aynı versiyon gereklidir. Versiyon kontrolü için
SELECT VERSION();kullanın.
Sonuç
CTE’ler, MariaDB ve MySQL dünyasında veritabanı geliştiricileri ve sysadminler için gerçekten vazgeçilmez bir araç haline gelmiştir. Karmaşık raporlama sorgularını modüler bloklara ayırmak, hiyerarşik verileri zahmetsizce işlemek ve kod tekrarını ortadan kaldırmak için sunduğu olanaklar benzersizdir.
Özellikle şu durumlarda CTE’ye geçmek işleri önemli ölçüde kolaylaştırır: iç içe alt sorgulardan oluşan, okuması güç SQL dosyalarıyla boğuştuğunuzda, organizasyon şeması veya kategori ağacı gibi hiyerarşik verileri sorgulamanız gerektiğinde ve aynı hesaplama mantığını birden fazla noktada kullandığınızda.
Üretim ortamında CTE kullanmadan önce her zaman EXPLAIN ile sorgu planını kontrol edin, özyinelemeli CTE’lerde derinlik sınırını ve döngüsel referans kontrolünü ihmal etmeyin. Performans kritik sorgularda CTE ile geçici tablo yaklaşımını karşılaştırın ve ortamınıza en uygun çözümü seçin. Temiz, okunabilir SQL yazmak yalnızca geliştirici deneyimini iyileştirmekle kalmaz, uzun vadede bakım maliyetlerini de ciddi ölçüde düşürür.
