EXISTS ile Alt Sorgu Varlık Kontrolü: MariaDB ve MySQL Örnekleri

Veritabanı sorgularında performans ve doğruluk açısından en çok göz ardı edilen konulardan biri, bir kaydın başka bir tabloda var olup olmadığını kontrol etmektir. Çoğu geliştirici bu iş için JOIN veya IN operatörüne başvurur. Oysa EXISTS ile yazılan alt sorgular, doğru senaryolarda hem daha okunabilir hem de ciddi ölçüde daha hızlı çalışır. Bu yazıda MariaDB ve MySQL üzerinde EXISTS operatörünün nasıl çalıştığını, ne zaman tercih edilmesi gerektiğini ve gerçek dünya senaryolarıyla nasıl uygulanacağını ele alacağız.

EXISTS Nedir ve Nasıl Çalışır

EXISTS, bir alt sorgunun en az bir satır döndürüp döndürmediğini kontrol eden bir SQL operatörüdür. Sonuç olarak TRUE ya da FALSE döner. Buradaki kritik nokta şudur: EXISTS, alt sorgu ilk eşleşen satırı bulduğu anda durur. Tüm tabloyu taramak zorunda kalmaz. Buna short-circuit evaluation denir ve büyük tablolarda performans farkı oldukça belirgin olur.

Temel sözdizimi şu şekildedir:

SELECT kolonlar
FROM tablo_adi
WHERE EXISTS (
    SELECT 1
    FROM baska_tablo
    WHERE kosul
);

Alt sorguda SELECT 1 ya da SELECT * kullanmak performans açısından fark yaratmaz. MariaDB/MySQL, EXISTS içindeki alt sorguda hangi kolonun seçildiğine bakmaz, yalnızca satır olup olmadığına bakar. Ancak SELECT 1 kullanmak kodun niyetini daha açık ifade eder ve okunabilirliği artırır.

Basit Bir Varlık Kontrolü

Önce sıfırdan bir senaryo kuralım. E-ticaret sistemimizde musteriler ve siparisler tablolarımız var. Sipariş vermiş müşterilerin listesini çekmek istiyoruz.

-- Örnek tablo yapıları
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,
    toplam_tutar DECIMAL(10,2),
    siparis_tarihi DATETIME,
    durum VARCHAR(50)
);

EXISTS ile sipariş vermiş müşterileri sorgulayalım:

SELECT m.id, m.ad, m.email
FROM musteriler m
WHERE EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
);

Bu sorgu dışarıdan içeriye çalışır. Her müşteri için alt sorgu çalışır ve o müşteriye ait en az bir sipariş kaydı bulunduğu anda TRUE döner, müşteri sonuç kümesine dahil edilir. Aynı sonucu IN ile de elde edebilirsiniz, ancak EXISTS büyük veri setlerinde belleği daha verimli kullanır çünkü tüm id listesini belleğe yüklemez.

NOT EXISTS ile Eksik Kayıtları Bulmak

NOT EXISTS, belki de EXISTS‘in en güçlü kullanım alanıdır. Bir tabloda olup diğerinde olmayan kayıtları bulmak için kullanılır.

Örneğin, hiç sipariş vermemiş müşterileri bulmak istiyoruz:

SELECT m.id, m.ad, m.email, m.kayit_tarihi
FROM musteriler m
WHERE NOT EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
);

Bu tür sorgu, pasif müşteri kampanyaları için pazarlama ekibine rapor hazırlarken çok işe yarar. Aynı sorguyu LEFT JOIN ile de yazabilirsiniz:

-- LEFT JOIN ile aynı sonuç (kıyaslama için)
SELECT m.id, m.ad, m.email
FROM musteriler m
LEFT JOIN siparisler s ON s.musteri_id = m.id
WHERE s.id IS NULL;

Her iki yaklaşım da aynı sonucu üretir. Ancak NOT EXISTS versiyonu, özellikle siparisler tablosunda musteri_id kolonu üzerinde indeks varsa genellikle daha iyi performans gösterir. Execution plan’ı her zaman EXPLAIN ile kontrol edin.

Koşullu Varlık Kontrolü

Daha ileri gidelim. Sadece son 30 gün içinde sipariş vermiş aktif müşterileri listeleyelim:

SELECT m.id, m.ad, m.email
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)
      AND s.durum = 'tamamlandi'
);

