MariaDB ve MySQL’de Korelasyonlu Alt Sorgu (Correlated Subquery) Kullanımı

Veritabanı sorgularında çoğu zaman tek bir SELECT ifadesiyle işin içinden çıkamayız. Özellikle bir tablodaki veriyi, başka bir tablonun dinamik sonuçlarıyla karşılaştırmamız gerektiğinde işler karmaşıklaşmaya başlar. İşte tam bu noktada korelasyonlu alt sorgular devreye girer. Normal alt sorgulardan farklı olarak, korelasyonlu alt sorgular dış sorgunun her satırı için yeniden çalışır ve bu sayede son derece esnek, bağlama duyarlı sorgular yazmanızı sağlar. Bu yazıda MySQL ve MariaDB üzerinde korelasyonlu alt sorguları derinlemesine inceleyeceğiz, gerçek dünya senaryolarıyla pekiştireceğiz ve performans ipuçlarına da değineceğiz.

Korelasyonlu Alt Sorgu Nedir?

Normal (bağımsız) bir alt sorguda, iç sorgu dış sorgudan bağımsız olarak çalışır. Yani önce iç sorgu çalışır, sonucu dış sorguya aktarılır. Korelasyonlu alt sorguda ise iç sorgu, dış sorgunun her satırı için tekrar tekrar çalışır. Bu yüzden iç sorgu, dış sorgudaki bir sütuna referans verir.

Bunu şöyle hayal edebilirsiniz: Normal alt sorgu bir fabrikada toplu üretim gibidir, bir kez çalışır. Korelasyonlu alt sorgu ise sipariş üzerine üretim gibidir, her müşteri için ayrı ayrı çalışır.

-- Normal (bağımsız) alt sorgu örneği
SELECT ad, maas
FROM calisanlar
WHERE maas > (SELECT AVG(maas) FROM calisanlar);

-- Korelasyonlu alt sorgu örneği
-- Dış sorgunun her satırı için iç sorgu yeniden çalışır
SELECT ad, departman, maas
FROM calisanlar c1
WHERE maas > (
    SELECT AVG(maas)
    FROM calisanlar c2
    WHERE c2.departman = c1.departman  -- c1 dış sorguya referans
);

İkinci sorguda dikkat edin: c1.departman ifadesi dış sorguya aittir. İç sorgu her seferinde farklı bir departman için ortalama maaşı hesaplar.

Temel Söz Dizimi ve Çalışma Mantığı

Korelasyonlu alt sorgu yazarken dikkat etmeniz gereken birkaç temel nokta vardır. Takma ad (alias) kullanımı zorunludur çünkü hem iç hem dış sorguda aynı tablo adı kullanılıyorsa MySQL hangisine referans verdiğinizi anlayamazsa hata verir.

-- Temel yapı
SELECT sütun1, sütun2
FROM tablo1 alias_dis
WHERE koşul (
    SELECT fonksiyon(sütun)
    FROM tablo2 alias_ic
    WHERE alias_ic.ortak_sütun = alias_dis.ortak_sütun
);

Çalışma sırası şöyledir:

  • Dış sorgu ilk satırı alır
  • İç sorgu bu satırın değerleriyle çalışır
  • Sonuç dış sorgunun koşuluyla karşılaştırılır
  • Eşleşirse satır sonuç kümesine eklenir
  • Dış sorgu bir sonraki satıra geçer
  • İç sorgu tekrar çalışır

Bu döngüsel yapı, büyük tablolarda performans sorunlarına yol açabilir. Bunu ilerleyen bölümlerde ele alacağız.

Gerçek Dünya Senaryo 1: Her Departmanın En Yüksek Maaşlı Çalışanı

Bir şirketin insan kaynakları veritabanında her departmandaki en yüksek maaşı alan çalışanı bulmak istiyorsunuz. Bu klasik bir korelasyonlu alt sorgu problemidir.

-- Örnek tablo yapısı
CREATE TABLE calisanlar (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ad VARCHAR(100),
    departman VARCHAR(50),
    maas DECIMAL(10,2),
    ise_giris_tarihi DATE
);

