Çok Satırlı Veriyi Tek Satırda Göstermek için GROUP_CONCAT Kullanımı

Veritabanı sorgularında en sık karşılaşılan durumlardan biri şudur: bir kullanıcıya ait birden fazla kayıt var ve bunları tek bir satırda göstermek istiyorsunuz. Örneğin bir kullanıcının tüm rolleri, bir siparişe ait tüm ürünler ya da bir departmandaki tüm çalışanlar. Normal koşullarda JOIN yapıları her kayıt için ayrı satır döndürür ve bu çoğu zaman işinizi zorlaştırır. İşte tam bu noktada GROUP_CONCAT devreye giriyor.

GROUP_CONCAT, MySQL ve MariaDB’ye özgü güçlü bir aggregate fonksiyonudur. Birden fazla satırdaki değerleri alır, bunları belirli bir ayraçla birleştirir ve tek bir string olarak döndürür. Bu yazıda bu fonksiyonu gerçek dünya senaryolarıyla, sık yapılan hatalarla ve performans ipuçlarıyla birlikte ele alacağız.

GROUP_CONCAT Nedir ve Nasıl Çalışır?

Fonksiyonun temel sözdizimi şöyle:

GROUP_CONCAT([DISTINCT] ifade [ORDER BY sütun [ASC|DESC]] [SEPARATOR 'ayraç'])

Parametreleri tek tek açıklayalım:

  • DISTINCT: Tekrarlanan değerleri filtreler, her benzersiz değeri bir kez alır
  • ORDER BY: Birleştirilen değerlerin sıralamasını belirler
  • SEPARATOR: Değerler arasına konulacak ayracı belirler, varsayılan değer virgüldür
  • ifade: Birleştirilecek sütun ya da hesaplama

Basit bir örnek görelim. Elimizde şöyle bir kullanici_roller tablosu olsun:

-- Örnek tablo yapısı ve veri
CREATE TABLE kullaniciler (
    id INT PRIMARY KEY AUTO_INCREMENT,
    ad VARCHAR(100)
);

CREATE TABLE roller (
    id INT PRIMARY KEY AUTO_INCREMENT,
    kullanici_id INT,
    rol VARCHAR(50)
);

INSERT INTO kullaniciler VALUES (1, 'Ahmet'), (2, 'Ayşe'), (3, 'Mehmet');
INSERT INTO roller VALUES 
    (1, 1, 'admin'),
    (2, 1, 'editor'),
    (3, 2, 'editor'),
    (4, 2, 'moderator'),
    (5, 2, 'yazar'),
    (6, 3, 'yazar');

Normal bir JOIN ile sorguladığınızda her kullanıcı için birden fazla satır gelir:

SELECT k.ad, r.rol
FROM kullaniciler k
JOIN roller r ON k.id = r.kullanici_id;

-- Çıktı:
-- Ahmet | admin
-- Ahmet | editor
-- Ayşe  | editor
-- Ayşe  | moderator
-- Ayşe  | yazar
-- Mehmet| yazar

GROUP_CONCAT ile bunu tek satıra indirgiyoruz:

SELECT 
    k.ad,
    GROUP_CONCAT(r.rol ORDER BY r.rol ASC SEPARATOR ', ') AS roller
FROM kullaniciler k
JOIN roller r ON k.id = r.kullanici_id
GROUP BY k.id, k.ad;

-- Çıktı:
-- Ahmet  | admin, editor
-- Ayşe   | editor, moderator, yazar
-- Mehmet | yazar

Çok daha temiz ve işlenebilir bir çıktı. Uygulama katmanında bu sonucu split etmek ya da direkt göstermek artık çok daha kolay.

Gerçek Dünya Senaryosu 1: E-Ticaret Sipariş Özeti

Bir e-ticaret sisteminde çalışıyorsunuz. Müşteri servisi ekibi size şöyle bir rapor istiyor: her siparişin yanında o siparişe dahil olan ürün isimlerini tek satırda görmek istiyorlar. Bunu her gün çalışacak bir sorgu olarak yazmanız gerekiyor.

SELECT 
    s.siparis_id,
    s.musteri_adi,
    s.siparis_tarihi,
    s.toplam_tutar,
    GROUP_CONCAT(
        CONCAT(u.urun_adi, ' (', sd.adet, ' adet)')
        ORDER BY u.urun_adi ASC
        SEPARATOR ' | '
    ) AS urunler,
    COUNT(sd.urun_id) AS urun_cesit_sayisi
FROM siparisler s
JOIN siparis_detay sd ON s.siparis_id = sd.siparis_id
JOIN urunler u ON sd.urun_id = u.id
WHERE s.siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY s.siparis_id, s.musteri_adi, s.siparis_tarihi, s.toplam_tutar
ORDER BY s.siparis_tarihi DESC;

