MySQL ve MariaDB’de GROUP BY ile Verileri Gruplama
Veritabanı yönetiminde en çok ihtiyaç duyulan işlemlerden biri, ham veriyi anlamlı gruplara ayırıp her grup için özet bilgi üretmektir. Bir e-ticaret sitesinde kategori bazlı satış rakamlarına bakmak, bir log sisteminde hata türlerine göre dağılımı görmek ya da kullanıcı aktivitelerini günlük olarak analiz etmek istediğinizde GROUP BY devreye girer. Bu yazıda MariaDB ve MySQL üzerinde GROUP BY kullanımını gerçek dünya senaryolarıyla birlikte ele alacağız.
GROUP BY Nedir ve Ne Zaman Kullanılır
GROUP BY, SQL sorgularında belirtilen sütun veya sütunlara göre satırları gruplayan ve genellikle COUNT, SUM, AVG, MIN, MAX gibi aggregate (toplam) fonksiyonlarla birlikte kullanılan bir ifadedir. Temel mantığı şudur: aynı değere sahip satırları bir araya topla, sonra her grup için bir hesaplama yap.
Şunu hayal edin: binlerce sipariş kaydı var ve siz hangi müşterinin ne kadar harcama yaptığını öğrenmek istiyorsunuz. Her satırı tek tek okumak yerine GROUP BY ile müşteri bazında gruplarsınız ve SUM ile toplam harcamayı hesaplarsınız. İşin özü budur.
GROUP BY olmadan aggregate fonksiyonlar tüm tablo için tek bir sonuç döndürür. GROUP BY eklendiğinde ise her grup için ayrı bir sonuç satırı gelir.
Örnek Veritabanı Yapısı
Bu yazı boyunca kullanacağımız örnek veritabanını hemen oluşturalım. Bir e-ticaret senaryosu üzerinden gideceğiz.
-- Veritabanı ve tabloları oluştur
CREATE DATABASE eticaret;
USE eticaret;
CREATE TABLE musteriler (
id INT AUTO_INCREMENT PRIMARY KEY,
ad VARCHAR(100),
sehir VARCHAR(50),
uyelik_tarihi DATE
);
CREATE TABLE siparisler (
id INT AUTO_INCREMENT PRIMARY KEY,
musteri_id INT,
kategori VARCHAR(50),
urun_adi VARCHAR(100),
tutar DECIMAL(10,2),
siparis_tarihi DATE,
durum VARCHAR(20)
);
-- Örnek veri ekle
INSERT INTO musteriler (ad, sehir, uyelik_tarihi) VALUES
('Ahmet Yılmaz', 'Istanbul', '2022-01-15'),
('Ayşe Kaya', 'Ankara', '2022-03-20'),
('Mehmet Demir', 'Izmir', '2021-11-05'),
('Fatma Çelik', 'Istanbul', '2023-02-10'),
('Ali Arslan', 'Ankara', '2021-08-30');
INSERT INTO siparisler (musteri_id, kategori, urun_adi, tutar, siparis_tarihi, durum) VALUES
(1, 'Elektronik', 'Laptop', 15000.00, '2024-01-10', 'tamamlandi'),
(1, 'Giyim', 'Ceket', 450.00, '2024-01-15', 'tamamlandi'),
(2, 'Elektronik', 'Telefon', 8000.00, '2024-01-12', 'tamamlandi'),
(2, 'Kitap', 'Roman', 75.00, '2024-01-20', 'iptal'),
(3, 'Elektronik', 'Tablet', 5500.00, '2024-02-05', 'tamamlandi'),
(3, 'Giyim', 'Ayakkabı', 800.00, '2024-02-10', 'tamamlandi'),
(4, 'Kitap', 'Teknik Kitap', 150.00, '2024-02-15', 'tamamlandi'),
(5, 'Giyim', 'Tişört', 200.00, '2024-03-01', 'tamamlandi'),
(1, 'Elektronik', 'Kulaklık', 1200.00, '2024-03-05', 'tamamlandi');
Temel GROUP BY Kullanımı
En basit haliyle GROUP BY bir sütuna göre gruplama yapar. Kaç farklı kategoride sipariş geldiğini ve her kategoride kaç sipariş olduğunu görelim:
-- Kategori bazında sipariş sayısı
SELECT
kategori,
COUNT(*) AS siparis_sayisi
FROM siparisler
GROUP BY kategori;
Bu sorgu şunu yapar: kategori sütunundaki her benzersiz değer için bir grup oluşturur ve COUNT(*) ile o gruptaki satır sayısını döndürür. Çıktı olarak her kategori için tek bir satır gelir.
Şimdi bunu biraz daha zenginleştirelim ve aynı sorguda birden fazla aggregate fonksiyon kullanalım:
-- Kategori bazında detaylı analiz
SELECT
kategori,
COUNT(*) AS siparis_sayisi,
SUM(tutar) AS toplam_ciro,
AVG(tutar) AS ortalama_siparis_tutari,
MIN(tutar) AS en_dusuk_siparis,
MAX(tutar) AS en_yuksek_siparis
FROM siparisler
GROUP BY kategori
ORDER BY toplam_ciro DESC;
ORDER BY ile sonuçları toplam ciroya göre büyükten küçüğe sıraladık. Bu sayede en çok ciro üreten kategoriyi hemen görebiliriz.
HAVING ile Gruplama Sonuçlarını Filtrelemek
WHERE ile satırları filtrelersiniz, HAVING ile ise gruplama yapıldıktan sonra grupları filtrelersiniz. Bu fark kritik öneme sahiptir. WHERE aggregate fonksiyonlarla çalışmaz, HAVING ise tam olarak bu amaç için tasarlanmıştır.
-- Toplam cirosu 1000 TL'nin üzerinde olan kategoriler
SELECT
kategori,
COUNT(*) AS siparis_sayisi,
SUM(tutar) AS toplam_ciro
FROM siparisler
GROUP BY kategori
HAVING toplam_ciro > 1000
ORDER BY toplam_ciro DESC;
Gerçek dünya örneği olarak şunu düşünelim: ayda 3’ten fazla sipariş veren müşterileri bulmak istiyorsunuz. VIP müşteri programı için bu listeyi kullanacaksınız:
-- Ayda 3'ten fazla sipariş veren müşteriler (sadece tamamlanan siparişler)
SELECT
m.ad AS musteri_adi,
m.sehir,
COUNT(s.id) AS siparis_sayisi,
SUM(s.tutar) AS toplam_harcama
FROM musteriler m
INNER JOIN siparisler s ON m.id = s.musteri_id
WHERE s.durum = 'tamamlandi'
GROUP BY m.id, m.ad, m.sehir
HAVING siparis_sayisi >= 2
ORDER BY toplam_harcama DESC;
Burada WHERE s.durum = 'tamamlandi' satırları filtreler, HAVING siparis_sayisi >= 2 ise grupları filtreler. İki farklı filtreleme mekanizması bir arada kullanılıyor.
Birden Fazla Sütuna Göre Gruplama
GROUP BY ile birden fazla sütunu bir arada kullanabilirsiniz. Bu, daha granüler analizler yapmanıza olanak tanır.
-- Şehir ve kategori bazında sipariş analizi
SELECT
m.sehir,
s.kategori,
COUNT(s.id) AS siparis_sayisi,
SUM(s.tutar) AS toplam_tutar
FROM musteriler m
INNER JOIN siparisler s ON m.id = s.musteri_id
GROUP BY m.sehir, s.kategori
ORDER BY m.sehir, toplam_tutar DESC;
Bu sorgu her şehir ve kategori kombinasyonu için ayrı bir satır üretir. İstanbul’daki elektronik siparişleri, İstanbul’daki giyim siparişleri, Ankara’daki elektronik siparişleri… gibi. Pazarlama ekibine bölgesel kampanya planlaması için bu tür raporlar çok işe yarar.
Tarih Bazlı Gruplama
Log analizi, satış raporları veya performans takibi için tarih bazlı gruplama yapmanız gerektiğinde DATE_FORMAT, YEAR, MONTH, WEEK gibi fonksiyonlardan yararlanırsınız.
-- Aylık sipariş ve ciro raporu
SELECT
DATE_FORMAT(siparis_tarihi, '%Y-%m') AS ay,
COUNT(*) AS siparis_sayisi,
SUM(tutar) AS aylik_ciro,
AVG(tutar) AS ortalama_siparis
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY DATE_FORMAT(siparis_tarihi, '%Y-%m')
ORDER BY ay ASC;
Daha gerçekçi bir senaryo: bir web uygulamasının hata loglarını tutan bir tablonuz var ve günlük hata dağılımını görmek istiyorsunuz:
-- Örnek log tablosu için (aynı mantık)
SELECT
DATE(siparis_tarihi) AS gun,
durum,
COUNT(*) AS islem_sayisi
FROM siparisler
GROUP BY DATE(siparis_tarihi), durum
ORDER BY gun DESC, islem_sayisi DESC;
Haftalık raporlar için WEEK() fonksiyonunu kullanabilirsiniz:
-- Haftalık ciro özeti
SELECT
YEAR(siparis_tarihi) AS yil,
WEEK(siparis_tarihi) AS hafta_no,
SUM(tutar) AS haftalik_ciro,
COUNT(*) AS siparis_sayisi
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY YEAR(siparis_tarihi), WEEK(siparis_tarihi)
ORDER BY yil, hafta_no;
GROUP BY ile WITH ROLLUP
WITH ROLLUP, gruplama hiyerarşisinde ara toplamlar ve genel toplam üretmenizi sağlar. Raporlama senaryolarında oldukça kullanışlıdır.
-- Kategori bazında ciro ve genel toplam (ROLLUP ile)
SELECT
COALESCE(kategori, 'GENEL TOPLAM') AS kategori,
COUNT(*) AS siparis_sayisi,
SUM(tutar) AS toplam_ciro
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY kategori WITH ROLLUP;
COALESCE(kategori, 'GENEL TOPLAM') ifadesi, ROLLUP tarafından üretilen NULL değerini anlamlı bir etiketle değiştirir. Böylece son satırda tüm kategorilerin toplamını “GENEL TOPLAM” başlığıyla görebilirsiniz. Muhasebe veya yönetim raporlarına bu tür sorgular çok pratik gelir.
Birden fazla sütunla ROLLUP kullanıldığında her gruplama seviyesi için ara toplamlar da üretilir:
-- Şehir ve kategori bazında hiyerarşik toplam
SELECT
COALESCE(m.sehir, 'TUM SEHIRLER') AS sehir,
COALESCE(s.kategori, 'TUM KATEGORILER') AS kategori,
SUM(s.tutar) AS toplam_ciro
FROM musteriler m
INNER JOIN siparisler s ON m.id = s.musteri_id
WHERE s.durum = 'tamamlandi'
GROUP BY m.sehir, s.kategori WITH ROLLUP
ORDER BY m.sehir, s.kategori;
COUNT DISTINCT ile Benzersiz Değerleri Saymak
Zaman zaman toplam kayıt sayısı değil, benzersiz değerlerin sayısı ilginizi çeker. Örneğin bir kategoride kaç farklı müşterinin alışveriş yaptığını bulmak:
-- Kategori bazında benzersiz müşteri sayısı ve sipariş sayısı
SELECT
kategori,
COUNT(*) AS toplam_siparis,
COUNT(DISTINCT musteri_id) AS benzersiz_musteri,
SUM(tutar) AS toplam_ciro,
ROUND(SUM(tutar) / COUNT(DISTINCT musteri_id), 2) AS musteri_basi_ciro
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY kategori
ORDER BY benzersiz_musteri DESC;
musteri_basi_ciro hesaplaması, her kategoride müşteri başına düşen ortalama harcamayı verir. Hangi kategorinin daha değerli müşteri çektiğini anlamak için bu metrik oldukça faydalıdır.
Alt Sorgu ve GROUP BY Kombinasyonu
Gerçek dünya uygulamalarında bazen bir gruplama sorgusunun sonucunu başka bir sorgunun içinde kullanmanız gerekir.
-- En yüksek ciro yapan kategorideki siparişleri listele
SELECT
s.*,
m.ad AS musteri_adi
FROM siparisler s
INNER JOIN musteriler m ON s.musteri_id = m.id
WHERE s.kategori = (
SELECT kategori
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY kategori
ORDER BY SUM(tutar) DESC
LIMIT 1
);
Bir başka senaryo: her müşterinin ortalama sipariş tutarının üzerinde harcama yaptığı siparişleri bulmak. Bu tür analizler anomali tespiti veya premium ürün önerileri için kullanılabilir:
-- Her müşteri için ortalama üzerindeki siparişler
SELECT
s.id,
m.ad AS musteri_adi,
s.kategori,
s.tutar,
musteri_ort.ortalama_tutar
FROM siparisler s
INNER JOIN musteriler m ON s.musteri_id = m.id
INNER JOIN (
SELECT
musteri_id,
AVG(tutar) AS ortalama_tutar
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY musteri_id
) AS musteri_ort ON s.musteri_id = musteri_ort.musteri_id
WHERE s.tutar > musteri_ort.ortalama_tutar
AND s.durum = 'tamamlandi'
ORDER BY m.ad, s.tutar DESC;
GROUP BY Performansı ve İndeksleme
GROUP BY sorguları özellikle büyük tablolarda ciddi performans sorunlarına yol açabilir. Birkaç önemli noktayı paylaşayım:
EXPLAIN ile sorgu planını kontrol edin:
-- Sorgu planını incele
EXPLAIN SELECT
kategori,
COUNT(*) AS siparis_sayisi,
SUM(tutar) AS toplam_ciro
FROM siparisler
WHERE durum = 'tamamlandi'
GROUP BY kategori;
EXPLAIN çıktısında Using filesort veya Using temporary görüyorsanız sorgunun optimize edilmesi gerektiğini anlarsınız. Bu uyarılar, MySQL’in disk üzerinde geçici tablo veya dosya sıralaması kullandığını gösterir.
İndeks oluşturma:
-- GROUP BY sütunlarına indeks ekle
CREATE INDEX idx_kategori_durum ON siparisler(kategori, durum);
-- Tarih bazlı gruplama için
CREATE INDEX idx_siparis_tarihi ON siparisler(siparis_tarihi);
Şu pratik noktaları aklınızda bulundurun:
- GROUP BY sütunları: Gruplama yapılan sütunlara indeks eklemek sorguyu dramatik biçimde hızlandırabilir.
- WHERE + GROUP BY:
WHEREkoşulunda veGROUP BYda aynı anda kullanılan sütunlar için bileşik indeks oluşturun. - Büyük tablolar: Milyonlarca satırlık tablolarda
GROUP BYöncesiWHEREile satır sayısını azaltın. - sql_mode ayarı: MariaDB ve MySQL 5.7+ sürümlerinde
ONLY_FULL_GROUP_BYmodu varsayılan olarak açıktır.SELECTlistesindeki her sütun yaGROUP BYda yer almalı ya da aggregate fonksiyon içinde olmalıdır.
Gerçek Dünya Senaryosu: Günlük Rapor Sorgusu
Diyelim ki her gece çalışan bir cron job ile günlük operasyon raporunu bir tabloya kaydetmek istiyorsunuz:
-- Günlük özet rapor tablosu oluştur
CREATE TABLE gunluk_raporlar (
id INT AUTO_INCREMENT PRIMARY KEY,
rapor_tarihi DATE,
kategori VARCHAR(50),
siparis_sayisi INT,
iptal_sayisi INT,
toplam_ciro DECIMAL(12,2),
olusturma_zamani TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Dünün verisini rapor tablosuna aktar
INSERT INTO gunluk_raporlar (
rapor_tarihi,
kategori,
siparis_sayisi,
iptal_sayisi,
toplam_ciro
)
SELECT
DATE(siparis_tarihi) AS rapor_tarihi,
kategori,
COUNT(CASE WHEN durum = 'tamamlandi' THEN 1 END) AS siparis_sayisi,
COUNT(CASE WHEN durum = 'iptal' THEN 1 END) AS iptal_sayisi,
SUM(CASE WHEN durum = 'tamamlandi' THEN tutar ELSE 0 END) AS toplam_ciro
FROM siparisler
WHERE DATE(siparis_tarihi) = CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(siparis_tarihi), kategori;
Bu sorguda CASE WHEN ifadelerini GROUP BY ile birleştirerek tek bir sorguda hem tamamlanan hem de iptal edilen siparişlerin sayısını hesapladık. Bu teknik, birden fazla JOIN veya alt sorgu kullanmak yerine tek geçişte farklı koşullar için ayrı sayımlar yapmanızı sağlar.
Sık Yapılan Hatalar
Birkaç yaygın hatadan bahsetmek istiyorum:
ONLY_FULL_GROUP_BY hatası: SELECT listesinde GROUP BYda yer almayan ve aggregate fonksiyon içinde olmayan bir sütun kullanırsanız hata alırsınız.
-- YANLIS: urun_adi GROUP BY veya aggregate icinde degil
SELECT kategori, urun_adi, COUNT(*) FROM siparisler GROUP BY kategori;
-- DOGRU: urun_adi aggregate icinde
SELECT kategori, ANY_VALUE(urun_adi), COUNT(*) FROM siparisler GROUP BY kategori;
-- VEYA GROUP BY'a ekle
SELECT kategori, urun_adi, COUNT(*) FROM siparisler GROUP BY kategori, urun_adi;
WHERE yerine HAVING kullanmak: Aggregate fonksiyon içermeyen koşullar için HAVING yerine WHERE kullanmak performansı artırır çünkü WHERE gruplama öncesi çalışarak işlenecek satır sayısını azaltır.
NULL değerlerin gruplara dahil edilmesi: GROUP BY NULL değerleri tek bir grup olarak ele alır. Bu bazen beklenmedik sonuçlar doğurabilir. COALESCE veya IFNULL ile NULL değerleri anlamlı bir etikete çevirmeyi unutmayın.
Sonuç
GROUP BY, sysadmin’ler ve DBA’lar için olduğu kadar uygulama geliştiricileri için de temel bir SQL yetkinliğidir. Sunucu log analizinden uygulama metriklerine, e-ticaret raporlarından kullanıcı aktivite takibine kadar pek çok alanda düzenli olarak ihtiyaç duyulur.
Özetlemek gerekirse:
- Temel kullanım:
GROUP BY sütun_adiile gruplama, aggregate fonksiyonlarla hesaplama yapın. - Filtreleme: Satırları filtrelemek için
WHERE, grupları filtrelemek içinHAVINGkullanın. - Çoklu gruplama: Birden fazla sütunla daha granüler analizler yapın.
- Tarih grupları:
DATE_FORMAT,YEAR,MONTH,WEEKfonksiyonlarıyla zaman bazlı raporlama yapın. - ROLLUP: Ara ve genel toplamlar için
WITH ROLLUPekleyin. - Performans:
EXPLAINile sorgu planını kontrol edin, gruplama sütunlarına indeks ekleyin. - Güvenli kodlama:
ONLY_FULL_GROUP_BYmodunu dikkate alarak sorgularınızı yazın.
Bu konuları özümsedikten sonra, karmaşık raporlama sorgularını bile rahatlıkla yazabilir hale gelirsiniz. Bir sonraki adım olarak WINDOW FUNCTION konusuna bakmanızı tavsiye ederim; GROUP BYın yapamadığı bazı analizleri pencere fonksiyonlarıyla gerçekleştirebilirsiniz.
