MariaDB ve MySQL’de SUM ve AVG ile Toplam ve Ortalama Hesaplama
Veritabanı yönetiminde en sık karşılaşılan ihtiyaçlardan biri, sayısal veriler üzerinde toplam ve ortalama hesaplamaktır. Satış raporları, kullanıcı istatistikleri, performans metrikleri veya envanter yönetimi gibi onlarca senaryoda SUM ve AVG fonksiyonlarına başvururuz. Bu iki fonksiyon, SQL’in temel yapı taşlarından olup doğru kullanıldığında karmaşık iş analizlerini bile tek bir sorguyla çözebilirsiniz.
Bu yazıda MariaDB ve MySQL üzerinde SUM ve AVG fonksiyonlarını gerçek dünya senaryolarıyla ele alacağız. Basit toplamlardan başlayıp GROUP BY, HAVING, alt sorgular ve pencere fonksiyonlarıyla birlikte kullanımına kadar ilerliyoruz.
Hazırlık: Test Ortamı ve Örnek Veriler
Önce üzerinde çalışacağımız veritabanını ve tabloları oluşturalım. Bir e-ticaret senaryosu düşünüyoruz: siparişler, ürünler ve müşteriler.
mysql -u root -p << 'EOF'
CREATE DATABASE IF NOT EXISTS eticaret;
USE eticaret;
CREATE TABLE musteriler (
id INT AUTO_INCREMENT PRIMARY KEY,
ad VARCHAR(100),
sehir VARCHAR(50),
uyelik_tarihi DATE
);
CREATE TABLE urunler (
id INT AUTO_INCREMENT PRIMARY KEY,
ad VARCHAR(100),
kategori VARCHAR(50),
fiyat DECIMAL(10,2)
);
CREATE TABLE siparisler (
id INT AUTO_INCREMENT PRIMARY KEY,
musteri_id INT,
urun_id INT,
adet INT,
toplam_tutar DECIMAL(10,2),
siparis_tarihi DATE,
FOREIGN KEY (musteri_id) REFERENCES musteriler(id),
FOREIGN KEY (urun_id) REFERENCES urunler(id)
);
INSERT INTO musteriler (ad, sehir, uyelik_tarihi) VALUES
('Ahmet Yilmaz', 'Istanbul', '2022-01-15'),
('Ayse Demir', 'Ankara', '2022-03-20'),
('Mehmet Kaya', 'Izmir', '2021-11-05'),
('Fatma Celik', 'Istanbul', '2023-02-10'),
('Ali Sahin', 'Bursa', '2021-08-30');
INSERT INTO urunler (ad, kategori, fiyat) VALUES
('Laptop', 'Elektronik', 15000.00),
('Klavye', 'Elektronik', 450.00),
('Mouse', 'Elektronik', 250.00),
('Masa', 'Mobilya', 2500.00),
('Sandalye', 'Mobilya', 1200.00);
INSERT INTO siparisler (musteri_id, urun_id, adet, toplam_tutar, siparis_tarihi) VALUES
(1, 1, 1, 15000.00, '2024-01-10'),
(1, 2, 2, 900.00, '2024-01-15'),
(2, 3, 3, 750.00, '2024-02-05'),
(2, 4, 1, 2500.00, '2024-02-20'),
(3, 1, 2, 30000.00, '2024-03-01'),
(3, 5, 1, 1200.00, '2024-03-15'),
(4, 2, 5, 2250.00, '2024-04-10'),
(5, 3, 2, 500.00, '2024-04-22'),
(5, 4, 1, 2500.00, '2024-05-01'),
(1, 5, 2, 2400.00, '2024-05-10');
EOF
Artık elimizde gerçekçi bir veri seti var. Şimdi asıl konumuza geçelim.
SUM Fonksiyonu: Toplam Hesaplama
SUM, belirttiğiniz sütundaki tüm sayısal değerleri toplar. NULL değerleri otomatik olarak görmezden gelir, bu önemli bir detay.
Temel SUM Kullanımı
En basit haliyle tüm siparişlerin toplam tutarını çekiyoruz:
mysql -u root -p eticaret -e "
SELECT
SUM(toplam_tutar) AS genel_toplam,
SUM(adet) AS toplam_adet
FROM siparisler;
"
Bu sorgu size tek satır döndürür ve tablodaki tüm siparişlerin toplamını verir. Gerçek hayatta bu sorguyu genellikle belirli bir tarih aralığıyla filtrelersiniz.
GROUP BY ile Müşteri Bazında Toplam
Her müşterinin ne kadar harcadığını öğrenmek, CRM analizlerinin temelidir:
mysql -u root -p eticaret << 'EOF'
SELECT
m.ad AS musteri_adi,
m.sehir,
COUNT(s.id) AS siparis_sayisi,
SUM(s.adet) AS toplam_urun_adedi,
SUM(s.toplam_tutar) AS toplam_harcama
FROM musteriler m
LEFT JOIN siparisler s ON m.id = s.musteri_id
GROUP BY m.id, m.ad, m.sehir
ORDER BY toplam_harcama DESC;
EOF
LEFT JOIN kullanmamızın sebebi, hiç sipariş vermemiş müşterileri de listeye dahil etmek. INNER JOIN kullansaydık onları göremezdik. COUNT(s.id) ise NULL değerleri saymadığı için sipariş vermemiş müşteriler için 0 döner.
Koşullu SUM ile Gelişmiş Filtreleme
Bazen aynı sorguda birden fazla koşula göre toplam almak istersiniz. Mesela Ocak ve Şubat aylarındaki satışları ayrı ayrı görmek:
mysql -u root -p eticaret << 'EOF'
SELECT
SUM(CASE WHEN MONTH(siparis_tarihi) = 1 THEN toplam_tutar ELSE 0 END) AS ocak_satisi,
SUM(CASE WHEN MONTH(siparis_tarihi) = 2 THEN toplam_tutar ELSE 0 END) AS subat_satisi,
SUM(CASE WHEN MONTH(siparis_tarihi) = 3 THEN toplam_tutar ELSE 0 END) AS mart_satisi,
SUM(CASE WHEN MONTH(siparis_tarihi) >= 4 THEN toplam_tutar ELSE 0 END) AS sonraki_aylar,
SUM(toplam_tutar) AS toplam
FROM siparisler
WHERE YEAR(siparis_tarihi) = 2024;
EOF
Bu teknik pivot sorgusu olarak da bilinir. Satırları sütunlara dönüştürür ve raporlama için son derece kullanışlıdır. Özellikle uygulama katmanında pivot işlemi yapamadığınız durumlarda bu yönteme sıklıkla başvurursunuz.
AVG Fonksiyonu: Ortalama Hesaplama
AVG fonksiyonu, belirtilen sütundaki değerlerin aritmetik ortalamasını alır. SUM / COUNT işlemine eşdeğerdir ama NULL değerleri hem toplamdan hem de sayımdan hariç tutar. Bu nokta kritik.
Temel AVG Kullanımı
Ürün kategorilerine göre ortalama sipariş tutarını hesaplayalım:
mysql -u root -p eticaret << 'EOF'
SELECT
u.kategori,
COUNT(s.id) AS siparis_sayisi,
AVG(s.toplam_tutar) AS ortalama_tutar,
ROUND(AVG(s.toplam_tutar), 2) AS ortalama_tutar_yuvarlanmis,
MIN(s.toplam_tutar) AS en_dusuk,
MAX(s.toplam_tutar) AS en_yuksek
FROM urunler u
JOIN siparisler s ON u.id = s.urun_id
GROUP BY u.kategori;
EOF
ROUND fonksiyonunu AVG ile birlikte kullanmak iyi bir alışkanlık. Ondalık kalabalığını önler ve sonuçlar daha okunabilir olur.
NULL Değerlerin AVG Üzerindeki Etkisi
Bu konuyu anlamak için küçük bir test yapalım:
mysql -u root -p eticaret << 'EOF'
-- Test tablosu oluşturalım
CREATE TEMPORARY TABLE test_null (deger DECIMAL(10,2));
INSERT INTO test_null VALUES (100), (200), (NULL), (300), (NULL);
-- NULL etkisini karşılaştıralım
SELECT
AVG(deger) AS avg_null_haric, -- NULL'ları saymaz: (100+200+300)/3 = 200
SUM(deger)/COUNT(*) AS avg_null_dahil, -- Tüm satırları sayar: 600/5 = 120
SUM(deger)/COUNT(deger) AS avg_manuel -- NULL olmayan satırlar: 600/3 = 200
FROM test_null;
EOF
Gördüğünüz gibi AVG(deger) ile SUM(deger)/COUNT(deger) aynı sonucu verir: 200. Ama SUM/COUNT(*) kullandığınızda NULL satırları da toplam sayıya dahil olur ve ortalama düşer. Hangisi “doğru” hesaplama? İş gereksinimlerinize göre değişir. Eksik veriyi sıfır saymak istiyorsanız COALESCE kullanmanız gerekir.
HAVING ile Filtreleme
WHERE ile HAVING arasındaki farkı burada net görebilirsiniz. WHERE gruplama öncesinde, HAVING ise gruplama sonrasında filtre uygular.
mysql -u root -p eticaret << 'EOF'
-- 1000 TL'nin üzerinde ortalama harcaması olan müşteriler
SELECT
m.ad,
m.sehir,
COUNT(s.id) AS siparis_sayisi,
ROUND(AVG(s.toplam_tutar), 2) AS ortalama_siparis_tutari,
SUM(s.toplam_tutar) AS toplam_harcama
FROM musteriler m
JOIN siparisler s ON m.id = s.musteri_id
GROUP BY m.id, m.ad, m.sehir
HAVING AVG(s.toplam_tutar) > 1000
AND COUNT(s.id) >= 2
ORDER BY ortalama_siparis_tutari DESC;
EOF
Bu sorgu hem belirli bir ortalama eşiğini hem de minimum sipariş sayısını birlikte filtreler. Sadık ve değerli müşterileri bulmak için tipik bir kullanım senaryosudur.
Alt Sorgular ve SUM/AVG Kombinasyonu
Gerçek hayatta tek düzey sorgular genellikle yetmez. Alt sorgularla daha güçlü analizler yapabilirsiniz.
mysql -u root -p eticaret << 'EOF'
-- Ortalamanın üzerinde harcama yapan müşteriler
SELECT
m.ad,
s_ozet.musteri_toplami,
genel.genel_ortalama,
ROUND(s_ozet.musteri_toplami - genel.genel_ortalama, 2) AS fark
FROM musteriler m
JOIN (
SELECT musteri_id, SUM(toplam_tutar) AS musteri_toplami
FROM siparisler
GROUP BY musteri_id
) s_ozet ON m.id = s_ozet.musteri_id
CROSS JOIN (
SELECT AVG(musteri_top) AS genel_ortalama
FROM (
SELECT musteri_id, SUM(toplam_tutar) AS musteri_top
FROM siparisler
GROUP BY musteri_id
) alt
) genel
WHERE s_ozet.musteri_toplami > genel.genel_ortalama
ORDER BY s_ozet.musteri_toplami DESC;
EOF
Bu sorgu biraz karmaşık görünebilir ama adım adım düşünürsek mantıklı bir yapı var. Önce her müşterinin toplam harcamasını hesaplıyoruz, sonra tüm müşterilerin ortalama harcamasını buluyoruz, ardından ortalamanın üzerinde kalanları listeliyoruz. CROSS JOIN burada genel ortalamayı her satıra eklememizi sağlıyor.
Tarih Bazlı Analizler
Zaman serisi analizi, operasyonel raporlamanın önemli bir parçasıdır. Aylık satış raporları buna iyi bir örnek:
mysql -u root -p eticaret << 'EOF'
SELECT
YEAR(siparis_tarihi) AS yil,
MONTH(siparis_tarihi) AS ay,
DATE_FORMAT(siparis_tarihi, '%Y-%m') AS ay_yil,
COUNT(*) AS siparis_adedi,
SUM(toplam_tutar) AS aylik_ciro,
ROUND(AVG(toplam_tutar), 2) AS ortalama_siparis_degeri,
SUM(adet) AS toplam_satilan_urun
FROM siparisler
WHERE siparis_tarihi BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY YEAR(siparis_tarihi), MONTH(siparis_tarihi)
ORDER BY yil, ay;
EOF
Prodüksiyonda bu sorguyu sık çalıştırıyorsanız siparis_tarihi sütununa mutlaka index ekleyin:
mysql -u root -p eticaret -e "
ALTER TABLE siparisler ADD INDEX idx_siparis_tarihi (siparis_tarihi);
EXPLAIN SELECT YEAR(siparis_tarihi), SUM(toplam_tutar)
FROM siparisler
GROUP BY YEAR(siparis_tarihi);
"
EXPLAIN çıktısı ile sorgunuzun index kullanıp kullanmadığını kontrol edin. type kolonunda ALL görüyorsanız full table scan yapıyor demektir; bu büyük tablolarda ciddi performans sorunlarına yol açar.
Pencere Fonksiyonları ile Kümülatif Toplam
MariaDB 10.2 ve MySQL 8.0 ile birlikte gelen pencere fonksiyonları, SUM ve AVG‘yi çok daha güçlü kılar. Kümülatif (rolling) toplamlar için OVER cümleciğini kullanıyoruz:
mysql -u root -p eticaret << 'EOF'
SELECT
DATE_FORMAT(siparis_tarihi, '%Y-%m') AS ay,
SUM(toplam_tutar) AS aylik_ciro,
SUM(SUM(toplam_tutar)) OVER (
ORDER BY DATE_FORMAT(siparis_tarihi, '%Y-%m')
) AS kumulatif_toplam,
ROUND(AVG(SUM(toplam_tutar)) OVER (
ORDER BY DATE_FORMAT(siparis_tarihi, '%Y-%m')
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 2) AS hareketli_ortalama_3ay
FROM siparisler
GROUP BY DATE_FORMAT(siparis_tarihi, '%Y-%m')
ORDER BY ay;
EOF
Bu sorguda iki önemli kavram var. Birincisi kümülatif toplam: her ay için o aya kadar olan tüm satışların toplamını verir. İkincisi ise 3 aylık hareketli ortalama: son 3 ayın ortalamasını alır, bu mevsimsel dalgalanmaları düzleştirmek için idealdir.
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW ifadesi şu anlama gelir: mevcut satır dahil önceki 2 satırı al. Finans raporlarında, stok analizlerinde ve trend takibinde bu yapıyı sıklıkla kullanırsınız.
Performans İpuçları
Büyük tablolarda SUM ve AVG sorgularının yavaşlaması yaygın bir sorundur. Birkaç pratik ipucu:
Index stratejisi: GROUP BY ve WHERE içinde kullandığınız sütunları indeksleyin. Bileşik indexler genellikle tek sütun indexlerden daha verimlidir.
Kapsayıcı index (covering index): Sorgunuzda yalnızca belirli sütunlar kullanılıyorsa, o sütunları içeren bir index oluşturun:
mysql -u root -p eticaret -e "
-- Aylık rapor sorgusu için optimize index
ALTER TABLE siparisler
ADD INDEX idx_tarih_tutar (siparis_tarihi, toplam_tutar, adet);
-- Musteri bazlı rapor için
ALTER TABLE siparisler
ADD INDEX idx_musteri_tutar (musteri_id, toplam_tutar);
"
Özet tablo yaklaşımı: Çok büyük tablolarda (milyonlarca satır) ayrı bir özet tablo tutmak mantıklıdır. Her gün sonu bir cron job ile önceki günün verilerini özetleyin:
mysql -u root -p eticaret << 'EOF'
CREATE TABLE IF NOT EXISTS gunluk_ozet (
tarih DATE PRIMARY KEY,
toplam_siparis INT,
toplam_ciro DECIMAL(15,2),
ortalama_siparis DECIMAL(10,2),
toplam_urun INT
);
-- Her gün çalıştırılacak INSERT ... ON DUPLICATE KEY UPDATE
INSERT INTO gunluk_ozet (tarih, toplam_siparis, toplam_ciro, ortalama_siparis, toplam_urun)
SELECT
DATE(siparis_tarihi),
COUNT(*),
SUM(toplam_tutar),
ROUND(AVG(toplam_tutar), 2),
SUM(adet)
FROM siparisler
WHERE DATE(siparis_tarihi) = CURDATE() - INTERVAL 1 DAY
GROUP BY DATE(siparis_tarihi)
ON DUPLICATE KEY UPDATE
toplam_siparis = VALUES(toplam_siparis),
toplam_ciro = VALUES(toplam_ciro),
ortalama_siparis = VALUES(ortalama_siparis),
toplam_urun = VALUES(toplam_urun);
EOF
Bu sayede dashboard sorguları milyonlarca satır yerine birkaç yüz satırlık özet tablodan okur ve saniyeler içinde sonuç döner.
DISTINCT ile Tekrarsız Toplam ve Ortalama
Bazen aynı değerleri birden fazla kez saymak istemezsiniz. SUM(DISTINCT ...) ve AVG(DISTINCT ...) bu durumda işe yarar:
mysql -u root -p eticaret << 'EOF'
-- Kaç farklı ürün fiyatı var ve bunların ortalaması nedir?
SELECT
COUNT(DISTINCT fiyat) AS farkli_fiyat_sayisi,
AVG(DISTINCT fiyat) AS farkli_fiyatlarin_ortalamasi,
AVG(fiyat) AS tum_fiyatlarin_ortalamasi,
SUM(DISTINCT fiyat) AS farkli_fiyatlarin_toplami
FROM urunler;
EOF
Bu kullanım nadir ama bazen hayat kurtarıcı olabilir. Özellikle veri kalitesinin düşük olduğu tablolarda, mükerrer kayıtların ortalamanıza sızmasını önlemek için başvurursunuz.
Gerçek Hayat Senaryosu: Kategori Performans Raporu
Şimdi her şeyi bir araya getirelim. Bir e-ticaret yöneticisinin haftalık olarak görmek isteyeceği kategori bazlı performans raporu:
mysql -u root -p eticaret << 'EOF'
SELECT
u.kategori,
COUNT(DISTINCT s.musteri_id) AS benzersiz_musteri,
COUNT(s.id) AS toplam_siparis,
SUM(s.adet) AS toplam_satilan_adet,
SUM(s.toplam_tutar) AS toplam_ciro,
ROUND(AVG(s.toplam_tutar), 2) AS ortalama_siparis_tutari,
ROUND(SUM(s.toplam_tutar) / SUM(s.adet), 2) AS adet_basi_ortalama,
ROUND(
SUM(s.toplam_tutar) * 100.0 / SUM(SUM(s.toplam_tutar)) OVER(),
2
) AS ciro_yuzdesi
FROM urunler u
JOIN siparisler s ON u.id = s.urun_id
GROUP BY u.kategori
ORDER BY toplam_ciro DESC;
EOF
SUM(SUM(s.toplam_tutar)) OVER() ifadesi pencere fonksiyonu olup genel toplamı hesaplar. Bu sayede her kategorinin toplam ciroya oranını (yüzdesini) tek bir sorguda elde edersiniz. Normalde bunu iki ayrı sorguyla yapar veya uygulamanızda hesaplardınız; pencere fonksiyonları ile tek geçişte bitiriyoruz.
Sonuç
SUM ve AVG, SQL’in en temel ama en güçlü araçlarından ikisidir. Basit toplamdan kümülatif analize, koşullu pivot sorgularından pencere fonksiyonlarına kadar geniş bir kullanım yelpazesi sunar.
Pratikte dikkat etmeniz gereken birkaç temel nokta var:
- NULL yönetimi:
AVG‘nin NULL’ları nasıl ele aldığını bilin ve gerektiğindeCOALESCEile sıfır doldurun. - Index kullanımı:
GROUP BYveWHEREsütunlarınızı mutlaka indeksleyin,EXPLAINile doğrulayın. - Yuvarlama:
ROUNDkullanmayı alışkanlık haline getirin, ondalık kalabalığı raporları okunamaz hale getirir. - Büyük tablolar için özet tablo: Milyonlarca satır üzerinde anlık
SUM/AVGsorguları çalıştırmak yerine periyodik özet tablolar tutun. - Pencere fonksiyonları: MariaDB 10.2+ veya MySQL 8.0+ kullanıyorsanız
OVER()cümleciğini öğrenin; karmaşık raporlamaları tek sorguda çözüyor.
Bu fonksiyonları GROUP BY, HAVING ve alt sorgularla doğru kombine ettiğinizde, uygulama katmanında saatler sürecek hesaplamaları veritabanı motorunun optimize edilmiş yürütme planıyla saniyeler içinde tamamlayabilirsiniz.