Bu sorguda birkaç önemli nokta var. CONCAT içinde GROUP_CONCAT kullanarak ürün adı ve adeti birleştirdik. Ayraç olarak | kullandık çünkü ürün adlarında virgül olabilir. Bu tür durumlarda virgül yerine farklı bir ayraç seçmek önemlidir.

Gerçek Dünya Senaryosu 2: Tag ve Etiket Sistemleri

Blog ya da içerik yönetim sistemlerinde her yazıya birden fazla etiket atanabilir. Bu etiketleri yazı listesiyle birlikte göstermek çok yaygın bir ihtiyaçtır:

SELECT 
    y.id,
    y.baslik,
    y.yazar,
    y.yayin_tarihi,
    GROUP_CONCAT(
        DISTINCT e.etiket_adi 
        ORDER BY e.etiket_adi ASC 
        SEPARATOR ', '
    ) AS etiketler,
    GROUP_CONCAT(
        DISTINCT k.kategori_adi
        SEPARATOR ' > '
    ) AS kategoriler
FROM yazilar y
LEFT JOIN yazi_etiket ye ON y.id = ye.yazi_id
LEFT JOIN etiketler e ON ye.etiket_id = e.id
LEFT JOIN yazi_kategori yk ON y.id = yk.yazi_id
LEFT JOIN kategoriler k ON yk.kategori_id = k.id
WHERE y.durum = 'yayinda'
GROUP BY y.id, y.baslik, y.yazar, y.yayin_tarihi
ORDER BY y.yayin_tarihi DESC
LIMIT 50;

Burada LEFT JOIN kullandık çünkü etiketi olmayan yazılar da listeye dahil olsun istiyoruz. DISTINCT ile aynı etiketin birden fazla kez gelmesini önledik. Bu tür durumlar özellikle birden fazla JOIN varken ortaya çıkabilir.

GROUP_CONCAT Limiti ve Dikkat Edilmesi Gerekenler

En çok başa bela olan konu group_concat_max_len değişkenidir. Varsayılan olarak 1024 byte ile sınırlıdır. Uzun değerler ya da çok sayıda kayıt birleştirildiğinde sonuç bu limitte kesilir ve size hiçbir uyarı vermez. Bu sessiz kesme davranışı production ortamında gerçekten sinir bozucu sorunlara yol açabilir.

Mevcut değeri kontrol etmek için:

SHOW VARIABLES LIKE 'group_concat_max_len';

-- ya da global değeri görmek için:
SELECT @@global.group_concat_max_len;
SELECT @@session.group_concat_max_len;

Limiti artırmak için iki yol var. Oturum bazında:

-- Sadece bu bağlantı için geçerli
SET SESSION group_concat_max_len = 1048576;  -- 1MB

-- Sonra sorgunuzu çalıştırın
SELECT 
    departman,
    GROUP_CONCAT(calisan_adi ORDER BY calisan_adi SEPARATOR ', ') AS calisanlar
FROM calisanlar
GROUP BY departman;

Global olarak ayarlamak ve my.cnf veya my.ini dosyasına yazmak için:

-- Çalışan sunucuda global değeri değiştir (restart gerekmez)
SET GLOBAL group_concat_max_len = 1048576;

-- my.cnf dosyasına ekle (kalıcı değişiklik için)
# /etc/mysql/my.cnf veya /etc/my.cnf
[mysqld]
group_concat_max_len = 1048576

MariaDB ve MySQL yapılandırma dosyasını düzenledikten sonra servisi yeniden başlatın:

# MySQL için
sudo systemctl restart mysql

# MariaDB için
sudo systemctl restart mariadb

# Değişikliği doğrula
mysql -u root -p -e "SHOW VARIABLES LIKE 'group_concat_max_len';"

Gerçek Dünya Senaryosu 3: Kullanıcı İzin Matrisi

Büyük bir kurumsal uygulamada kullanıcıların izinlerini yönetiyorsunuz. Güvenlik denetimi için her kullanıcının hangi modüllere erişimi olduğunu tek satırda listeleyen bir rapor hazırlamanız gerekiyor:

SET SESSION group_concat_max_len = 65536;

SELECT 
    k.kullanici_adi,
    k.email,
    k.departman,
    k.son_giris,
    GROUP_CONCAT(
        DISTINCT CONCAT(m.modul_adi, ':', i.izin_tipi)
        ORDER BY m.modul_adi ASC, i.izin_tipi ASC
        SEPARATOR '; '
    ) AS izinler,
    COUNT(DISTINCT m.id) AS erisilen_modul_sayisi,
    GROUP_CONCAT(
        DISTINCT r.rol_adi
        ORDER BY r.rol_adi ASC
        SEPARATOR ', '
    ) AS roller