Bu örnekte alt sorgu iki ek koşulla filtre yapıyor. EXISTS bu sayede çok daha spesifik varlık kontrolleri için kullanılabilir hale gelir. Sadece “kayıt var mı” değil, “şu özelliklere sahip kayıt var mı” sorusunu yanıtlar.

Birden Fazla Tablolu Karmaşık Senaryolar

Gerçek dünya uygulamalarında genellikle birden fazla tabloyla çalışırız. Şimdi daha kapsamlı bir senaryo ele alalım. Sistemimizde urunler, siparis_kalemleri ve stok tabloları da var olsun.

CREATE TABLE urunler (
    id INT AUTO_INCREMENT PRIMARY KEY,
    urun_adi VARCHAR(200),
    kategori_id INT,
    aktif TINYINT(1) DEFAULT 1
);

CREATE TABLE siparis_kalemleri (
    id INT AUTO_INCREMENT PRIMARY KEY,
    siparis_id INT,
    urun_id INT,
    miktar INT,
    birim_fiyat DECIMAL(10,2)
);

Son 6 ay içinde hiç satılmamış aktif ürünleri bulmak istiyoruz. Bu, stok temizliği raporları için klasik bir sysadmin görevi haline gelir çünkü bu raporları genellikle veritabanı yöneticileri hazırlar:

SELECT u.id, u.urun_adi
FROM urunler u
WHERE u.aktif = 1
  AND NOT EXISTS (
      SELECT 1
      FROM siparis_kalemleri sk
      JOIN siparisler s ON s.id = sk.siparis_id
      WHERE sk.urun_id = u.id
        AND s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 6 MONTH)
        AND s.durum != 'iptal'
  );

Dikkat edin, alt sorgu içinde JOIN kullanıyoruz. EXISTS alt sorguları tek tabloyla sınırlı değildir. Bu esneklik sayesinde çok boyutlu varlık kontrolleri yapılabilir.

EXISTS ile INSERT, UPDATE ve DELETE Operasyonları

EXISTS yalnızca SELECT sorgularında değil, veri manipülasyon işlemlerinde de kullanılır. Bu kullanım biçimi özellikle toplu güncelleme işlemlerinde kritik önem taşır.

Koşullu UPDATE

Bir kampanya tablosunda tanımlanmış müşterilerin profilini güncellemek isteyelim:

-- kampanya_katilimcilari tablosunda olan müşterilerin email tercihini güncelle
UPDATE musteriler m
SET m.email_tercih = 'kampanya'
WHERE EXISTS (
    SELECT 1
    FROM kampanya_katilimcilari kk
    WHERE kk.musteri_id = m.id
      AND kk.kampanya_id = 42
);

Bu yaklaşım, önce tüm katılımcı id’lerini çekip sonra IN (...)‘e eklemeye kıyasla bellek açısından çok daha verimlidir. Özellikle kampanya listesi binlerce kayıt içerdiğinde fark kendini gösterir.

Koşullu DELETE

Belirli bir ürün kategorisi silindiğinde, o kategoriye bağlı ürünlerle ilişkili sipariş kalemi olmayan pasif ürünleri temizlemek isteyelim:

DELETE FROM urunler
WHERE aktif = 0
  AND kategori_id = 15
  AND NOT EXISTS (
      SELECT 1
      FROM siparis_kalemleri sk
      WHERE sk.urun_id = urunler.id
  );

Bu sorgu, sipariş geçmişi olan ürünleri dokunmadan bırakır. NOT EXISTS burada güvenlik ağı işlevi görür.

EXPLAIN ile Performans Analizi

EXISTS sorgularının gerçekten verimli çalışıp çalışmadığını anlamak için EXPLAIN çıktısını okumayı bilmek gerekir:

EXPLAIN SELECT m.id, m.ad
FROM musteriler m
WHERE EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
      AND s.durum = 'tamamlandi'
);

EXPLAIN çıktısında dikkat etmeniz gereken noktalar:

  • type: ref ya da eq_ref görüyorsanız indeks kullanılıyor, iyidir. ALL görüyorsanız full table scan var, kötüdür.
  • key: Hangi indeksin kullanıldığını gösterir. NULL ise indeks yok.
  • rows: Tahmini incelenen satır sayısı. Bu değer düşük olmalı.
  • Extra: Using index veya Using where ifadeleri iyidir. Using filesort veya Using temporary alarm işareti olabilir.

İndeks eklemek için:

-- siparisler tablosunda musteri_id ve durum kolonlarına bileşik indeks
CREATE INDEX idx_siparisler_musteri_durum 
ON siparisler (musteri_id, durum);

