MariaDB ve MySQL’de En Çok Tekrar Eden Değeri Bulma Sorgusu

Veritabanlarında analiz yaparken en sık karşılaştığımız ihtiyaçlardan biri şu: “Bu sütunda en çok hangi değer var?” Müşteri siparişlerinde en popüler ürün hangisi, log tablomuzda en çok hangi hata kodu tekrar ediyor, kullanıcı tablomuzda en yaygın şehir hangisi… Bu tür sorular günlük operasyonlarda sıkça karşımıza çıkar ve doğru SQL sorgusunu bilmiyorsan saatler kaybedebilirsin.

Bu yazıda MariaDB ve MySQL üzerinde en çok tekrar eden değeri bulma tekniklerini, farklı senaryolar için optimize edilmiş sorgu örnekleriyle anlatacağım. Hem basit tek sütun sorgularından hem de çok tablolu karmaşık analizlerden bahsedeceğiz.

Temel Yaklaşım: GROUP BY ve COUNT Kombinasyonu

En sık tekrar eden değeri bulmanın en klasik ve anlaşılır yolu GROUP BY ile COUNT fonksiyonunu birlikte kullanmaktır. Mantık basit: değerleri grupla, her grubun kaç kez geçtiğini say, en yüksek sayıya göre sırala ve ilk sonucu al.

Diyelim ki bir e-ticaret sitesinin sipariş tablosu var ve en çok sipariş edilen ürün kategorisini bulmak istiyoruz:

SELECT kategori, COUNT(*) AS tekrar_sayisi
FROM siparisler
GROUP BY kategori
ORDER BY tekrar_sayisi DESC
LIMIT 1;

Bu sorgu son derece verimli çalışır. ORDER BY ... DESC ile büyükten küçüğe sıralarız, LIMIT 1 ile de sadece en üstteki sonucu çekeriz. Eğer birden fazla değer aynı sayıda tekrar ediyorsa bu sorgu sadece birini getirir, bunu aklında tut.

Beraberlik Durumu: Aynı Sayıda Tekrar Eden Birden Fazla Değer

Gerçek hayatta çoğu zaman birden fazla değer aynı frekansa sahip olabilir. Bunu yönetmek için sorguyu biraz genişletmemiz gerekir:

SELECT kategori, COUNT(*) AS tekrar_sayisi
FROM siparisler
GROUP BY kategori
HAVING COUNT(*) = (
    SELECT COUNT(*) AS max_sayi
    FROM siparisler
    GROUP BY kategori
    ORDER BY max_sayi DESC
    LIMIT 1
)
ORDER BY kategori;

Bu sorgu alt sorgu ile maksimum tekrar sayısını hesaplar, sonra HAVING ile sadece o sayıya eşit olan tüm grupları filtreler. Yani beraberlik varsa hepsini görürsün.

Log Tablosu Senaryosu: Hata Kodlarını Analiz Etmek

Sistem yöneticisi olarak en sık kullandığım senaryo log analizi. Uygulama loglarını veritabanına yazan bir sistemde en çok hangi hata kodunun fırlatıldığını bulmak incident çözümünde kritik öneme sahip.

Önce örnek tablomuzu oluşturalım:

CREATE TABLE uygulama_loglari (
    id INT AUTO_INCREMENT PRIMARY KEY,
    hata_kodu VARCHAR(20),
    mesaj TEXT,
    sunucu_adi VARCHAR(50),
    olusturma_tarihi DATETIME DEFAULT NOW()
);

Şimdi bu tabloda en çok tekrar eden hata kodunu bulalım:

SELECT 
    hata_kodu,
    COUNT(*) AS kac_kez,
    ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM uygulama_loglari), 2) AS yuzde
FROM uygulama_loglari
WHERE olusturma_tarihi >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
GROUP BY hata_kodu
ORDER BY kac_kez DESC
LIMIT 5;