FROM kullaniciler k
LEFT JOIN kullanici_roller kr ON k.id = kr.kullanici_id
LEFT JOIN roller r ON kr.rol_id = r.id
LEFT JOIN rol_izinler ri ON r.id = ri.rol_id
LEFT JOIN izinler i ON ri.izin_id = i.id
LEFT JOIN moduller m ON i.modul_id = m.id
WHERE k.aktif = 1
GROUP BY k.id, k.kullanici_adi, k.email, k.departman, k.son_giris
HAVING erisilen_modul_sayisi > 0
ORDER BY k.departman, k.kullanici_adi;

Bu sorgu güvenlik denetimleri için harika. Tek bakışta kim hangi modüle erişiyor, hangi rollere sahip, hepsini görüyorsunuz.

HAVING ile GROUP_CONCAT Filtreleme

GROUP_CONCAT sonuçlarını HAVING ile filtreleyebilirsiniz. Örneğin belirli bir rol içeren kullanıcıları bulmak istiyorsunuz:

-- Admin rolü olan kullanıcıları bul ve tüm rollerini göster
SELECT 
    k.kullanici_adi,
    GROUP_CONCAT(r.rol ORDER BY r.rol SEPARATOR ', ') AS tum_roller
FROM kullaniciler k
JOIN kullanici_roller kr ON k.id = kr.kullanici_id
JOIN roller r ON kr.rol_id = r.id
GROUP BY k.id, k.kullanici_adi
HAVING FIND_IN_SET('admin', GROUP_CONCAT(r.rol SEPARATOR ',')) > 0;

-- Alternatif olarak daha okunabilir yaklaşım:
-- 3 veya daha fazla role sahip kullanıcılar
SELECT 
    k.kullanici_adi,
    GROUP_CONCAT(r.rol ORDER BY r.rol SEPARATOR ', ') AS roller,
    COUNT(r.rol) AS rol_sayisi
FROM kullaniciler k
JOIN kullanici_roller kr ON k.id = kr.kullanici_id
JOIN roller r ON kr.rol_id = r.id
GROUP BY k.id, k.kullanici_adi
HAVING rol_sayisi >= 3
ORDER BY rol_sayisi DESC;

Pivot Tablo Benzeri Kullanım

GROUP_CONCAT ile MySQL ve MariaDB’de dinamik pivot tablo benzeri sonuçlar üretebilirsiniz. Bu özellikle raporlama sorgularında işe yarar:

-- Aylık satış özetini pivot formatında göster
SELECT 
    urun_adi,
    GROUP_CONCAT(
        CONCAT(
            DATE_FORMAT(satis_tarihi, '%Y-%m'), 
            ':', 
            FORMAT(SUM(miktar * birim_fiyat), 2)
        )
        ORDER BY satis_tarihi ASC
        SEPARATOR ' | '
    ) AS aylik_satislar,
    FORMAT(SUM(miktar * birim_fiyat), 2) AS toplam_satis
FROM satis_detay sd
JOIN urunler u ON sd.urun_id = u.id
WHERE YEAR(sd.satis_tarihi) = YEAR(CURDATE())
GROUP BY sd.urun_id, urun_adi
ORDER BY SUM(miktar * birim_fiyat) DESC
LIMIT 20;

Subquery ile GROUP_CONCAT Kullanımı

Bazen GROUP_CONCAT sonucunu dış sorguda filtrelemek ya da işlemek istersiniz. Bu durumda subquery içinde kullanabilirsiniz:

-- En az 3 ürünü olan ve toplam siparişi 1000 TL üzeri siparişleri getir
SELECT 
    siparis_id,
    musteri_adi,
    toplam_tutar,
    urunler,
    urun_sayisi
FROM (
    SELECT 
        s.siparis_id,
        s.musteri_adi,
        s.toplam_tutar,
        GROUP_CONCAT(
            u.urun_adi 
            ORDER BY u.urun_adi 
            SEPARATOR ', '
        ) AS urunler,
        COUNT(sd.id) AS urun_sayisi
    FROM siparisler s
    JOIN siparis_detay sd ON s.siparis_id = sd.siparis_id
    JOIN urunler u ON sd.urun_id = u.id
    GROUP BY s.siparis_id, s.musteri_adi, s.toplam_tutar
) AS ozet
WHERE urun_sayisi >= 3 
  AND toplam_tutar > 1000
ORDER BY toplam_tutar DESC;

Performans Konuları

GROUP_CONCAT özellikle büyük tablolarda dikkatli kullanılması gereken bir fonksiyon. Birkaç önemli nokta:

  • Index kullanımı: GROUP BY sütunlarında index olduğundan emin olun. Bu sorgu hızını dramatik şekilde etkiler.
  • Sonuç boyutu: Çok fazla veri birleştiriyorsanız group_concat_max_len limitini aşabilirsiniz. Sessiz kesmeye karşı sonuç uzunluğunu CHAR_LENGTH() ile doğrulayın.
  • Bellek kullanımı: Her GROUP_CONCAT işlemi geçici bellek kullanır. Büyük datasette memory kullanımı artabilir.
  • DISTINCT maliyeti: DISTINCT kullanımı ek sorting gerektirdiğinden yavaşlatabilir, gerçekten gerekli değilse kullanmayın.