-- Sorguyu tekrar EXPLAIN ile kontrol et
EXPLAIN SELECT m.id, m.ad
FROM musteriler m
WHERE EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
      AND s.durum = 'tamamlandi'
);

İndeks eklendikten sonra type kolonunun ref‘e döndüğünü ve rows değerinin dramatik biçimde düştüğünü göreceksiniz.

Gerçek Dünya Senaryosu: Kullanıcı Yetki Kontrolü

Bir web uygulamasında kullanıcıların belirli bir modüle erişim yetkisi var mı kontrol eden bir yapı düşünelim:

CREATE TABLE kullanicilar (
    id INT AUTO_INCREMENT PRIMARY KEY,
    kullanici_adi VARCHAR(100),
    aktif TINYINT(1) DEFAULT 1
);

CREATE TABLE roller (
    id INT AUTO_INCREMENT PRIMARY KEY,
    rol_adi VARCHAR(50)
);

CREATE TABLE kullanici_roller (
    kullanici_id INT,
    rol_id INT,
    atanma_tarihi DATE,
    bitis_tarihi DATE NULL
);

CREATE TABLE rol_izinleri (
    rol_id INT,
    modul VARCHAR(50),
    islem VARCHAR(50)
);

Belirli bir modüle yazma izni olan aktif kullanıcıları listeleyelim:

SELECT k.id, k.kullanici_adi
FROM kullanicilar k
WHERE k.aktif = 1
  AND EXISTS (
      SELECT 1
      FROM kullanici_roller kr
      JOIN rol_izinleri ri ON ri.rol_id = kr.rol_id
      WHERE kr.kullanici_id = k.id
        AND ri.modul = 'raporlama'
        AND ri.islem = 'yazma'
        AND (kr.bitis_tarihi IS NULL OR kr.bitis_tarihi >= CURDATE())
  );

Bu sorgu hem rol bazlı yetki hem de rol bitiş tarihi kontrolü yapıyor. Tek bir EXISTS bloğu içinde birden fazla tablo ve koşul yönetebiliyoruz. Aynı mantığı uygulama katmanında PHP veya Python kodu ile yazmak daha uzun ve hata yapmaya açık olurdu.

Çift Yönlü Varlık Kontrolü

Bazen iki tablo arasında karşılıklı ilişki kontrolü yapmanız gerekir. Hem sipariş vermiş hem de ürün değerlendirmesi yapmış müşterileri bulmak gibi:

SELECT m.id, m.ad, m.email
FROM musteriler m
WHERE EXISTS (
    SELECT 1
    FROM siparisler s
    WHERE s.musteri_id = m.id
      AND s.durum = 'tamamlandi'
)
AND EXISTS (
    SELECT 1
    FROM urun_degerlendirmeleri ud
    WHERE ud.musteri_id = m.id
      AND ud.puan >= 4
);

Birden fazla EXISTS bloğunu AND ile birleştirebilirsiniz. MySQL/MariaDB bu koşulları sırayla değerlendirirken kısa devre mantığını uyguladığından, ilk koşul FALSE döndüğünde ikincisini değerlendirmez.

EXISTS vs IN vs JOIN: Hangisini Ne Zaman Kullanmalı

Bu üçü arasındaki seçim çoğu zaman tartışma konusu olur. Genel kurallar şu şekilde özetlenebilir:

EXISTS tercih edin:

  • Alt sorguda dönen satır sayısı çok fazlaysa
  • Yalnızca varlık kontrolü yapıyorsanız, değer döndürmenize gerek yoksa
  • Alt sorgu dışarıdaki tabloya korelasyon kuruyorsa
  • NOT EXISTS ile eksik kayıt arıyorsanız

IN tercih edin:

  • Alt sorgu az sayıda benzersiz değer döndürüyorsa
  • Alt sorgu bağımsızsa ve tekrar kullanılabilir bir liste üretiyorsa
  • Sabit bir değer listesiyle karşılaştırma yapıyorsanız

JOIN tercih edin:

  • Eşleşen tablodan kolon değeri çekmeniz gerekiyorsa
  • Birden fazla satır dönebilecek bir ilişki varsa ve bunu görmek istiyorsanız
  • Aggregation yapacaksanız (COUNT, SUM gibi)

Büyük Tablolarda Performans İpuçları

