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:
refya daeq_refgörüyorsanız indeks kullanılıyor, iyidir.ALLgörüyorsanız full table scan var, kötüdür. - key: Hangi indeksin kullanıldığını gösterir.
NULLise indeks yok. - rows: Tahmini incelenen satır sayısı. Bu değer düşük olmalı.
- Extra:
Using indexveyaUsing whereifadeleri iyidir.Using filesortveyaUsing temporaryalarm 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 EXISTSile 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,SUMgibi)
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.idifadesindemusteri_idkolonu mutlaka indekslenmiş olmalı. - Alt sorguda ek filtreler varsa bileşik indeks değerlendirin: Hem
musteri_idhemdurumgibi iki koşul varsa bileşik indeks tek tek indeksten daha verimlidir. EXPLAIN ANALYZEkullanı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 EXISTSile 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 EXISTSalt sorgularını optimize ederken bazı durumlarda MySQL 8.0’dan daha agresif bir yaklaşım benimser. - MariaDB
EXPLAIN FORMAT=JSONile daha ayrıntılı alt sorgu optimizasyon bilgisi sunar. - MySQL 8.0 ile gelen CTE (Common Table Expression) kullanımı
EXISTSile 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.