Performans doğrulama için:

-- Sorgu planını incele
EXPLAIN SELECT 
    k.ad,
    GROUP_CONCAT(r.rol SEPARATOR ', ') AS roller
FROM kullaniciler k
JOIN roller r ON k.id = r.kullanici_id
GROUP BY k.id, k.ad;

-- Sonuç uzunluğunu kontrol et (kesme olup olmadığını anlamak için)
SELECT 
    k.ad,
    GROUP_CONCAT(r.rol SEPARATOR ', ') AS roller,
    CHAR_LENGTH(GROUP_CONCAT(r.rol SEPARATOR ', ')) AS uzunluk
FROM kullaniciler k
JOIN roller r ON k.id = r.kullanici_id
GROUP BY k.id, k.ad
HAVING uzunluk > 900;  -- 1024 byte limitine yakın olanları bul

NULL Değerler ile GROUP_CONCAT

GROUP_CONCAT NULL değerleri otomatik olarak atlar, bu genellikle istenen davranıştır. Ama bazen NULL’ları da dahil etmek isteyebilirsiniz:

-- NULL değerler atlanır (varsayılan davranış)
SELECT GROUP_CONCAT(deger SEPARATOR ', ')
FROM test_tablo;

-- NULL değerleri 'Belirtilmemiş' olarak dahil et
SELECT GROUP_CONCAT(
    COALESCE(deger, 'Belirtilmemiş') 
    SEPARATOR ', '
)
FROM test_tablo;

-- Pratik örnek: bazı kullanıcıların telefonu NULL olabilir
SELECT 
    k.ad,
    GROUP_CONCAT(
        COALESCE(i.telefon, 'Tel yok')
        ORDER BY i.iletisim_tipi
        SEPARATOR ' / '
    ) AS iletisim_bilgileri
FROM kullaniciler k
LEFT JOIN iletisim i ON k.id = i.kullanici_id
GROUP BY k.id, k.ad;

Dinamik SQL Üretimi ile GROUP_CONCAT

Sysadmin olarak çok işinize yarayacak bir kullanım: dinamik SQL üretmek. Örneğin tüm tabloları yedeklemek için komut listesi oluşturmak:

-- Bir veritabanındaki tüm tablolar için mysqldump komutları üret
SELECT CONCAT(
    'mysqldump -u root -p veritabani_adi ',
    GROUP_CONCAT(table_name SEPARATOR ' '),
    ' > /backup/partial_backup.sql'
) AS yedek_komutu
FROM information_schema.tables
WHERE table_schema = 'veritabani_adi'
  AND table_type = 'BASE TABLE';

-- Tüm tabloların optimize komutlarını üret
SELECT 
    GROUP_CONCAT(
        CONCAT('OPTIMIZE TABLE `', table_name, '`;')
        SEPARATOR 'n'
    ) AS optimize_komutlari
FROM information_schema.tables
WHERE table_schema = DATABASE()
  AND data_free > 1048576;  -- 1MB üzeri boş alan olan tablolar

Bu tür kullanımlar özellikle maintenance scriptleri yazarken çok zaman kazandırır.

Sonuç

GROUP_CONCAT, MySQL ve MariaDB ekosisteminde sıkça ihtiyaç duyulan ama gücü yeterince bilinmeyen bir fonksiyon. Doğru kullanıldığında uygulama katmanında yapılması gereken onlarca satır kodu veritabanı sorgusu seviyesine indirebilir ve performansı artırabilirsiniz.

En kritik noktaları tekrar özetleyelim:

  • 1024 byte limiti gerçek bir tehlikedir, production’da mutlaka group_concat_max_len değerini ihtiyacınıza göre ayarlayın
  • Sessiz kesme davranışına karşı önemli sorgularda CHAR_LENGTH() ile uzunluk doğrulaması yapın
  • DISTINCT kullanımı sıralama maliyeti getirir, gerçekten gerekmedikçe kullanmayın
  • NULL değerler otomatik atlanır, bunu bir özellik olarak kullanabilir ya da COALESCE ile override edebilirsiniz
  • Index tasarımınızı GROUP BY sütunlarını dikkate alarak yapın

Fonksiyonun en büyük faydası raporlama sorgularında ve API sonuçlarını düzleştirmede ortaya çıkıyor. Bir kez alışkanlık haline getirdiğinizde, birçok senaryoda vazgeçilmez araçlarınızdan biri haline gelecek.

Bir yanıt yazın

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