MySQL ve MariaDB’de COUNT Fonksiyonu ile Kayıt Sayma
Veritabanı yönetiminde en sık kullandığım fonksiyonlardan biri COUNT’tur. Kulağa basit gelir, “kayıt say” der geçersiniz, ama iş pratiğe gelince COUNT’un ne kadar güçlü ve bir o kadar da ince noktalara sahip olduğunu görürsünüz. Yanlış kullanıldığında yavaş sorgular üretir, doğru kullanıldığında saniyeler içinde milyonlarca kaydı analiz edebilirsiniz. Bu yazıda MariaDB ve MySQL üzerinde COUNT fonksiyonunu her açıdan ele alacağız.
COUNT Fonksiyonu Nedir?
COUNT, SQL’de bir sorgu sonucunda kaç satır döndüğünü ya da belirli bir sütunda kaç değer bulunduğunu hesaplayan bir aggregate (toplama) fonksiyonudur. En temel kullanımıyla bir tablodaki toplam kayıt sayısını verir, ama işin içine GROUP BY, HAVING, JOIN ve subquery girince çok daha karmaşık analizler yapmanızı sağlar.
Üç temel kullanım biçimi vardır:
- COUNT(*): Tüm satırları sayar, NULL değerler dahil
- COUNT(sütun_adı): Belirtilen sütundaki NULL olmayan değerleri sayar
- COUNT(DISTINCT sütun_adı): Belirtilen sütundaki tekrar etmeyen değerleri sayar
Bu üç arasındaki fark çok önemlidir ve performans açısından ciddi sonuçlar doğurabilir.
Temel COUNT Kullanımı
Önce basit bir senaryo ile başlayalım. Elimizde bir e-ticaret veritabanı var ve sipariş tablomuzda kaç sipariş olduğunu öğrenmek istiyoruz.
-- Tablodaki toplam kayıt sayısı
SELECT COUNT(*) FROM siparisler;
-- Daha açıklayıcı bir alias ile
SELECT COUNT(*) AS toplam_siparis FROM siparisler;
Bu sorgu tablodaki tüm satırları sayar. Eğer tabloda 500.000 kayıt varsa sonuç 500000 döner. Burada dikkat edilmesi gereken nokta COUNT(*) kullanımının NULL değerleri de saydığıdır. Yani bir satırda tüm sütunlar NULL bile olsa o satır sayıma dahil edilir.
Şimdi COUNT(sütun_adı) ile farkı görelim:
-- musteri_id sütunundaki NULL olmayan değerleri say
SELECT COUNT(musteri_id) AS musteri_olan_siparisler FROM siparisler;
-- Karşılaştırma için her ikisini aynı anda
SELECT
COUNT(*) AS toplam_kayit,
COUNT(musteri_id) AS musteri_id_dolu,
COUNT(notlar) AS notu_olan_siparisler
FROM siparisler;
Eğer notlar sütunu çoğu kayıtta boşsa (NULL), COUNT(notlar) çok daha küçük bir değer döndürecektir. Bu davranışı bilerek kullanmak bazı durumlarda işinizi kolaylaştırır.
DISTINCT ile Benzersiz Kayıt Sayma
Gerçek dünya senaryolarında sıkça karşılaşılan bir ihtiyaç: kaç farklı müşterinin sipariş verdiğini bulmak. Burada DISTINCT devreye giriyor.
-- Sipariş veren benzersiz müşteri sayısı
SELECT COUNT(DISTINCT musteri_id) AS benzersiz_musteri FROM siparisler;
-- Birden fazla sütunla benzersizlik
SELECT COUNT(DISTINCT musteri_id, urun_id) AS musteri_urun_kombinasyonu FROM siparisler;
-- Pratik örnek: Hangi şehirlerden kaç farklı müşterimiz var?
SELECT
sehir,
COUNT(DISTINCT musteri_id) AS musteri_sayisi,
COUNT(*) AS siparis_sayisi
FROM siparisler
JOIN musteriler ON siparisler.musteri_id = musteriler.id
GROUP BY sehir
ORDER BY musteri_sayisi DESC;
DISTINCT kullanımı performans açısından biraz daha maliyetlidir çünkü veritabanı motorunun tekrar eden değerleri elemesi gerekir. Büyük tablolarda bu işlem için uygun index’lerin olması kritik önem taşır.
GROUP BY ile Gruplara Göre Sayma
COUNT fonksiyonunun asıl gücü GROUP BY ile birleşince ortaya çıkar. Verilerinizi belirli kategorilere göre gruplayıp her grubun kaç kayıt içerdiğini görebilirsiniz.
-- Her kategorideki ürün sayısı
SELECT
kategori_id,
COUNT(*) AS urun_sayisi
FROM urunler
GROUP BY kategori_id
ORDER BY urun_sayisi DESC;
-- Kategori adıyla birlikte (JOIN ile)
SELECT
kategoriler.ad AS kategori_adi,
COUNT(urunler.id) AS urun_sayisi,
COUNT(DISTINCT urunler.tedarikci_id) AS tedarikci_sayisi
FROM kategoriler
LEFT JOIN urunler ON kategoriler.id = urunler.kategori_id
GROUP BY kategoriler.id, kategoriler.ad
ORDER BY urun_sayisi DESC;
Burada önemli bir nokta var: LEFT JOIN kullandığımızda ürün olmayan kategoriler de listede görünür, ancak COUNT(urunler.id) onlar için 0 döndürür. COUNT(*) kullansaydık 1 dönerdi çünkü LEFT JOIN bir satır üretir. Bu fark sistematik hatalara yol açabilir, dikkatli olun.
HAVING ile Sonuçları Filtreleme
WHERE aggregate fonksiyonlarla kullanılamaz. Bu durumda HAVING devreye girer. HAVING, GROUP BY sonuçlarını filtrelemek için kullanılır.
-- 10'dan fazla sipariş veren müşteriler
SELECT
musteri_id,
COUNT(*) AS siparis_sayisi
FROM siparisler
GROUP BY musteri_id
HAVING COUNT(*) > 10
ORDER BY siparis_sayisi DESC;
-- WHERE ve HAVING birlikte kullanımı
-- WHERE önce filtreleme yapar, HAVING sonra
SELECT
musteri_id,
COUNT(*) AS siparis_sayisi
FROM siparisler
WHERE siparis_tarihi >= '2024-01-01' -- Önce tarih filtresi
GROUP BY musteri_id
HAVING COUNT(*) >= 5 -- Sonra en az 5 sipariş filtresi
ORDER BY siparis_sayisi DESC
LIMIT 20;
Bir sysadmin olarak şunu söyleyeyim: WHERE ile filtreleyebildiğiniz şeyi HAVING ile filtrelemeyin. WHERE veritabanının işlemesi gereken veri miktarını azaltır, HAVING ise gruplamadan sonra çalışır. Performans farkı büyük tablolarda dramatik olabilir.
Alt Sorgular (Subquery) ile COUNT Kullanımı
Bazen COUNT sonucunu başka bir sorgunun parçası olarak kullanmanız gerekir. Subquery’ler bu noktada işe yarar.
-- Her müşteriyle ilgili sipariş sayısını ana sorguya dahil etme
SELECT
m.id,
m.ad,
m.soyad,
m.email,
(SELECT COUNT(*) FROM siparisler WHERE musteri_id = m.id) AS siparis_sayisi,
(SELECT COUNT(*) FROM siparisler WHERE musteri_id = m.id AND durum = 'tamamlandi') AS tamamlanan
FROM musteriler m
ORDER BY siparis_sayisi DESC
LIMIT 50;
Ancak burada dikkatli olun. Correlated subquery (ilişkili alt sorgu) kullandığınızda her satır için ayrı sorgu çalıştırılır. 10.000 müşteriniz varsa 10.000 ek sorgu demektir. Bu, özellikle production ortamında ciddi performans sorunlarına yol açar. Bunun yerine JOIN kullanmak genellikle daha iyidir:
-- Subquery yerine JOIN ile daha performanslı versiyon
SELECT
m.id,
m.ad,
m.soyad,
m.email,
COALESCE(s.siparis_sayisi, 0) AS siparis_sayisi,
COALESCE(s.tamamlanan, 0) AS tamamlanan
FROM musteriler m
LEFT JOIN (
SELECT
musteri_id,
COUNT(*) AS siparis_sayisi,
SUM(CASE WHEN durum = 'tamamlandi' THEN 1 ELSE 0 END) AS tamamlanan
FROM siparisler
GROUP BY musteri_id
) s ON m.id = s.musteri_id
ORDER BY siparis_sayisi DESC;
Bu sorgu tek seferde tüm hesaplamayı yapar. Büyük veri setlerinde bu fark çok belirgin şekilde hissedilir.
Koşullu COUNT: CASE WHEN ile
Bazen aynı tablodaki farklı koşulları sayan birden fazla COUNT değerini tek sorguda almak istersiniz. CASE WHEN bu iş için biçilmiş kaftandır.
-- Sipariş durumlarına göre dağılım tek sorguda
SELECT
COUNT(*) AS toplam,
COUNT(CASE WHEN durum = 'beklemede' THEN 1 END) AS beklemede,
COUNT(CASE WHEN durum = 'hazirlaniyor' THEN 1 END) AS hazirlaniyor,
COUNT(CASE WHEN durum = 'kargoda' THEN 1 END) AS kargoda,
COUNT(CASE WHEN durum = 'tamamlandi' THEN 1 END) AS tamamlandi,
COUNT(CASE WHEN durum = 'iptal' THEN 1 END) AS iptal_edilen,
ROUND(COUNT(CASE WHEN durum = 'tamamlandi' THEN 1 END) * 100.0 / COUNT(*), 2) AS tamamlanma_yuzdesi
FROM siparisler
WHERE siparis_tarihi BETWEEN '2024-01-01' AND '2024-12-31';
Bu teknik özellikle raporlama sorgularında çok kullanışlıdır. Birden fazla sorgu yerine tek bir sorgu çalıştırıp pivot tablo benzeri sonuçlar elde edebilirsiniz.
Gerçek Dünya Senaryosu: Log Analizi
Sistem yöneticisi olarak en sık kullandığım COUNT senaryolarından biri log analizi. Diyelim ki web sunucusu loglarını MariaDB’ye aktarıyorsunuz ve hangi IP adreslerinin en fazla istek attığını, hangi saatlerde yoğunluk olduğunu analiz etmek istiyorsunuz.
-- En aktif 20 IP adresi
SELECT
ip_adresi,
COUNT(*) AS istek_sayisi,
COUNT(DISTINCT kullanici_agent) AS farkli_tarayici,
MIN(istek_zamani) AS ilk_istek,
MAX(istek_zamani) AS son_istek,
COUNT(CASE WHEN http_kodu >= 400 THEN 1 END) AS hata_sayisi,
ROUND(COUNT(CASE WHEN http_kodu >= 400 THEN 1 END) * 100.0 / COUNT(*), 2) AS hata_yuzdesi
FROM web_loglari
WHERE istek_zamani >= NOW() - INTERVAL 24 HOUR
GROUP BY ip_adresi
HAVING COUNT(*) > 100 -- Sadece 100'den fazla istek atanları göster
ORDER BY istek_sayisi DESC
LIMIT 20;
-- Saatlik trafik dağılımı
SELECT
HOUR(istek_zamani) AS saat,
COUNT(*) AS istek_sayisi,
COUNT(DISTINCT ip_adresi) AS benzersiz_ip,
COUNT(CASE WHEN http_kodu = 200 THEN 1 END) AS basarili,
COUNT(CASE WHEN http_kodu >= 500 THEN 1 END) AS sunucu_hatalari
FROM web_loglari
WHERE DATE(istek_zamani) = CURDATE()
GROUP BY HOUR(istek_zamani)
ORDER BY saat;
Bu tür sorgular güvenlik analizinde de çok işe yarar. 404 hata oranı anormal derecede yüksek olan IP adresleri genellikle otomatik tarayıcılar ya da saldırı araçlarıdır.
COUNT ile Performans: Index Kullanımı
COUNT sorgularının performansı büyük ölçüde index yapısına bağlıdır. InnoDB’de COUNT() için özel bir not düşmek gerekiyor: MyISAM toplam satır sayısını ayrıca sakladığından COUNT() anlık sonuç döndürür, InnoDB ise her seferinde sayar. Bu nedenle büyük InnoDB tablolarında COUNT(*) yavaş olabilir.
-- Sorgu planını inceleyelim
EXPLAIN SELECT COUNT(*) FROM siparisler WHERE musteri_id = 12345;
-- Index oluşturma
CREATE INDEX idx_musteri_id ON siparisler (musteri_id);
-- Composite index (birden fazla koşul varsa)
CREATE INDEX idx_musteri_tarih ON siparisler (musteri_id, siparis_tarihi);
-- Index kullanımını doğrulama
EXPLAIN FORMAT=JSON SELECT
musteri_id,
COUNT(*)
FROM siparisler
WHERE siparis_tarihi >= '2024-01-01'
GROUP BY musteri_id;
EXPLAIN çıktısında “Using index” ibaresini görüyorsanız sorgunuz index üzerinden çalışıyor demektir, bu iyi bir işaret. “Using filesort” ya da “Using temporary” görüyorsanız dikkat edin, bu sorgular büyük tablolarda çok yavaşlayabilir.
Büyük tablolarda yaklaşık kayıt sayısı almanın hızlı yolu şudur:
-- Kesin sayı yerine yaklaşık değer (çok hızlı)
SELECT TABLE_ROWS
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'veritabani_adi'
AND TABLE_NAME = 'siparisler';
-- Tüm tabloların kayıt sayıları
SELECT
TABLE_NAME AS tablo,
TABLE_ROWS AS yaklasik_kayit_sayisi,
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS boyut_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_ROWS DESC;
Bu yaklaşık değerler özellikle kapasite planlaması yaparken işinize yarar.
COUNT ile Window Fonksiyonları (MariaDB 10.2+)
MariaDB 10.2 sürümüyle birlikte gelen window fonksiyonları COUNT’u çok daha güçlü hale getirir. Gruplama yaparken diğer satırları kaybetmeden sayım yapabilirsiniz.
-- Her siparişle birlikte o müşterinin toplam sipariş sayısını göster
SELECT
s.id AS siparis_id,
s.musteri_id,
s.siparis_tarihi,
s.toplam_tutar,
COUNT(*) OVER (PARTITION BY s.musteri_id) AS musterinin_toplam_siparisi,
COUNT(*) OVER (PARTITION BY s.musteri_id ORDER BY s.siparis_tarihi) AS o_ana_kadar_siparis,
COUNT(*) OVER () AS genel_toplam
FROM siparisler s
ORDER BY s.musteri_id, s.siparis_tarihi;
Window fonksiyonları olmadan bu sonucu elde etmek için çok daha karmaşık sorgular yazmak gerekirdi. Burada PARTITION BY GROUP BY gibi çalışır ancak satırları gruplamaz, her satır görünür kalmaya devam eder.
Sık Yapılan Hatalar ve Çözümleri
Yıllar içinde gözlemlediğim yaygın COUNT hatalarını ve çözümlerini paylaşayım.
NULL karışıklığı: COUNT(sütun) ile COUNT(*) farkını göz ardı etmek büyük hatalara yol açar.
-- Hatalı kullanım: musteri_id NULL olan kayıtlar sayılmaz
SELECT COUNT(musteri_id) AS zannedilen_toplam FROM siparisler;
-- Doğru kullanım: Tüm kayıtları saymak istiyorsak
SELECT COUNT(*) AS gercek_toplam FROM siparisler;
-- NULL kontrolü ile beraber
SELECT
COUNT(*) AS toplam,
COUNT(musteri_id) AS musteri_id_dolu,
COUNT(*) - COUNT(musteri_id) AS musteri_id_bos
FROM siparisler;
GROUP BY olmadan COUNT ve diğer sütunlar: SQL standartlarına göre COUNT gibi aggregate fonksiyonlarla birlikte kullanılan sütunların GROUP BY’da yer alması gerekir.
-- Hatalı (MySQL ONLY_FULL_GROUP_BY modunda hata verir)
SELECT musteri_id, ad, COUNT(*) FROM siparisler;
-- Doğru
SELECT musteri_id, COUNT(*) AS siparis_sayisi
FROM siparisler
GROUP BY musteri_id;
-- MariaDB'de ONLY_FULL_GROUP_BY modunu kontrol etme
SELECT @@sql_mode;
-- Modu ayarlama (kalıcı yapmak için my.cnf'e ekleyin)
SET GLOBAL sql_mode = 'STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION';
COUNT yerine EXISTS kullanımı: Sadece kaydın var olup olmadığını kontrol etmek istiyorsanız COUNT yerine EXISTS çok daha hızlıdır.
-- Yavaş yöntem: Gereksiz yere tüm kayıtları sayıyor
SELECT * FROM musteriler m
WHERE (SELECT COUNT(*) FROM siparisler WHERE musteri_id = m.id) > 0;
-- Hızlı yöntem: İlk eşleşmede durur
SELECT * FROM musteriler m
WHERE EXISTS (SELECT 1 FROM siparisler WHERE musteri_id = m.id);
Raporlama Senaryosu: Aylık İstatistikler
Son olarak, pratikte sık kullanacağınız kapsamlı bir raporlama sorgusu yazalım. Aylık satış istatistikleri ve büyüme oranları:
-- Aylık sipariş ve müşteri istatistikleri
SELECT
DATE_FORMAT(siparis_tarihi, '%Y-%m') AS ay,
COUNT(*) AS toplam_siparis,
COUNT(DISTINCT musteri_id) AS aktif_musteri,
COUNT(CASE WHEN durum = 'tamamlandi' THEN 1 END) AS tamamlanan,
COUNT(CASE WHEN durum = 'iptal' THEN 1 END) AS iptal,
ROUND(
COUNT(CASE WHEN durum = 'tamamlandi' THEN 1 END) * 100.0 / COUNT(*),
2
) AS tamamlanma_orani,
-- Bir önceki aya göre sipariş değişimi
COUNT(*) - LAG(COUNT(*)) OVER (ORDER BY DATE_FORMAT(siparis_tarihi, '%Y-%m')) AS siparis_degisimi
FROM siparisler
WHERE siparis_tarihi >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY DATE_FORMAT(siparis_tarihi, '%Y-%m')
ORDER BY ay;
Bu sorguyu cron job ile çalıştırıp sonuçları başka bir tabloya kaydetmek ve dashboard uygulamalarına beslemek oldukça yaygın bir yaklaşımdır.
Sonuç
COUNT fonksiyonu MariaDB ve MySQL’de en temel araçlardan biri, ama “temel” kelimesi onu “basit” yapmıyor. COUNT(*), COUNT(sütun) ve COUNT(DISTINCT) arasındaki farkları iyi anlamak, HAVING ve WHERE’in ne zaman kullanılacağını bilmek, subquery yerine JOIN tercih etmek gibi pratik bilgiler sorgu performansınızı ve doğruluğunuzu doğrudan etkiler.
Büyük tablolarla çalışırken EXPLAIN ile sorgu planınızı daima inceleyin. Index’lerin doğru kullanıldığından emin olun. Sadece var olup olmadığını kontrol ediyorsanız COUNT yerine EXISTS tercih edin. Ve ONLY_FULL_GROUP_BY modunu production ortamınızda aktif tutun, bu sizi belirsiz sonuçlar üreten hatalardan korur.
Pratikte bu örnekleri kendi tablolarınıza uyarlayarak deneyin. COUNT’u iyi öğrenmek, ilerleyen süreçte daha karmaşık aggregate sorgularını ve window fonksiyonlarını öğrenmeniz için sağlam bir temel oluşturur.