-- Her departmanda en yüksek maaş alan çalışanlar
SELECT c1.ad, c1.departman, c1.maas
FROM calisanlar c1
WHERE c1.maas = (
    SELECT MAX(c2.maas)
    FROM calisanlar c2
    WHERE c2.departman = c1.departman
)
ORDER BY c1.departman;

Bu sorgu şöyle çalışır: Dış sorgudaki her çalışan için iç sorgu o çalışanın departmanındaki maksimum maaşı bulur. Eğer çalışanın maaşı o departmanın maksimumuna eşitse, bu çalışan sonuç kümesine dahil edilir.

Gerçek Dünya Senaryo 2: EXISTS ile Korelasyonlu Alt Sorgu

EXISTS operatörü, korelasyonlu alt sorgularla birlikte en sık kullanılan yapılardan biridir. İç sorgu en az bir satır döndürüyorsa TRUE, hiç satır döndürmüyorsa FALSE üretir. Bu yapı özellikle “şu koşulu sağlayan kayıtlar var mı?” tarzı sorgulamalarda çok kullanışlıdır.

Bir e-ticaret sisteminde, en az bir sipariş vermiş müşterileri listeleyelim:

-- Siparişler ve müşteriler tabloları
CREATE TABLE musteriler (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ad VARCHAR(100),
    email VARCHAR(150),
    kayit_tarihi DATE
);

CREATE TABLE siparisler (
    id INT AUTO_INCREMENT PRIMARY KEY,
    musteri_id INT,
    siparis_tarihi DATE,
    tutar DECIMAL(10,2),
    durum VARCHAR(20)
);

-- En az bir sipariş vermiş müşteriler
SELECT m.id, m.ad, m.email
FROM musteriler m
WHERE EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
);

-- Hiç sipariş vermemiş müşteriler (NOT EXISTS)
SELECT m.id, m.ad, m.email
FROM musteriler m
WHERE NOT EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
);

SELECT 1 kullanımına dikkat edin. EXISTS sadece satır var mı yok mu diye baktığı için iç sorguda ne döndürdüğünüz önemsizdir. SELECT 1 en performanslı yazım biçimidir çünkü MySQL gereksiz sütun okuma yapmaz.

Gerçek Dünya Senaryo 3: Son 30 Gün İçinde Sipariş Veren Müşterilerin Toplam Harcaması

Bu senaryo hem EXISTS hem de korelasyonlu hesaplama içerir. Log yönetim sistemlerinde veya CRM uygulamalarında sıkça karşılaşılan bir durumdur:

-- Son 30 gün içinde aktif olan müşterilerin
-- toplam harcamasını ve sipariş sayısını getir
SELECT
    m.ad,
    m.email,
    (
        SELECT COUNT(*)
        FROM siparisler s
        WHERE s.musteri_id = m.id
        AND s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 30 DAY)
    ) AS son_30_gun_siparis_sayisi,
    (
        SELECT SUM(s.tutar)
        FROM siparisler s
        WHERE s.musteri_id = m.id
        AND s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 30 DAY)
    ) AS son_30_gun_toplam_harcama
FROM musteriler m
WHERE EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
    AND s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 30 DAY)
)
ORDER BY son_30_gun_toplam_harcama DESC;

Bu sorguda SELECT listesinde iki ayrı korelasyonlu alt sorgu kullanıldığına dikkat edin. Her satır için iki ayrı hesaplama yapılır. Bu güçlü ama aynı zamanda dikkatli kullanılması gereken bir yaklaşımdır.

Gerçek Dünya Senaryo 4: Güncelleştirme (UPDATE) ile Korelasyonlu Alt Sorgu

Korelasyonlu alt sorgular sadece SELECT’te değil, UPDATE ve DELETE ifadelerinde de son derece kullanışlıdır. Bir envanter yönetim sisteminde, stoğu biten ürünlerin durumunu güncelleyelim:

CREATE TABLE urunler (
    id INT AUTO_INCREMENT PRIMARY KEY,
    ad VARCHAR(200),
    stok_miktari INT,
    durum VARCHAR(20) DEFAULT 'aktif'
);

