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: WHERE koşulunda ve GROUP 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 öncesi WHERE ile satır sayısını azaltın.
  • sql_mode ayarı: MariaDB ve MySQL 5.7+ sürümlerinde ONLY_FULL_GROUP_BY modu varsayılan olarak açıktır. SELECT listesindeki her sütun ya GROUP 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_adi ile gruplama, aggregate fonksiyonlarla hesaplama yapın.
  • Filtreleme: Satırları filtrelemek için WHERE, grupları filtrelemek için HAVING kullanın.
  • Çoklu gruplama: Birden fazla sütunla daha granüler analizler yapın.
  • Tarih grupları: DATE_FORMAT, YEAR, MONTH, WEEK fonksiyonlarıyla zaman bazlı raporlama yapın.
  • ROLLUP: Ara ve genel toplamlar için WITH ROLLUP ekleyin.
  • Performans: EXPLAIN ile sorgu planını kontrol edin, gruplama sütunlarına indeks ekleyin.
  • Güvenli kodlama: ONLY_FULL_GROUP_BY modunu 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.

Bir yanıt yazın

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