MariaDB ve MySQL’de Alt Sorgu (Subquery) Kullanım Örnekleri
Veritabanı sorgularında işler karmaşıklaştıkça, tek bir SELECT ifadesiyle sonuca ulaşmak çoğu zaman yeterli olmuyor. İşte tam bu noktada alt sorgular devreye giriyor. Bir sorgunun içine gömülmüş başka bir sorgu olan subquery’ler, özellikle filtreleme, karşılaştırma ve hesaplama işlemlerinde hayat kurtarıcı oluyor. Bu yazıda MariaDB ve MySQL üzerinde gerçek dünya senaryolarıyla alt sorguların nasıl kullanıldığını ele alacağız.
Alt Sorgu Nedir?
Alt sorgu (subquery), bir SQL ifadesinin içinde yer alan ve önce çalışan bağımsız bir SELECT sorgusudur. Dış sorgu, iç sorgunun döndürdüğü sonucu kullanarak kendi işlemini tamamlar. Alt sorgular WHERE, FROM, SELECT ve HAVING gibi farklı clause’larda kullanılabilir.
Temel yapı şu şekildedir:
SELECT kolon_adi
FROM tablo_adi
WHERE kolon_adi = (SELECT kolon_adi FROM baska_tablo WHERE kosul);
Alt sorgular iki temel kategoriye ayrılır:
- Correlated Subquery: Dış sorgunun her satırı için ayrı ayrı çalışan, dış sorguya bağımlı alt sorgular
- Non-Correlated Subquery: Dış sorgudan bağımsız olarak bir kez çalışan alt sorgular
Örnek Veritabanı Yapısı
Yazı boyunca kullanacağımız senaryolar için şu tabloları referans alacağız:
-- Çalışanlar tablosu
CREATE TABLE calisanlar (
id INT PRIMARY KEY AUTO_INCREMENT,
ad VARCHAR(100),
departman_id INT,
maas DECIMAL(10,2),
yonetici_id INT,
ise_giris_tarihi DATE
);
-- Departmanlar tablosu
CREATE TABLE departmanlar (
id INT PRIMARY KEY AUTO_INCREMENT,
ad VARCHAR(100),
lokasyon VARCHAR(100)
);
-- Siparişler tablosu
CREATE TABLE siparisler (
id INT PRIMARY KEY AUTO_INCREMENT,
musteri_id INT,
tutar DECIMAL(10,2),
siparis_tarihi DATE,
durum VARCHAR(50)
);
-- Müşteriler tablosu
CREATE TABLE musteriler (
id INT PRIMARY KEY AUTO_INCREMENT,
ad VARCHAR(100),
sehir VARCHAR(100),
kayit_tarihi DATE
);
WHERE Clause ile Alt Sorgu Kullanımı
Tek Değer Döndüren Alt Sorgular
En yaygın kullanım senaryolarından biri, bir tablodan hesaplanan değeri başka bir sorgunun filtresi olarak kullanmaktır. Örneğin şirket genelinde ortalama maaşın üzerinde kazanan çalışanları bulmak istiyoruz:
SELECT ad, maas
FROM calisanlar
WHERE maas > (
SELECT AVG(maas)
FROM calisanlar
);
Bu sorgu önce iç kısımda tüm çalışanların ortalama maaşını hesaplar, ardından dış sorgu bu değerin üzerinde maaş alan çalışanları listeler. Pratik bir senaryo: aylık bordro raporlarında veya zam dönemlerinde kimlerin ortalama üstünde olduğunu görmek için idealdir.
IN Operatörü ile Alt Sorgu
Birden fazla değerle karşılaştırma yapmak gerektiğinde IN operatörü kullanılır. İstanbul’daki departmanlarda çalışan personeli listeleyelim:
SELECT c.ad, c.maas
FROM calisanlar c
WHERE c.departman_id IN (
SELECT id
FROM departmanlar
WHERE lokasyon = 'Istanbul'
);
Bu yapı JOIN ile de yazılabilir ama alt sorgu özellikle okunabilirlik açısından daha temiz bir ifade sunar. Özellikle birden fazla şehir için filtre yapmak istediğinizde IN ile alt sorgu kombinasyonu güçlü bir araç haline gelir.
NOT IN ile Negatif Filtreleme
Hiç sipariş vermemiş müşterileri bulmak klasik bir CRM senaryosudur. NOT IN bu işi güzelce halleder:
SELECT ad, sehir, kayit_tarihi
FROM musteriler
WHERE id NOT IN (
SELECT DISTINCT musteri_id
FROM siparisler
WHERE durum != 'iptal'
);
Önemli uyarı: NOT IN kullanırken iç sorgunun NULL değer döndürmemesine dikkat edin. Eğer alt sorgu sonucunda NULL varsa, NOT IN her zaman boş sonuç döndürür. Bu durumda NOT EXISTS kullanmak daha güvenlidir.
EXISTS ve NOT EXISTS Kullanımı
EXISTS operatörü, alt sorgunun en az bir satır döndürüp döndürmediğini kontrol eder. Büyük veri setlerinde IN’e göre genellikle daha performanslıdır çünkü ilk eşleşmeyi bulduğunda durur.
-- Son 6 ayda sipariş veren müşteriler
SELECT m.ad, m.sehir
FROM musteriler m
WHERE EXISTS (
SELECT 1
FROM siparisler s
WHERE s.musteri_id = m.id
AND s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 6 MONTH)
AND s.durum = 'tamamlandi'
);
Bu sorgu bir correlated subquery örneğidir. Dış sorgudaki her müşteri için iç sorgu ayrıca çalışır ve o müşteriye ait sipariş olup olmadığını kontrol eder. SELECT 1 kullanımı kasıtlıdır; EXISTS sadece satır varlığını kontrol ettiği için hangi kolonun seçildiği önemli değildir, bu da gereksiz veri okumayı önler.
NOT EXISTS ise hiç sipariş vermemiş veya belirli bir işlem yapmamış kayıtları bulmak için kullanılır:
-- Hiç yöneticisi olmayan veya kendi departmanından başka yerde yöneticisi olan çalışanlar
SELECT c.ad
FROM calisanlar c
WHERE NOT EXISTS (
SELECT 1
FROM calisanlar y
WHERE y.id = c.yonetici_id
AND y.departman_id = c.departman_id
);
FROM Clause’da Alt Sorgu (Derived Table)
FROM içinde kullanılan alt sorgular “derived table” veya “inline view” olarak adlandırılır. Bu yaklaşım, önce bir ara sonuç kümesi oluşturup ardından bu küme üzerinde işlem yapmak için kullanılır.
-- Her departmandaki en yüksek maaşı alanın adını bulmak
SELECT c.ad, c.maas, d.ad AS departman
FROM calisanlar c
JOIN departmanlar d ON c.departman_id = d.id
JOIN (
SELECT departman_id, MAX(maas) AS max_maas
FROM calisanlar
GROUP BY departman_id
) AS dept_max ON c.departman_id = dept_max.departman_id
AND c.maas = dept_max.max_maas;
Bu sorgu özellikle raporlama sistemlerinde sıkça karşılaşılan bir ihtiyacı karşılıyor. Her departman için en yüksek maaşlı kişiyi listelemek istiyoruz ama sadece maaşı değil, kişinin adını da görmek istiyoruz. Derived table bu senaryoyu temiz bir şekilde çözüyor.
Bir e-ticaret senaryosu düşünelim; aylık sipariş özeti üzerinden analiz yapmak istiyoruz:
-- Aylık sipariş ortalamasının üzerinde satış yapılan ayları bul
SELECT ay, toplam_satis
FROM (
SELECT
DATE_FORMAT(siparis_tarihi, '%Y-%m') AS ay,
SUM(tutar) AS toplam_satis,
COUNT(*) AS siparis_sayisi
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY DATE_FORMAT(siparis_tarihi, '%Y-%m')
) AS aylik_ozet
WHERE toplam_satis > (
SELECT AVG(aylik_toplam)
FROM (
SELECT SUM(tutar) AS aylik_toplam
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY DATE_FORMAT(siparis_tarihi, '%Y-%m')
) AS ic_ozet
);
Bu örnekte iç içe iki alt sorgu kullanıldığına dikkat edin. İlk derived table aylık toplamları hesaplıyor, ikincisi bu toplamların ortalamasını alıyor, dış sorgu ise ortalama üzerindeki ayları filtreliyor.
SELECT Clause’da Alt Sorgu
SELECT içinde alt sorgu kullanmak, her satır için hesaplanan bir değeri eklemek istediğinizde işe yarar. Bu yaklaşım correlated subquery kullanır ve dikkatli kullanılmazsa performans sorunlarına yol açabilir.
-- Her çalışanın maaşının departman ortalamasına oranı
SELECT
c.ad,
c.maas,
(SELECT AVG(c2.maas)
FROM calisanlar c2
WHERE c2.departman_id = c.departman_id) AS dept_ort,
ROUND(c.maas / (SELECT AVG(c2.maas)
FROM calisanlar c2
WHERE c2.departman_id = c.departman_id) * 100, 2) AS oran
FROM calisanlar c;
Bu sorgu her çalışan için departman ortalamasını dinamik olarak hesaplar. Performans açısından büyük tablolarda dikkatli olunmalı; bu tür sorgularda JOIN veya window function alternatifleri düşünülebilir. Ancak küçük-orta ölçekli veri setlerinde oldukça okunabilir bir yapı sunar.
HAVING Clause’da Alt Sorgu
HAVING, GROUP BY sonucunu filtrelemek için kullanılır ve alt sorgularla güçlü kombinasyonlar oluşturur:
-- Ortalama sipariş tutarı şirket genelinin üzerinde olan şehirler
SELECT m.sehir, AVG(s.tutar) AS sehir_ortalamasi, COUNT(s.id) AS siparis_sayisi
FROM musteriler m
JOIN siparisler s ON m.id = s.musteri_id
WHERE s.durum = 'tamamlandi'
GROUP BY m.sehir
HAVING AVG(s.tutar) > (
SELECT AVG(tutar)
FROM siparisler
WHERE durum = 'tamamlandi'
);
Bu sorgu hangi şehirlerdeki müşterilerin ortalama harcamasının genel ortalamanın üzerinde olduğunu gösterir. Pazarlama ekipleri için hedef kitle analizi yaparken bu tür sorgular son derece kullanışlıdır.
Gerçek Dünya Senaryosu: Stok ve Satış Yönetimi
Bir envanter yönetim sistemi düşünelim. Stok seviyesi kritik eşiğin altına düşmüş ama son 30 günde sipariş alınmış ürünleri bulmak istiyoruz:
-- Stok kritik ama satış aktif olan ürünler
SELECT
u.urun_adi,
u.stok_miktari,
u.kritik_stok_seviyesi,
(SELECT COUNT(*)
FROM siparis_detaylari sd
JOIN siparisler s ON sd.siparis_id = s.id
WHERE sd.urun_id = u.id
AND s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 30 DAY)) AS son30gun_satis
FROM urunler u
WHERE u.stok_miktari < u.kritik_stok_seviyesi
AND u.id IN (
SELECT DISTINCT sd.urun_id
FROM siparis_detaylari sd
JOIN siparisler s ON sd.siparis_id = s.id
WHERE s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 30 DAY)
)
ORDER BY son30gun_satis DESC;
Bu sorgu hem stok eksikliğini hem de satış hareketliliğini aynı anda değerlendiriyor. Sonucu satış adedine göre sıralamak, hangi ürüne önce stok temin edilmesi gerektiğini gösterir. Bir WMS (Warehouse Management System) üzerinde çalışan sysadmin olarak bu tür sorgular günlük iş akışının parçası haline gelebilir.
Alt Sorgu Performansı ve Optimizasyon
Alt sorgular güçlü olmakla birlikte yanlış kullanıldığında ciddi performans sorunlarına yol açabilir. Birkaç pratik ipucu:
- EXPLAIN kullanın: Sorgunun execution plan’ını görmek için her zaman EXPLAIN ile test edin
EXPLAIN SELECT c.ad
FROM calisanlar c
WHERE c.departman_id IN (
SELECT id FROM departmanlar WHERE lokasyon = 'Istanbul'
);
- Correlated subquery’lere dikkat: Her satır için tekrar çalıştıklarından büyük tablolarda ciddi yavaşlamaya neden olabilirler. Mümkünse JOIN’e dönüştürün.
- Indeksleri kontrol edin: Alt sorgu içindeki filtreleme kolonlarının indeksli olduğundan emin olun. Özellikle WHERE ve JOIN koşullarındaki kolonlar.
- MariaDB 10.2+ için WITH (CTE) alternatifi: Karmaşık iç içe alt sorgular yerine Common Table Expression kullanmak hem okunabilirliği artırır hem de optimizer’ın daha iyi plan üretmesine yardımcı olur.
-- CTE ile daha okunabilir alternatif
WITH dept_maks AS (
SELECT departman_id, MAX(maas) AS max_maas
FROM calisanlar
GROUP BY departman_id
),
istanbul_deptlari AS (
SELECT id FROM departmanlar WHERE lokasyon = 'Istanbul'
)
SELECT c.ad, c.maas
FROM calisanlar c
JOIN dept_maks dm ON c.departman_id = dm.departman_id
AND c.maas = dm.max_maas
WHERE c.departman_id IN (SELECT id FROM istanbul_deptlari);
Pratik İpuçları ve Dikkat Edilmesi Gerekenler
Günlük kullanımda karşılaşılan yaygın hatalar ve bunlardan kaçınma yolları:
- NULL değer tuzağı: NOT IN kullanırken alt sorgunun NULL döndürmediğinden emin olun. Şüphe durumunda
WHERE kolon IS NOT NULLekleyin veya NOT EXISTS tercih edin.
- Tek satır beklentisi:
=operatörü ile kullandığınız alt sorgu birden fazla satır dönerse hata alırsınız. Bu durumdaINkullanın ya daLIMIT 1ekleyin.
- Alt sorgu içinde ORDER BY: Alt sorgu içinde ORDER BY tek başına anlamsızdır ve MariaDB bunu genellikle ignore eder. LIMIT ile birlikte kullanıldığında anlam kazanır.
- Alias zorunluluğu: FROM clause’daki derived table’lara mutlaka alias vermek gerekir, aksi halde syntax hatası alırsınız.
-- YANLIŞ - alias yok
SELECT * FROM (SELECT id, ad FROM calisanlar WHERE maas > 5000);
-- DOĞRU - alias var
SELECT * FROM (SELECT id, ad FROM calisanlar WHERE maas > 5000) AS yuksek_maaslilar;
- Scalar subquery: SELECT clause’da kullandığınız alt sorgu her zaman tek bir değer döndürmeli. Birden fazla satır veya kolon döndürürse hata alırsınız.
Sonuç
Alt sorgular, SQL’in en esnek ve güçlü araçlarından biridir. WHERE, FROM, SELECT ve HAVING clause’larında farklı amaçlarla kullanılabilen bu yapılar, özellikle karmaşık raporlama sorgularında ve çok adımlı veri filtrelemelerinde büyük kolaylık sağlar. Ancak güç, sorumluluk gerektirir: correlated subquery’lerin performans etkisini göz ardı etmemek, NULL değer davranışlarını iyi anlamak ve büyük veri setlerinde EXPLAIN ile sorgu planını kontrol etmek sağlıklı bir veritabanı yönetiminin temel taşlarıdır.
Başlangıçta JOIN ile halledebileceğiniz işlemler için alt sorgu kullanmaktan kaçının. Ama derived table gerektiren karmaşık analizlerde, EXISTS ile varlık kontrolü yapmanız gereken senaryolarda ve iç içe hesaplamalar söz konusu olduğunda alt sorgular gerçek değerini gösterir. MariaDB 10.2 ve üzeri sürümlerde CTE (WITH clause) desteğiyle birlikte bu karmaşık yapıları daha okunabilir hale getirme imkanına da sahipsiniz.