CREATE TABLE stok_hareketleri (
    id INT AUTO_INCREMENT PRIMARY KEY,
    urun_id INT,
    hareket_tipi ENUM('giris', 'cikis'),
    miktar INT,
    tarih DATETIME
);

-- Gerçek stok miktarını hesaplayarak ürün durumunu güncelle
UPDATE urunler u
SET u.durum = CASE
    WHEN (
        SELECT COALESCE(SUM(CASE
            WHEN sh.hareket_tipi = 'giris' THEN sh.miktar
            WHEN sh.hareket_tipi = 'cikis' THEN -sh.miktar
            ELSE 0
        END), 0)
        FROM stok_hareketleri sh
        WHERE sh.urun_id = u.id
    ) <= 0 THEN 'tukendi'
    ELSE 'aktif'
END;

Bu tür toplu güncellemeler, gece yarısı çalışan cron job’larında veya bakım pencerelerinde çok işe yarar.

Gerçek Dünya Senaryo 5: DELETE ile Yinelenen Kayıtları Temizleme

DBA’lerin en sık karşılaştığı sorunlardan biri yinelenen (duplicate) kayıtlardır. Korelasyonlu alt sorgu ile yinelenenleri temizleyebilirsiniz:

-- Yinelenen e-posta adreslerini temizle
-- Her e-posta için sadece en küçük ID'li kaydı tut
DELETE FROM musteriler
WHERE id NOT IN (
    SELECT min_id FROM (
        SELECT MIN(id) AS min_id
        FROM musteriler
        GROUP BY email
    ) AS temiz_kayitlar
);

-- Alternatif: Korelasyonlu EXISTS ile
DELETE FROM musteriler m1
WHERE EXISTS (
    SELECT 1
    FROM (
        SELECT id
        FROM musteriler m2
        WHERE m2.email = m1.email
        AND m2.id < m1.id
    ) AS daha_eski_kayit
);

Önemli not: MySQL ve MariaDB’de aynı tabloya hem DELETE hem de alt sorguda başvuruda bulunmak kısıtlıdır. Bu nedenle iç içe türetilmiş tablo (derived table) tekniği kullanmak gerekebilir.

Performans Konuları ve Optimizasyon

Korelasyonlu alt sorgular çok güçlü olmakla birlikte, yanlış kullanıldığında ciddi performans sorunlarına yol açar. Dış sorguda 100.000 satır varsa iç sorgu 100.000 kez çalışır. Bu durumu N+1 problemi olarak da adlandırabiliriz.

-- Performans sorunlu korelasyonlu sorgu
-- Bu sorgu her çalışan için ayrı ayrı çalışır
SELECT c.ad,
    (SELECT d.ad FROM departmanlar d WHERE d.id = c.departman_id) AS departman_adi
FROM calisanlar c;

-- Daha iyi alternatif: JOIN kullanımı
SELECT c.ad, d.ad AS departman_adi
FROM calisanlar c
INNER JOIN departmanlar d ON d.id = c.departman_id;

EXPLAIN ile sorgu planını inceleme alışkanlığı edinin:

-- Sorgu planını incele
EXPLAIN SELECT c1.ad, c1.departman, c1.maas
FROM calisanlar c1
WHERE c1.maas = (
    SELECT MAX(c2.maas)
    FROM calisanlar c2
    WHERE c2.departman = c1.departman
);

-- Daha detaylı analiz için
EXPLAIN FORMAT=JSON SELECT c1.ad, c1.departman, c1.maas
FROM calisanlar c1
WHERE c1.maas = (
    SELECT MAX(c2.maas)
    FROM calisanlar c2
    WHERE c2.departman = c1.departman
);

EXPLAIN çıktısında şunlara dikkat edin:

  • type: “ALL” görüyorsanız tam tablo taraması yapılıyor demektir, bu tehlike işaretidir
  • rows: Tahmini işlenecek satır sayısı, yüksekse performans sorunu olabilir
  • Extra: “Using index” görmek iyidir, “Using filesort” veya “Using temporary” görüyorsanız dikkat edin