Bu sorguda birkaç önemli nokta var. DATE_SUB(NOW(), INTERVAL 24 HOUR) ile son 24 saati filtreliyoruz, yani anlık sorun analizi yapıyoruz. Yüzde hesabı da ekledik, böylece “500 hata var” yerine “toplam hataların yüzde 34’ü bu koddan oluşuyor” gibi daha anlamlı bir sonuç elde ediyoruz. LIMIT 5 ile de sadece ilk beşi alıyoruz.

Çok Sütunlu Analiz: Hangi Sunucuda Hangi Hata En Fazla?

Bazen tek bir sütun yetmez, birleşik analiz yapmamız gerekir. Örneğin hangi sunucuda hangi hata kodunun en fazla tekrar ettiğini bulmak istiyoruz:

SELECT 
    sunucu_adi,
    hata_kodu,
    COUNT(*) AS tekrar_sayisi
FROM uygulama_loglari
GROUP BY sunucu_adi, hata_kodu
ORDER BY sunucu_adi, tekrar_sayisi DESC;

Ama bu sorgu her sunucu için tüm hata kodlarını listeler. Eğer her sunucu için sadece en çok tekrar eden tek hata kodunu istiyorsak, işte o zaman biraz daha akıllı olmamız gerekiyor:

SELECT l1.sunucu_adi, l1.hata_kodu, l1.tekrar_sayisi
FROM (
    SELECT sunucu_adi, hata_kodu, COUNT(*) AS tekrar_sayisi
    FROM uygulama_loglari
    GROUP BY sunucu_adi, hata_kodu
) AS l1
INNER JOIN (
    SELECT sunucu_adi, MAX(tekrar_sayisi) AS max_sayi
    FROM (
        SELECT sunucu_adi, hata_kodu, COUNT(*) AS tekrar_sayisi
        FROM uygulama_loglari
        GROUP BY sunucu_adi, hata_kodu
    ) AS alt
    GROUP BY sunucu_adi
) AS l2 ON l1.sunucu_adi = l2.sunucu_adi AND l1.tekrar_sayisi = l2.max_sayi
ORDER BY l1.sunucu_adi;

Bu sorgu biraz kalabalık görünüyor ama mantığı basit. İlk alt sorgu her sunucu-hata kombinasyonu için sayıyı hesaplar. İkinci alt sorgu her sunucu için maksimum sayıyı bulur. Dış JOIN ise bu ikisini birleştirir.

MariaDB 10.2+ için Window Functions Kullanımı

Eğer MariaDB 10.2 veya üzerini kullanıyorsan (ki 2024’te hala eski versiyonda kalıyorsan nedenini sorgulamalısın), ROW_NUMBER() ve RANK() gibi pencere fonksiyonlarıyla çok daha temiz sorgular yazabilirsin:

SELECT sunucu_adi, hata_kodu, tekrar_sayisi
FROM (
    SELECT 
        sunucu_adi,
        hata_kodu,
        COUNT(*) AS tekrar_sayisi,
        ROW_NUMBER() OVER (
            PARTITION BY sunucu_adi 
            ORDER BY COUNT(*) DESC
        ) AS sira
    FROM uygulama_loglari
    GROUP BY sunucu_adi, hata_kodu
) AS siralanmis
WHERE sira = 1
ORDER BY sunucu_adi;

PARTITION BY sunucu_adi her sunucu için ayrı bir sıralama penceresi açar. ROW_NUMBER() her pencerede 1’den başlar. Dış sorguda sira = 1 filtresini uygulayınca her sunucu için sadece en yüksek sayılı hata kodu kalır. RANK() kullanırsan beraberliklerde aynı sıra numarası verilir, ROW_NUMBER() kullanırsan beraberlikte rastgele birini seçer.

Kullanıcı Davranışı Analizi: En Aktif Saati Bulmak

Bir başka gerçek dünya senaryosu: sitenize en fazla trafik gelen saati bulmak. Bu bilgi kapasite planlaması için altın değerinde.

SELECT 
    HOUR(istek_zamani) AS saat,
    COUNT(*) AS istek_sayisi,
    CONCAT(ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER(), 1), '%') AS trafik_orani