Üretim ortamında milyonlarca kayıt barındıran tablolarda EXISTS kullanırken dikkat edilmesi gereken pratik noktalar:

  • Korelasyon koşulundaki kolonlara indeks koyun: Alt sorgudaki WHERE s.musteri_id = m.id ifadesinde musteri_id kolonu mutlaka indekslenmiş olmalı.
  • Alt sorguda ek filtreler varsa bileşik indeks değerlendirin: Hem musteri_id hem durum gibi iki koşul varsa bileşik indeks tek tek indeksten daha verimlidir.
  • EXPLAIN ANALYZE kullanın (MariaDB 10.3+, MySQL 8.0+): Gerçek çalışma sürelerini ve tahmini değerleri karşılaştırmanıza olanak tanır.
  • Büyük DELETE/UPDATE işlemlerini parçalara bölün: Tek seferde milyonlarca satırı NOT EXISTS ile silmeye çalışmak lock sorunlarına yol açar. LIMIT kullanarak toplu işlemi parçalara bölün.
-- Büyük toplu silme işlemini parçalara bölme
DELETE FROM gecici_veriler
WHERE NOT EXISTS (
    SELECT 1
    FROM aktif_islemler ai
    WHERE ai.referans_id = gecici_veriler.id
)
LIMIT 1000;

-- Bu sorguyu döngüyle çalıştırın, affected rows 0 olana kadar

Bu işlemi bir bash betiğiyle otomatize edebilirsiniz:

#!/bin/bash
# toplu_silme.sh

DB="veritabani_adi"
USER="kullanici"
PASS="sifre"

while true; do
    AFFECTED=$(mysql -u "$USER" -p"$PASS" "$DB" -e "
        DELETE FROM gecici_veriler
        WHERE NOT EXISTS (
            SELECT 1 FROM aktif_islemler ai
            WHERE ai.referans_id = gecici_veriler.id
        )
        LIMIT 1000;
        SELECT ROW_COUNT();" 2>/dev/null | tail -1)
    
    echo "Silinen satir: $AFFECTED"
    
    if [ "$AFFECTED" -eq 0 ]; then
        echo "Islem tamamlandi."
        break
    fi
    
    sleep 0.5
done

MariaDB ve MySQL Arasındaki Davranış Farkları

Genel olarak her iki sistem de EXISTS sorgularını benzer biçimde işler. Ancak bazı ince farklar vardır:

  • MariaDB 10.4 ve üzeri, NOT EXISTS alt sorgularını optimize ederken bazı durumlarda MySQL 8.0’dan daha agresif bir yaklaşım benimser.
  • MariaDB EXPLAIN FORMAT=JSON ile daha ayrıntılı alt sorgu optimizasyon bilgisi sunar.
  • MySQL 8.0 ile gelen CTE (Common Table Expression) kullanımı EXISTS ile birleştirildiğinde bazı yazım kolaylıkları sağlar, MariaDB 10.2 ve üzeri de CTEyi destekler.
-- CTE ile EXISTS kullanımı (her iki sistemde de çalışır)
WITH son_siparisler AS (
    SELECT DISTINCT musteri_id
    FROM siparisler
    WHERE siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 90 DAY)
      AND durum = 'tamamlandi'
)
SELECT m.id, m.ad
FROM musteriler m
WHERE EXISTS (
    SELECT 1
    FROM son_siparisler ss
    WHERE ss.musteri_id = m.id
);

Sonuç

EXISTS operatörü, SQL araç kutunuzda mutlaka yerini alması gereken güçlü bir yapıdır. Özellikle NOT EXISTS ile eksik kayıt tespiti, koşullu güncelleme ve silme işlemleri ile büyük tablolarda varlık kontrolü yapılması gereken senaryolarda JOIN veya IN‘e kıyasla belirgin avantajlar sunar. Temel mantığını kavradıktan sonra, korelasyon koşullarını doğru yazmak ve ilgili kolonları indekslemek yeterlidir.

Üretim ortamında her zaman önce EXPLAIN ile sorgu planını inceleyin, ardından gerçek veriyle test edin. Teoride hızlı görünen bir sorgu, indeks eksikliği nedeniyle pratikte tam tersi sonuç verebilir. Sysadmin olarak veritabanı sorgularının yalnızca “çalışması” değil, “verimli çalışması” gerektiğini unutmayın. EXISTS ile tanışın, doğru yerde kullanın ve sorgu optimizasyonunda bir adım öne geçin.

Bir yanıt yazın

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