İndeks Stratejisi

Korelasyonlu alt sorguların performansını artırmak için doğru indeksleme kritiktir:

-- Korelasyonlu sorgunun iç tarafında JOIN yapılan sütunlara indeks ekle
CREATE INDEX idx_calisanlar_departman ON calisanlar(departman);
CREATE INDEX idx_siparisler_musteri ON siparisler(musteri_id);
CREATE INDEX idx_siparisler_tarih ON siparisler(siparis_tarihi);

-- Bileşik (composite) indeks bazen daha etkilidir
CREATE INDEX idx_siparisler_musteri_tarih ON siparisler(musteri_id, siparis_tarihi);

-- Mevcut indeksleri kontrol et
SHOW INDEX FROM siparisler;

Korelasyonlu Alt Sorgu mu, JOIN mi?

Bu sorunun kesin bir cevabı yoktur. Duruma göre değişir. Bazı rehber kararlar:

  • Varlık kontrolü (EXISTS, NOT EXISTS) gerektiren durumlarda korelasyonlu alt sorgu kullanmak anlaşılırlık açısından daha iyidir
  • Sütun değeri hesaplama gerektiren durumlarda (örn. her satır için aggregate değer) LEFT JOIN ile GROUP BY kombinasyonu genellikle daha performanslıdır
  • Kod okunabilirliği öncelikliyse ve tablo küçükse korelasyonlu alt sorgu daha net bir ifade sunar
  • Büyük tablolarda her zaman EXPLAIN ile test edin
-- Korelasyonlu alt sorgu ile yazım
SELECT m.ad,
    (SELECT COUNT(*) FROM siparisler s WHERE s.musteri_id = m.id) AS siparis_sayisi
FROM musteriler m;

-- JOIN ile eşdeğer yazım (genellikle daha hızlı)
SELECT m.ad, COUNT(s.id) AS siparis_sayisi
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
GROUP BY m.id, m.ad;

MariaDB’ye Özgü Optimizasyonlar

MariaDB, MySQL’den bazı noktalarda ayrışır ve korelasyonlu alt sorgularda bazı ek optimizasyonlar sunar. MariaDB 10.x sürümlerinde semi-join optimizasyonu daha gelişmiştir ve bazı korelasyonlu EXISTS sorgularını otomatik olarak JOIN’e dönüştürebilir.

-- MariaDB'de optimizer_switch ayarlarını kontrol et
SHOW VARIABLES LIKE 'optimizer_switch';

-- Eğer semi-join optimizasyonu kapalıysa aç
SET optimizer_switch='semijoin=on,firstmatch=on,loosescan=on';

-- Sorgu seviyesinde optimizer hint kullanımı (MariaDB 10.4+)
SELECT /*+ NO_SEMIJOIN() */ m.ad
FROM musteriler m
WHERE EXISTS (
    SELECT 1 FROM siparisler s WHERE s.musteri_id = m.id
);

Sık Yapılan Hatalar ve Dikkat Edilmesi Gerekenler

NULL değer tuzakları: NOT IN ile korelasyonlu alt sorgu kullanırken, iç sorgu NULL döndürürse beklenmedik sonuçlar elde edebilirsiniz. NOT EXISTS bu durumda daha güvenlidir.

-- Tehlikeli: NULL varsa hiç sonuç dönmeyebilir
SELECT * FROM musteriler
WHERE id NOT IN (SELECT musteri_id FROM siparisler);
-- siparisler.musteri_id NULL içeriyorsa bu sorgu boş döner!

-- Güvenli: NOT EXISTS NULL'a karşı dayanıklıdır
SELECT * FROM musteriler m
WHERE NOT EXISTS (
    SELECT 1 FROM siparisler s
    WHERE s.musteri_id = m.id
);

-- Veya NULL'ları filtrele
SELECT * FROM musteriler
WHERE id NOT IN (
    SELECT musteri_id FROM siparisler
    WHERE musteri_id IS NOT NULL
);

