HAVING Koşulu ile Grup Filtreleme
Veritabanı yönetiminde en çok kafaları karıştıran konulardan biri, WHERE ile HAVING arasındaki farkı anlamak ve HAVING koşulunu doğru yerde kullanmaktır. Özellikle gruplama işlemleri söz konusu olduğunda, yanlış koşul kullanımı ya hatalı sonuçlara ya da gereksiz performans sorunlarına yol açar. Bu yazıda HAVING koşulunu derinlemesine ele alacağız, gerçek dünya senaryolarıyla desteklenen örnekler üzerinden konuyu somutlaştıracağız.
HAVING Nedir ve WHERE’den Farkı Nedir?
WHERE koşulu, satırları gruplama işleminden önce filtreler. Yani GROUP BY devreye girmeden, ham veri üzerinde çalışır. HAVING ise tam tersi, gruplama işleminden sonra uygulanan bir filtredir. Aggregate fonksiyonlar (COUNT, SUM, AVG, MAX, MIN) üzerinde filtreleme yapmak istediğinizde WHERE yetersiz kalır, çünkü WHERE bu fonksiyonları tanımaz.
Basit bir kural olarak şunu aklınızda tutun: Eğer filtre koşulunuzda COUNT(), SUM(), AVG() gibi bir aggregate fonksiyon varsa, o koşul HAVING bloğuna gider. Yoksa WHERE bloğuna.
SQL sorgusunun mantıksal çalışma sırası şöyledir:
- FROM: Tablo(lar) seçilir
- WHERE: Satırlar filtrelenir
- GROUP BY: Gruplar oluşturulur
- HAVING: Gruplar filtrelenir
- SELECT: Sütunlar seçilir
- ORDER BY: Sıralama yapılır
- LIMIT: Satır sayısı sınırlanır
Bu sırayı bilmek, hangi koşulun nereye yazılacağını anlamak açısından kritiktir.
Örnek Veritabanı Yapısı
Yazı boyunca kullanacağımız örnek şemayı kuralım. Bir e-ticaret sistemini simüle eden basit bir yapı üzerinden gideceğiz.
CREATE DATABASE eticaret;
USE eticaret;
CREATE TABLE musteriler (
id INT AUTO_INCREMENT PRIMARY KEY,
ad VARCHAR(100),
sehir VARCHAR(100),
kayit_tarihi DATE
);
CREATE TABLE siparisler (
id INT AUTO_INCREMENT PRIMARY KEY,
musteri_id INT,
urun_adi VARCHAR(200),
miktar INT,
birim_fiyat DECIMAL(10,2),
siparis_tarihi DATE,
durum VARCHAR(50),
FOREIGN KEY (musteri_id) REFERENCES musteriler(id)
);
INSERT INTO musteriler (ad, sehir, kayit_tarihi) VALUES
('Ahmet Yilmaz', 'Istanbul', '2022-01-15'),
('Ayse Kaya', 'Ankara', '2022-03-20'),
('Mehmet Demir', 'Izmir', '2021-11-10'),
('Fatma Celik', 'Istanbul', '2023-02-05'),
('Ali Sahin', 'Bursa', '2022-07-30');
INSERT INTO siparisler (musteri_id, urun_adi, miktar, birim_fiyat, siparis_tarihi, durum) VALUES
(1, 'Laptop', 2, 15000.00, '2023-01-10', 'tamamlandi'),
(1, 'Mouse', 5, 250.00, '2023-02-15', 'tamamlandi'),
(2, 'Klavye', 3, 800.00, '2023-01-20', 'tamamlandi'),
(2, 'Monitor', 1, 8000.00, '2023-03-05', 'iptal'),
(3, 'Laptop', 1, 15000.00, '2023-02-10', 'tamamlandi'),
(3, 'Webcam', 2, 1200.00, '2023-03-15', 'tamamlandi'),
(4, 'Tablet', 3, 6000.00, '2023-01-25', 'tamamlandi'),
(5, 'Telefon', 2, 20000.00, '2023-04-01', 'tamamlandi');
Temel HAVING Kullanımı
En basit senaryodan başlayalım. Hangi müşterilerimizin 2’den fazla sipariş verdiğini bulmak istiyoruz.
SELECT
musteri_id,
COUNT(*) AS siparis_sayisi
FROM siparisler
GROUP BY musteri_id
HAVING COUNT(*) > 2;
Bu sorguyu WHERE COUNT(*) > 2 şeklinde yazmaya çalışsaydık MariaDB hata verirdi. Çünkü WHERE çalıştığında henüz gruplama yapılmamıştır, dolayısıyla COUNT() değeri ortada yoktur.
Şimdi bunu biraz daha insanileştirelim, müşteri adlarını da getirelim:
SELECT
m.ad,
m.sehir,
COUNT(s.id) AS siparis_sayisi,
SUM(s.miktar * s.birim_fiyat) AS toplam_harcama
FROM musteriler m
JOIN siparisler s ON m.id = s.musteri_id
GROUP BY m.id, m.ad, m.sehir
HAVING COUNT(s.id) >= 2
ORDER BY toplam_harcama DESC;
Bu sorgu bize en az 2 sipariş vermiş müşterileri toplam harcamalarına göre sıralı getirir. Gerçek hayatta bu tür bir sorguyu müşteri segmentasyonu veya sadakat programı için kullanabilirsiniz.
WHERE ve HAVING’i Birlikte Kullanmak
Bu iki koşul birbirinin rakibi değil, tamamlayıcısıdır. Çoğu gerçek dünya sorgusunda ikisini birlikte kullanırsınız.
Örneğin sadece tamamlanan siparişleri değerlendirip, bu siparişlerde 10.000 TL’nin üzerinde harcama yapan müşterileri bulmak istiyoruz:
SELECT
m.ad,
m.sehir,
COUNT(s.id) AS tamamlanan_siparis_sayisi,
SUM(s.miktar * s.birim_fiyat) AS toplam_harcama
FROM musteriler m
JOIN siparisler s ON m.id = s.musteri_id
WHERE s.durum = 'tamamlandi'
GROUP BY m.id, m.ad, m.sehir
HAVING SUM(s.miktar * s.birim_fiyat) > 10000
ORDER BY toplam_harcama DESC;
Burada WHERE s.durum = 'tamamlandi' koşulu gruplama öncesi iptal edilen siparişleri eliyor. Sonra gruplama yapılıyor ve HAVING ile toplam harcama filtresi uygulanıyor. Bu sırayı doğru anlamak, hem doğru sonuç almanızı hem de sorgunun performanslı çalışmasını sağlar.
Birden Fazla HAVING Koşulu
HAVING bloğunda AND ve OR operatörleri kullanarak birden fazla koşul tanımlayabilirsiniz.
SELECT
m.sehir,
COUNT(DISTINCT m.id) AS musteri_sayisi,
AVG(s.miktar * s.birim_fiyat) AS ortalama_siparis_tutari,
SUM(s.miktar * s.birim_fiyat) AS toplam_ciro
FROM musteriler m
JOIN siparisler s ON m.id = s.musteri_id
WHERE s.durum = 'tamamlandi'
GROUP BY m.sehir
HAVING COUNT(DISTINCT m.id) >= 1
AND AVG(s.miktar * s.birim_fiyat) > 5000
AND SUM(s.miktar * s.birim_fiyat) > 15000
ORDER BY toplam_ciro DESC;
Bu sorgu şehir bazında analiz yapıyor. Şehirde en az 1 müşteri olsun, ortalama sipariş tutarı 5.000 TL’yi geçsin ve toplam ciro 15.000 TL üzerinde olsun koşullarını birlikte uyguluyoruz. Bölgesel satış raporlamalarında bu tür sorgular oldukça yaygındır.
HAVING ile MIN ve MAX Kullanımı
Bir sysadmin olarak log analizi veya monitoring verisi işlerken de benzer sorgularla karşılaşırsınız. Aşağıdaki örneği sunucu performans logları üzerinde düşünebilirsiniz. Ama biz e-ticaret örneğimize sadık kalalım.
Ürün bazında en yüksek birim fiyatı belirli bir eşiğin üzerinde olan ürün kategorilerini bulmak:
SELECT
urun_adi,
COUNT(*) AS kac_kez_siparis_edildi,
MIN(birim_fiyat) AS en_dusuk_fiyat,
MAX(birim_fiyat) AS en_yuksek_fiyat,
AVG(birim_fiyat) AS ortalama_fiyat
FROM siparisler
GROUP BY urun_adi
HAVING MAX(birim_fiyat) > 1000
AND COUNT(*) >= 1
ORDER BY ortalama_fiyat DESC;
Burada MIN() ve MAX() fonksiyonlarını hem SELECT listesinde hem de HAVING koşulunda kullandık. Bu tamamen geçerlidir ve MariaDB bunu sorunsuz işler.
Alt Sorgu ile HAVING Kombinasyonu
Daha gelişmiş senaryolarda HAVING içinde alt sorgu kullanmak gerekebilir. Örneğin, ortalama harcamanın üzerinde harcama yapan müşterileri bulmak istiyoruz:
SELECT
m.ad,
m.sehir,
SUM(s.miktar * s.birim_fiyat) AS toplam_harcama
FROM musteriler m
JOIN siparisler s ON m.id = s.musteri_id
WHERE s.durum = 'tamamlandi'
GROUP BY m.id, m.ad, m.sehir
HAVING SUM(s.miktar * s.birim_fiyat) > (
SELECT AVG(musteri_toplam)
FROM (
SELECT SUM(s2.miktar * s2.birim_fiyat) AS musteri_toplam
FROM siparisler s2
WHERE s2.durum = 'tamamlandi'
GROUP BY s2.musteri_id
) AS alt_sorgu
)
ORDER BY toplam_harcama DESC;
Bu sorgu biraz karmaşık görünse de mantığı basittir. İç içe alt sorgu önce her müşterinin toplam harcamasını hesaplar, sonra bunların ortalamasını alır. Dış sorgu ise bu ortalamayı HAVING koşulunda referans olarak kullanır. Performans açısından dikkatli olun: büyük tablolarda bu tür alt sorgular yavaşlayabilir. Gerekirse WITH (CTE) yapısına geçmeyi düşünebilirsiniz.
Tarih Bazlı Gruplama ve HAVING
Gerçek dünyada zaman serisi analizleri çok sık yapılır. Aylık bazda belirli bir satış eşiğini geçen dönemleri bulmak:
SELECT
YEAR(siparis_tarihi) AS yil,
MONTH(siparis_tarihi) AS ay,
COUNT(*) AS siparis_adedi,
SUM(miktar * birim_fiyat) AS aylik_ciro
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY YEAR(siparis_tarihi), MONTH(siparis_tarihi)
HAVING SUM(miktar * birim_fiyat) > 20000
AND COUNT(*) >= 2
ORDER BY yil, ay;
Bu sorgu aylık cironun 20.000 TL’yi geçtiği ve en az 2 siparişin bulunduğu dönemleri listeler. Finansal raporlama, KPI takibi veya anomali tespiti için kullanışlı bir yapıdır.
Performans Notları: HAVING Kullanırken Dikkat Edilmesi Gerekenler
Burada biraz durup pratik sysadmin perspektifinden konuşalım. HAVING kullanımı doğru ama performansız olabilir. Birkaç önemli noktayı paylaşmak istiyorum:
WHERE ile filtrelenebilecek koşulları WHERE’e taşıyın. Bazı geliştiriciler her şeyi HAVING bloğuna koymak ister. Bu yanlış. Eğer bir koşul aggregate fonksiyon içermiyorsa ve doğrudan bir sütun değerine dayalıysa, WHERE bloğuna yazın. Böylece gruplama öncesi satır sayısı azalır ve işlem daha hızlı olur.
Kötü örnek:
-- Bu yaklasim yanlis, WHERE kullanilabilir
SELECT musteri_id, COUNT(*) as siparis_sayisi
FROM siparisler
GROUP BY musteri_id
HAVING durum = 'tamamlandi' AND COUNT(*) > 2;
Doğru örnek:
-- durum kosulu WHERE'e tasindi
SELECT musteri_id, COUNT(*) as siparis_sayisi
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY musteri_id
HAVING COUNT(*) > 2;
İkinci sorguda MariaDB önce durum = 'tamamlandi' filtresiyle satır sayısını düşürür, sonra gruplama yapar. İlkinde ise tüm satırları gruplar, sonra filtreler. Büyük tablolarda bu fark çok belirgin hale gelir.
Index kullanımını göz önünde bulundurun. GROUP BY sütunlarınız ve WHERE koşullarınızda kullandığınız sütunlar için uygun indexler tanımladığınızdan emin olun.
-- Sorgu performansini incelemek icin EXPLAIN kullanin
EXPLAIN SELECT
musteri_id,
COUNT(*) AS siparis_sayisi,
SUM(miktar * birim_fiyat) AS toplam
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY musteri_id
HAVING COUNT(*) > 1;
EXPLAIN çıktısında Using temporary veya Using filesort görüyorsanız, bu gruplama ve sıralama için geçici tablo oluşturulduğuna işaret eder. Index eklemek bu durumu iyileştirebilir.
Gerçek Dünya Senaryosu: Log Analizi
Sysadmin olarak çoğunlukla uygulama loglarını veritabanına aktarıp analiz ederiz. Şöyle bir log tablosu düşünelim:
CREATE TABLE uygulama_loglari (
id INT AUTO_INCREMENT PRIMARY KEY,
sunucu_adi VARCHAR(100),
log_seviyesi VARCHAR(20), -- INFO, WARNING, ERROR, CRITICAL
mesaj TEXT,
olusma_zamani DATETIME
);
-- Hangi sunucular son 24 saatte 10'dan fazla ERROR atti?
SELECT
sunucu_adi,
log_seviyesi,
COUNT(*) AS hata_sayisi,
MIN(olusma_zamani) AS ilk_hata,
MAX(olusma_zamani) AS son_hata
FROM uygulama_loglari
WHERE log_seviyesi IN ('ERROR', 'CRITICAL')
AND olusma_zamani >= NOW() - INTERVAL 24 HOUR
GROUP BY sunucu_adi, log_seviyesi
HAVING COUNT(*) > 10
ORDER BY hata_sayisi DESC;
Bu sorgu monitoring sisteminizde düzenli çalışacak bir sorgu olabilir. 10’dan fazla hata atan sunucuları tespit etmek, alarm sisteminizin temelini oluşturabilir.
HAVING ile ROLLUP Kombinasyonu
MariaDB’de GROUP BY ... WITH ROLLUP kullanarak ara toplamlar ve genel toplam elde edebilirsiniz. HAVING bu yapıyla da çalışır:
SELECT
COALESCE(m.sehir, 'GENEL TOPLAM') AS sehir,
COUNT(DISTINCT m.id) AS musteri_sayisi,
SUM(s.miktar * s.birim_fiyat) AS toplam_ciro
FROM musteriler m
JOIN siparisler s ON m.id = s.musteri_id
WHERE s.durum = 'tamamlandi'
GROUP BY m.sehir WITH ROLLUP
HAVING SUM(s.miktar * s.birim_fiyat) > 5000
OR m.sehir IS NULL;
ROLLUP kullanımında HAVING biraz farklı davranır. NULL değerleri genel toplam satırını temsil eder, bu yüzden OR m.sehir IS NULL koşulunu ekleyerek genel toplam satırının her durumda gösterilmesini sağlarız.
MariaDB’ye Özgü Bir Not: Alias Kullanımı
Standart SQL’de HAVING bloğunda SELECT listesinde tanımladığınız alias kullanılamaz. Ancak MariaDB ve MySQL bu konuda standarttan biraz sapar ve alias kullanımına izin verir:
-- Bu MariaDB/MySQL'de calisir, ancak diger veritabanlarda calismaeyabilir
SELECT
musteri_id,
COUNT(*) AS siparis_sayisi,
SUM(miktar * birim_fiyat) AS toplam_harcama
FROM siparisler
GROUP BY musteri_id
HAVING siparis_sayisi > 1 AND toplam_harcama > 5000;
Bu kullanım pratik olmakla birlikte, taşınabilirlik açısından dikkatli olun. PostgreSQL veya MSSQL’e geçiş durumunda bu sorgular çalışmaz. Standart yaklaşım olan HAVING COUNT() > 1 AND SUM(miktar birim_fiyat) > 5000 yazmak daha güvenlidir.
Özet: HAVING Kullanımında Akılda Tutulacaklar
- Aggregate fonksiyonlar üzerindeki filtreler her zaman
HAVINGbloğuna gider - Sütun değerlerine dayalı filtreler mümkün olduğunca
WHEREbloğuna taşınmalıdır HAVINGher zamanGROUP BY‘dan sonra çalışırANDveORile birden fazlaHAVINGkoşulu birleştirilebilirHAVINGiçinde alt sorgu kullanılabilir, ancak performansa dikkat edilmelidirEXPLAINile sorgu planını kontrol etmek iyi bir alışkanlıktır- MariaDB alias kullanımına izin verse de standart yaklaşım kullanmak önerilir
Sonuç
HAVING koşulu, veritabanı sorgularında grup bazlı filtreleme yapmanın tek ve doğru yoludur. WHERE ile aralarındaki fark, sadece bir yazım tercihi değil, çalışma mantığı açısından temel bir ayrımdır. Bunu kavradıktan sonra hem daha doğru sorgular yazarsınız hem de gereksiz performans sorunlarından kaçınırsınız.
Gerçek dünyada bu konuyu en çok satış raporları, müşteri segmentasyonu, sunucu log analizi ve monitoring sistemlerinde kullanacaksınız. Hangi koşulun nereye gideceğini, SQL’in mantıksal çalışma sırasını ve EXPLAIN ile performans analizini birlikte düşündüğünüzde, HAVING sizin için artık karmaşık değil, güçlü bir araç haline gelecektir.