FROM web_istekleri
WHERE istek_zamani >= CURDATE() - INTERVAL 7 DAY
GROUP BY HOUR(istek_zamani)
ORDER BY istek_sayisi DESC
LIMIT 1;

HOUR() fonksiyonu datetime değerinden sadece saat kısmını çeker (0-23 arası). Son 7 günün verisine bakıyoruz, bu sayede tek günün anormalliğine takılmıyoruz. SUM(COUNT(*)) OVER() pencere fonksiyonu ile toplam istek sayısına bölüp yüzde hesaplıyoruz.

NULL Değerlerin Yönetimi

Dikkat edilmesi gereken önemli bir nokta: sorgulayacağın sütun NULL değerler içerebilir. COUNT(*) NULL’ları da sayar, ama COUNT(sutun_adi) NULL’ları atlar. Hangi davranışı istediğine göre seçim yapmalısın.

Eğer NULL’ları “bilinmiyor” gibi ayrı bir kategori olarak saymak istiyorsan:

SELECT 
    COALESCE(kategori, 'Bilinmiyor') AS kategori,
    COUNT(*) AS tekrar_sayisi
FROM siparisler
GROUP BY COALESCE(kategori, 'Bilinmiyor')
ORDER BY tekrar_sayisi DESC
LIMIT 1;

COALESCE fonksiyonu NULL değerleri belirttiğin string ile değiştirir. Böylece NULL’lar da gruplama işlemine dahil edilir.

NULL’ları tamamen dışlamak istiyorsan:

SELECT kategori, COUNT(*) AS tekrar_sayisi
FROM siparisler
WHERE kategori IS NOT NULL
GROUP BY kategori
ORDER BY tekrar_sayisi DESC
LIMIT 1;

Performans Optimizasyonu: INDEX Kullanımı

Büyük tablolarda bu tür sorgular indeks olmadan çok yavaş çalışır. Milyonlarca kayıt içeren bir tabloda GROUP BY full table scan yapabilir ve bu saniyeler, hatta dakikalar sürebilir.

Analiz yaptığın sütuna INDEX eklemek sorgu performansını dramatik biçimde artırır:

-- Tek sütun index
ALTER TABLE uygulama_loglari ADD INDEX idx_hata_kodu (hata_kodu);

-- Composite index (hem filtreleme hem gruplama için)
ALTER TABLE uygulama_loglari ADD INDEX idx_sunucu_hata (sunucu_adi, hata_kodu);

-- Tarih filtreli sorgular için
ALTER TABLE uygulama_loglari ADD INDEX idx_tarih_hata (olusturma_tarihi, hata_kodu);

Sorgunun index kullanıp kullanmadığını görmek için EXPLAIN kullanmayı unutma:

EXPLAIN SELECT hata_kodu, COUNT(*) AS tekrar_sayisi
FROM uygulama_loglari
GROUP BY hata_kodu
ORDER BY tekrar_sayisi DESC
LIMIT 1;

EXPLAIN çıktısında type sütununda ALL görüyorsan full table scan var demektir, bu kötü. ref veya index görüyorsan index kullanılıyor, bu iyi. rows sütunundaki sayı da ne kadar satır tarandığını gösterir.

Stored Procedure ile Yeniden Kullanılabilir Sorgu

Aynı türde analizleri sık sık yapıyorsan, bunu bir stored procedure haline getirmek hem zaman kazandırır hem de tutarlılık sağlar:

DELIMITER //

CREATE PROCEDURE en_cok_tekrar_eden(
    IN tablo_adi VARCHAR(64),
    IN sutun_adi VARCHAR(64),
    IN sonuc_sayisi INT
)
BEGIN
    SET @sorgu = CONCAT(
        'SELECT ', sutun_adi, ', COUNT(*) AS tekrar_sayisi ',
        'FROM ', tablo_adi, ' ',
        'GROUP BY ', sutun_adi, ' ',
        'ORDER BY tekrar_sayisi DESC ',
        'LIMIT ', sonuc_sayisi
    );
    
    PREPARE stmt FROM @sorgu;
    EXECUTE stmt;
    DEALLOCATE PREPARE stmt;