Takma ad karışıklığı: İç ve dış sorgu aynı tablo adını kullanıyorsa mutlaka farklı takma adlar kullanın. Aksi halde MySQL hangi tabloya referans verdiğinizi yanlış anlayabilir ve hata vermeden yanlış sonuç üretebilir.

Skalar alt sorgu kısıtlaması: SELECT listesinde kullanılan korelasyonlu alt sorgu tek bir değer (skalar) döndürmek zorundadır. Birden fazla satır döndürürse hata alırsınız.

-- Hatalı: Birden fazla satır dönebilir
SELECT ad,
    (SELECT tutar FROM siparisler WHERE musteri_id = m.id) AS son_siparis
FROM musteriler m;
-- ERROR 1242: Subquery returns more than 1 row

-- Doğru: Aggregate veya LIMIT ile tekil değer garanti altına al
SELECT ad,
    (SELECT MAX(tutar) FROM siparisler WHERE musteri_id = m.id) AS en_yuksek_siparis
FROM musteriler m;

Pratik Bir Senaryo: Raporlama Sorgusu

Son olarak, birden fazla tekniği bir arada kullanan gerçekçi bir raporlama sorgusu yazalım. Bir SaaS ürününde, aylık aktif kullanıcıları ve bu kullanıcıların özelliklerini listeleyen bir sorgu:

SELECT
    k.id,
    k.ad,
    k.email,
    k.kayit_tarihi,
    -- Son oturum tarihi
    (
        SELECT MAX(o.tarih)
        FROM oturumlar o
        WHERE o.kullanici_id = k.id
    ) AS son_oturum,
    -- Bu ay kaç kez giriş yaptı
    (
        SELECT COUNT(*)
        FROM oturumlar o
        WHERE o.kullanici_id = k.id
        AND o.tarih >= DATE_FORMAT(NOW(), '%Y-%m-01')
    ) AS bu_ay_giris_sayisi,
    -- Toplam işlem tutarı
    (
        SELECT COALESCE(SUM(i.tutar), 0)
        FROM islemler i
        WHERE i.kullanici_id = k.id
        AND i.durum = 'tamamlandi'
    ) AS toplam_islem_tutari
FROM kullanicilar k
WHERE EXISTS (
    SELECT 1
    FROM oturumlar o
    WHERE o.kullanici_id = k.id
    AND o.tarih >= DATE_SUB(NOW(), INTERVAL 30 DAY)
)
ORDER BY bu_ay_giris_sayisi DESC
LIMIT 100;

Bu sorgu büyük tablolarda yavaş çalışabilir. oturumlar(kullanici_id, tarih) ve islemler(kullanici_id, durum) üzerinde bileşik indeksler oluşturarak performansı önemli ölçüde artırabilirsiniz.

Sonuç

Korelasyonlu alt sorgular, SQL araç kutunuzdaki en güçlü araçlardan biridir. Özellikle satır bazlı karşılaştırma, varlık kontrolü ve dinamik aggregate hesaplamaları gerektiren durumlarda oldukça kullanışlıdır. Ancak bu gücün bir bedeli vardır: her dış satır için iç sorgunun yeniden çalışması ciddi performans maliyetleri yaratabilir.

İyi bir sysadmin ve DBA olarak şu alışkanlıkları edinmenizi öneririm: Her korelasyonlu sorguyu geliştirme ortamında EXPLAIN ile test edin. Büyük tablolarda JOIN alternatifini mutlaka deneyin ve kıyaslayın. İç sorguda kullanılan sütunlara uygun indeksleri ekleyin. NULL değerlerinin oluşturabileceği sorunlara karşı dikkatli olun ve NOT EXISTS’i NOT IN’e tercih edin.

MariaDB ve MySQL sürekli gelişmeye devam ediyor. Query optimizer her yeni sürümde daha akıllı hale geliyor ve bazı korelasyonlu sorguları otomatik olarak daha verimli planlara dönüştürebiliyor. Bu yüzden kullandığınız sürümün belgesini takip etmek ve düzenli olarak sürüm güncellemelerini değerlendirmek de sizin elinizi güçlendiren bir alışkanlık olacaktır.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir