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ğinde COALESCE ile sıfır doldurun.
  • Index kullanımı: GROUP BY ve WHERE sütunlarınızı mutlaka indeksleyin, EXPLAIN ile doğrulayın.
  • Yuvarlama: ROUND kullanmayı 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/AVG sorguları ç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.

Bir yanıt yazın

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