END //

DELIMITER ;

-- Kullanım:
CALL en_cok_tekrar_eden('siparisler', 'kategori', 3);
CALL en_cok_tekrar_eden('uygulama_loglari', 'hata_kodu', 5);

Bu procedure’ü her tablo ve sütun kombinasyonu için rahatlıkla kullanabilirsin. Parametreleri kontrol altında tutmak için ek validasyon eklemen önerilir, ama günlük operasyon araçları için bu haliyle yeterli.

Zaman Dilimlerine Göre Karşılaştırmalı Analiz

Peki ya geçen hafta en çok tekrar eden değer bu hafta değiştiyse? Bunu yakalamak için dönemsel karşılaştırma sorgusu çok işe yarar:

SELECT 
    'Bu Hafta' AS donem,
    hata_kodu,
    COUNT(*) AS sayi
FROM uygulama_loglari
WHERE olusturma_tarihi >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY hata_kodu
ORDER BY sayi DESC
LIMIT 1

UNION ALL

SELECT 
    'Gecen Hafta' AS donem,
    hata_kodu,
    COUNT(*) AS sayi
FROM uygulama_loglari
WHERE olusturma_tarihi >= DATE_SUB(CURDATE(), INTERVAL 14 DAY)
  AND olusturma_tarihi < DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY hata_kodu
ORDER BY sayi DESC
LIMIT 1;

UNION ALL iki farklı sorgunun sonuçlarını birleştirir. Böylece tek sorguda hem bu haftanın hem de geçen haftanın en sık hata kodunu yan yana görürsün. Eğer değer değiştiyse, muhtemelen yeni bir sorun ortaya çıkmış demektir.

Koşullu Sayım: CASE ile Gelişmiş Analiz

Bazen belirli bir koşulu karşılayan değerler arasında en sık tekrar edeni bulmak istersin. Örneğin sadece başarısız işlemler arasında en sık tekrar eden hata kodunu bulmak:

SELECT 
    hata_kodu,
    COUNT(*) AS toplam,
    SUM(CASE WHEN islem_durumu = 'BASARISIZ' THEN 1 ELSE 0 END) AS basarisiz_sayi,
    SUM(CASE WHEN islem_durumu = 'BASARILI' THEN 1 ELSE 0 END) AS basarili_sayi,
    ROUND(
        SUM(CASE WHEN islem_durumu = 'BASARISIZ' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 
        1
    ) AS basarisizlik_orani
FROM islem_loglari
GROUP BY hata_kodu
HAVING basarisiz_sayi > 0
ORDER BY basarisiz_sayi DESC
LIMIT 10;

CASE WHEN ... THEN 1 ELSE 0 END yapısı her satır için koşullu sayım yapar. SUM() içine aldığında belirli bir koşulu karşılayan satırları sayabilirsin. HAVING ise WHERE gibi filtreleme yapar ama GROUP BY sonrası çalışır, yani aggregate fonksiyonlarla filtre yapabilirsin.

Monitoring Script: Otomatik Anomali Tespiti

Bunu bir adım daha ileriye taşıyalım. Aşağıdaki bash script’i cron’a ekleyerek her saat en sık hata kodunu loglar ve önceki saate göre büyük değişim varsa seni uyarır:

#!/bin/bash

DB_HOST="localhost"
DB_USER="monitor_user"
DB_PASS="monitor_pass"
DB_NAME="uygulama_db"
ESIK=50  # Yüzde artış eşiği

ONCEKI_SAAT=$(mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME -se "
    SELECT COALESCE(MAX(tekrar_sayisi), 0)
    FROM uygulama_loglari
    WHERE olusturma_tarihi >= DATE_SUB(NOW(), INTERVAL 2 HOUR)
      AND olusturma_tarihi < DATE_SUB(NOW(), INTERVAL 1 HOUR)
    GROUP BY hata_kodu
    ORDER BY COUNT(*) DESC
    LIMIT 1;
")

BU_SAAT=$(mysql -h $DB_HOST -u $DB_USER -p$DB_PASS $DB_NAME -se "
    SELECT COALESCE(MAX(tekrar_sayisi), 0)
    FROM (
        SELECT hata_kodu, COUNT(*) AS tekrar_sayisi
        FROM uygulama_loglari
        WHERE olusturma_tarihi >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
        GROUP BY hata_kodu
    ) AS alt;
")

if [ "$ONCEKI_SAAT" -gt 0 ]; then
    ARTIS=$(( (BU_SAAT - ONCEKI_SAAT) * 100 / ONCEKI_SAAT ))
    if [ "$ARTIS" -gt "$ESIK" ]; then
        echo "UYARI: Hata frekansinda %$ARTIS artis tespit edildi!" | 
        mail -s "DB Anomali Uyarisi" [email protected]
    fi
fi

echo "$(date): Bu saat en yuksek hata tekrari: $BU_SAAT" >> /var/log/db_monitor.log

Bu script’i /etc/cron.d/db_monitor dosyasına ekleyerek saatlik çalıştırabilirsin. ESIK değişkenini ortamına göre ayarla, yüzde 50 artış genellikle makul bir uyarı eşiği.

Dikkat Edilmesi Gereken Yaygın Hatalar

Büyük/Küçük Harf Duyarlılığı: MySQL ve MariaDB default olarak VARCHAR karşılaştırmalarında büyük/küçük harf duyarsızdır. Ama BINARY modifier kullanılmışsa veya collation _cs ile bitiyorsa duyarlı hale gelir. hata_kodu = 'ERROR_404' ve hata_kodu = 'error_404' aynı grup sayılabilir, dikkat et.

Çok Büyük Tablolarda LIMIT Yanıltıcılığı: ORDER BY ... LIMIT 1 büyük tablolarda tüm gruplanmış sonuçları sıralaması gerektiğinden yine de yavaş olabilir. Bu durumda geçici tablo veya materialized view alternatif düşünülebilir.

COUNT() vs COUNT(1) Tartışması: Performans açısından bu ikisi pratikte neredeyse aynıdır, MariaDB ve MySQL optimizer ikisini de aynı şekilde işler. COUNT() okunabilirlik açısından daha açıktır, onu kullan.

GROUP BY ile SELECT Uyumsuzluğu: MySQL’in eski sürümlerinde only_full_group_by modu kapalı olabilir ve SELECT içinde GROUP BY‘da olmayan sütunlar kullanabilirsin. Bu güvenilmez sonuçlara yol açar. Her zaman GROUP BY‘daki sütunların SELECT‘te ya aggregate fonksiyonla ya da direkt olarak listelendiğinden emin ol.

Sonuç

En çok tekrar eden değeri bulmak kulağa basit bir operasyon gibi gelse de farklı senaryo gereksinimleri, beraberlik durumları, NULL yönetimi ve performans optimizasyonu gibi konuları düşününce ciddi bir konu olduğu ortaya çıkıyor.

Özetlemek gerekirse: basit durumlar için GROUP BY + COUNT + ORDER BY DESC + LIMIT 1 kombinasyonu her zaman işe yarar ve anlaşılırdır. Beraberlik durumunu yönetmek için HAVING ile alt sorgu kullanmak gerekir. Çok gruplu analizlerde MariaDB 10.2+ ile gelen ROW_NUMBER() OVER (PARTITION BY ...) syntax’ı çok daha temiz ve verimli kod yazmana olanak sağlar. Büyük tablolarda INDEX eklemeyi ve EXPLAIN ile sorgu planını kontrol etmeyi ihmal etme. Operasyonel kullanım için monitoring script’leri kurarak anomali tespitini otomatikleştir.

Bu sorgu kalıpları günlük DBA işlerinde, uygulama performans analizlerinde, güvenlik log incelemelerinde ve kapasite planlamasında sana sürekli lazım olacak. Doğru sorguyu doğru zamanda yazmak, saatlik incident süresini dakikalara indirebilir.

Bir yanıt yazın

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