SUM/AVG/COUNT OVER — Running Total
Giriş — Rakamlar Tek Başına Anlam İfade Etmez
Bir e-ticaret raporunda "Mart ayı geliri: 312.000 TL" yazdığını düşün. Bu iyi mi kötü mü? Tek başına bir anlam ifade etmiyor. Ama "Mart ayı geliri: 312.000 TL — Yılın başından bu yana toplam: 825.000 TL — 3 aylık hareketli ortalama: 275.000 TL" dersen, resim netleşir.
Running total (kümülatif toplam), moving average (hareketli ortalama) ve yüzdelik paylar — bunlar raporlamada en çok ihtiyaç duyulan hesaplamalardır. Ve hepsi aggregate fonksiyonların OVER() ile kullanılmasıyla çözülür.
Bir önceki derslerde AVG(salary) OVER (PARTITION BY department_id) gibi basit partition hesaplamalarını gördük. Bu derste işi bir üst seviyeye taşıyoruz: ORDER BY eklendiğinde aggregate fonksiyonların nasıl kümülatif çalıştığını öğreneceğiz.
Kümülatif Toplam (Running Total)
Temel Kavram
ORDER BY eklendiğinde SUM() fonksiyonu kümülatif çalışır — her satırda, o satıra kadar olan tüm değerlerin toplamını verir:
-- Siparişlerin kümülatif toplamı
SELECT
order_id,
order_date,
total_amount,
SUM(total_amount) OVER (ORDER BY order_date) AS running_total
FROM orders
WHERE customer_id = 101
ORDER BY order_date;+----------+------------+--------------+---------------+
| order_id | order_date | total_amount | running_total |
+----------+------------+--------------+---------------+
| 1001 | 2024-01-15 | 1500 | 1500 | ← 1500
| 1045 | 2024-03-22 | 2200 | 3700 | ← 1500 + 2200
| 1067 | 2024-05-10 | 800 | 4500 | ← 1500 + 2200 + 800
| 1089 | 2024-07-10 | 1500 | 6000 | ← 1500 + 2200 + 800 + 1500
| 1112 | 2024-09-05 | 3200 | 9200 | ← toplam
+----------+------------+--------------+---------------+Her satırda, o tarihe kadar olan siparişlerin kümülatif toplamı görünüyor. Son satırdaki running_total, müşterinin tüm zamanlar boyunca toplam harcamasıdır.
🎯 Analoji: Running total, bankadaki hesap bakiyen gibi düşün. Her işlemde bakiyen güncellenir — 1000 TL yatırdın (bakiye: 1000), 500 TL daha yatırdın (bakiye: 1500), 200 TL çektin (bakiye: 1300). Her satırda o ana kadarki toplam bakiyeyi görürsün.
Aylık Kümülatif Gelir
-- Yıl boyunca kümülatif gelir (YTD — Year-To-Date)
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(total_amount) AS monthly_revenue,
SUM(SUM(total_amount)) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) AS ytd_revenue
FROM orders
WHERE YEAR(order_date) = 2024
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;+---------+-----------------+-------------+
| month | monthly_revenue | ytd_revenue |
+---------+-----------------+-------------+
| 2024-01 | 245000 | 245000 |
| 2024-02 | 268000 | 513000 |
| 2024-03 | 312000 | 825000 |
| 2024-04 | 287000 | 1112000 |
| 2024-05 | 334000 | 1446000 |
+---------+-----------------+-------------+Dikkat: SUM(SUM(total_amount)) — iç SUM GROUP BY'ın aggregate'i, dış SUM window function. GROUP BY ile window function birlikte çalışıyor.
PARTITION BY ile Kümülatif Toplam
-- Her müşterinin kümülatif harcaması (müşteri bazlı running total)
SELECT
c.first_name,
o.order_date,
o.total_amount,
SUM(o.total_amount) OVER (
PARTITION BY o.customer_id
ORDER BY o.order_date
) AS customer_running_total
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
ORDER BY c.first_name, o.order_date;+------------+------------+--------------+------------------------+
| first_name | order_date | total_amount | customer_running_total |
+------------+------------+--------------+------------------------+
| Ali | 2024-01-15 | 1500 | 1500 |
| Ali | 2024-03-22 | 2200 | 3700 |
| Ali | 2024-07-10 | 1500 | 5200 |
| Zeynep | 2024-02-01 | 3400 | 3400 | ← Sıfırlandı!
| Zeynep | 2024-05-18 | 4700 | 8100 |
+------------+------------+--------------+------------------------+Her müşteri için ayrı running total hesaplanıyor. Zeynep'in toplamı Ali'nin toplamından bağımsız.
Kümülatif Ortalama (Running Average)
-- Siparişlerin kümülatif ortalaması
SELECT
order_id,
order_date,
total_amount,
ROUND(AVG(total_amount) OVER (ORDER BY order_date), 2) AS running_avg
FROM orders
WHERE customer_id = 101
ORDER BY order_date;+----------+------------+--------------+-------------+
| order_id | order_date | total_amount | running_avg |
+----------+------------+--------------+-------------+
| 1001 | 2024-01-15 | 1500 | 1500.00 | ← 1500/1
| 1045 | 2024-03-22 | 2200 | 1850.00 | ← (1500+2200)/2
| 1067 | 2024-05-10 | 800 | 1500.00 | ← (1500+2200+800)/3
| 1089 | 2024-07-10 | 1500 | 1500.00 | ← (1500+2200+800+1500)/4
| 1112 | 2024-09-05 | 3200 | 1840.00 | ← (toplam)/5
+----------+------------+--------------+-------------+Kümülatif Sayım (Running Count)
-- Kümülatif sipariş sayısı
SELECT
order_date,
order_id,
COUNT(*) OVER (ORDER BY order_date) AS cumulative_orders
FROM orders
WHERE YEAR(order_date) = 2024
ORDER BY order_date;Bu, "bugüne kadar kaç sipariş alındı?" sorusunu her satırda cevaplar.
Hareketli Ortalama (Moving Average)
Running average tüm geçmişi hesaba katar. Ama bazen sadece son N dönemi dikkate almak istersin — buna hareketli ortalama (moving average) denir. Frame specification kullanarak çözülür:
-- 3 aylık hareketli ortalama
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(total_amount) AS monthly_revenue,
ROUND(
AVG(SUM(total_amount)) OVER (
ORDER BY DATE_FORMAT(order_date, '%Y-%m')
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 0
) AS moving_avg_3m
FROM orders
WHERE YEAR(order_date) = 2024
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;+---------+-----------------+---------------+
| month | monthly_revenue | moving_avg_3m |
+---------+-----------------+---------------+
| 2024-01 | 245000 | 245000 | ← Sadece Ocak (1 ay)
| 2024-02 | 268000 | 256500 | ← (245+268)/2 (2 ay)
| 2024-03 | 312000 | 275000 | ← (245+268+312)/3
| 2024-04 | 287000 | 289000 | ← (268+312+287)/3
| 2024-05 | 334000 | 311000 | ← (312+287+334)/3
+---------+-----------------+---------------+ROWS BETWEEN 2 PRECEDING AND CURRENT ROW demek: "şu anki satır + 2 önceki satır = toplam 3 satır." İlk 2 ayda yeterli geçmiş veri olmadığı için mevcut satırlarla hesaplanır.
💡 İpucu: Moving average, finansal raporlarda trend analizi için çok kullanılır. 7 günlük, 30 günlük, 90 günlük hareketli ortalamalar yaygın örneklerdir. Kısa dönem hareketli ortalama, mevsimsel dalgalanmaları yumuşatır.
7 Günlük Hareketli Ortalama
-- Günlük sipariş sayısının 7 günlük hareketli ortalaması
SELECT
order_date,
COUNT(*) AS daily_orders,
ROUND(
AVG(COUNT(*)) OVER (
ORDER BY order_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 1
) AS moving_avg_7d
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY order_date
ORDER BY order_date;Yüzdelik Pay Hesaplama
Her satırın toplam içindeki payını hesaplamak, window function'larla çok kolay:
-- Her ürünün kategori gelirindeki payı
SELECT
p.product_name,
c.category_name,
SUM(oi.quantity * oi.unit_price) AS product_revenue,
SUM(SUM(oi.quantity * oi.unit_price)) OVER (
PARTITION BY p.category_id
) AS category_total,
ROUND(
SUM(oi.quantity * oi.unit_price) * 100.0 /
SUM(SUM(oi.quantity * oi.unit_price)) OVER (PARTITION BY p.category_id),
1
) AS pct_of_category
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
JOIN categories c ON p.category_id = c.category_id
GROUP BY p.product_id, p.product_name, c.category_name, p.category_id
ORDER BY c.category_name, pct_of_category DESC;+-------------------+---------------+-----------------+----------------+-----------------+
| product_name | category_name | product_revenue | category_total | pct_of_category |
+-------------------+---------------+-----------------+----------------+-----------------+
| iPhone 15 | Elektronik | 12500000 | 25000000 | 50.0 |
| MacBook Pro | Elektronik | 8750000 | 25000000 | 35.0 |
| Samsung Galaxy | Elektronik | 3750000 | 25000000 | 15.0 |
| SQL Kitabı | Kitap | 120000 | 250000 | 48.0 |
| Python Kitabı | Kitap | 130000 | 250000 | 52.0 |
+-------------------+---------------+-----------------+----------------+-----------------+Toplam İçinde Pay (Pareto Analizi)
-- Pareto analizi: ürünlerin gelir payı ve kümülatif payı
SELECT
product_name,
revenue,
total_revenue,
ROUND(revenue * 100.0 / total_revenue, 1) AS pct,
ROUND(cumulative_revenue * 100.0 / total_revenue, 1) AS cumulative_pct
FROM (
SELECT
p.product_name,
SUM(oi.quantity * oi.unit_price) AS revenue,
SUM(SUM(oi.quantity * oi.unit_price)) OVER () AS total_revenue,
SUM(SUM(oi.quantity * oi.unit_price)) OVER (
ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS cumulative_revenue
FROM order_items oi
JOIN products p ON oi.product_id = p.product_id
GROUP BY p.product_id, p.product_name
) analysis
ORDER BY revenue DESC;+-------------------+----------+---------------+------+----------------+
| product_name | revenue | total_revenue | pct | cumulative_pct |
+-------------------+----------+---------------+------+----------------+
| iPhone 15 | 12500000 | 30000000 | 41.7 | 41.7 |
| MacBook Pro | 8750000 | 30000000 | 29.2 | 70.8 | ← İlk 2 ürün %70
| Samsung Galaxy | 3750000 | 30000000 | 12.5 | 83.3 |
| Python Kitabı | 130000 | 30000000 | 0.4 | 83.8 |
| ... | ... | ... | ... | ... |
+-------------------+----------+---------------+------+----------------+Bu, klasik Pareto (80/20) analizi: gelirin %80'ini hangi ürünler oluşturuyor? cumulative_pct sütununa bakarak karar verebilirsin.
MIN ve MAX OVER
-- Her siparişin yanında müşterinin min, max ve ortalama sipariş tutarı
SELECT
c.first_name,
o.order_id,
o.total_amount,
MIN(o.total_amount) OVER (PARTITION BY o.customer_id) AS min_order,
MAX(o.total_amount) OVER (PARTITION BY o.customer_id) AS max_order,
ROUND(AVG(o.total_amount) OVER (PARTITION BY o.customer_id), 0) AS avg_order
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
ORDER BY c.first_name, o.order_date;+------------+----------+--------------+-----------+-----------+-----------+
| first_name | order_id | total_amount | min_order | max_order | avg_order |
+------------+----------+--------------+-----------+-----------+-----------+
| Ali | 1001 | 1500 | 800 | 3200 | 1840 |
| Ali | 1045 | 2200 | 800 | 3200 | 1840 |
| Ali | 1067 | 800 | 800 | 3200 | 1840 |
| Ali | 1089 | 1500 | 800 | 3200 | 1840 |
| Ali | 1112 | 3200 | 800 | 3200 | 1840 |
+------------+----------+--------------+-----------+-----------+-----------+PARTITION BY var ama ORDER BY yok — bu yüzden kümülatif değil, tüm partition üzerinde hesaplanıyor.
Normalleştirme (0-1 Arası Ölçekleme)
-- Maaşları 0-1 arası normalize et (Min-Max normalization)
SELECT
first_name,
department_id,
salary,
MIN(salary) OVER (PARTITION BY department_id) AS min_sal,
MAX(salary) OVER (PARTITION BY department_id) AS max_sal,
ROUND(
(salary - MIN(salary) OVER (PARTITION BY department_id)) * 1.0 /
NULLIF(MAX(salary) OVER (PARTITION BY department_id) - MIN(salary) OVER (PARTITION BY department_id), 0),
2
) AS normalized
FROM employees
ORDER BY department_id, salary;+------------+---------------+--------+---------+---------+------------+
| first_name | department_id | salary | min_sal | max_sal | normalized |
+------------+---------------+--------+---------+---------+------------+
| Ali | 1 | 8000 | 8000 | 12000 | 0.00 |
| Zeynep | 1 | 9000 | 8000 | 12000 | 0.25 |
| Fatma | 1 | 12000 | 8000 | 12000 | 1.00 |
+------------+---------------+--------+---------+---------+------------+Birden Fazla Aggregate Fonksiyonu Bir Arada
Gerçek dünya raporlarında genellikle birden fazla metrik aynı sorguda hesaplanır:
-- Kapsamlı aylık rapor
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
COUNT(*) AS order_count,
SUM(total_amount) AS revenue,
-- Kümülatif metrikler
SUM(COUNT(*)) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) AS cumulative_orders,
SUM(SUM(total_amount)) OVER (ORDER BY DATE_FORMAT(order_date, '%Y-%m')) AS cumulative_revenue,
-- Hareketli ortalamalar
ROUND(AVG(SUM(total_amount)) OVER (
ORDER BY DATE_FORMAT(order_date, '%Y-%m')
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
), 0) AS ma_3m_revenue,
-- Yüzdelik pay
ROUND(
SUM(total_amount) * 100.0 / SUM(SUM(total_amount)) OVER (), 1
) AS pct_of_year
FROM orders
WHERE YEAR(order_date) = 2024
GROUP BY DATE_FORMAT(order_date, '%Y-%m')
ORDER BY month;Bu tek sorguda 7 metrik hesaplanıyor — aylık sipariş sayısı, gelir, kümülatif sipariş, kümülatif gelir, 3 aylık hareketli ortalama ve yıllık pay. Hepsi aynı sorguda, window function'lar sayesinde.
DISTINCT ile Window Function
-- Her siparişteki ürün çeşidi ve tüm siparişlerdeki toplam ürün çeşidi
SELECT DISTINCT
o.order_id,
COUNT(DISTINCT oi.product_id) OVER (PARTITION BY o.order_id) AS products_in_order,
COUNT(DISTINCT oi.product_id) OVER () AS total_products_sold
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id;⚠️ Dikkat:
COUNT(DISTINCT ...)window function olarak MySQL 8.0'da desteklenmez! Bu durumda subquery veya CTE kullanman gerekir:
-- MySQL'de alternatif
SELECT
o.order_id,
(SELECT COUNT(DISTINCT product_id) FROM order_items WHERE order_id = o.order_id) AS products_in_order
FROM orders o;PostgreSQL Farklılıkları
-- PostgreSQL: FILTER clause ile koşullu aggregate
SELECT
DATE_FORMAT(order_date, '%Y-%m') AS month,
SUM(total_amount) AS total_revenue,
-- PostgreSQL'de:
SUM(total_amount) FILTER (WHERE status = 'completed')
OVER (ORDER BY order_date) AS completed_running_total
-- MySQL'de:
-- SUM(CASE WHEN status = 'completed' THEN total_amount ELSE 0 END)
-- OVER (ORDER BY order_date) AS completed_running_total
FROM orders;
-- PostgreSQL: COUNT(DISTINCT) window function olarak desteklenir
SELECT COUNT(DISTINCT customer_id) OVER (ORDER BY order_date) AS cumulative_unique_customers
FROM orders;
-- MySQL'de bu doğrudan yapılamazÖzet
SUM() OVER (ORDER BY ...) kümülatif toplam (running total) hesaplar — banka bakiyesi gibi
AVG() OVER (ORDER BY ...) kümülatif ortalama hesaplar
ROWS BETWEEN N PRECEDING AND CURRENT ROW ile hareketli ortalama (moving average) yapılır
PARTITION BY olmadan
SUM() OVER ()tüm tablo toplamını her satırda gösterir — yüzdelik pay hesabı içinGROUP BY ile window function birlikte kullanılabilir —
SUM(SUM(x)) OVER (...)kalıbıPareto analizi, trend analizi, normalleştirme gibi ileri raporlama teknikleri window function'larla çözülür
ORDER BY eklenip eklenmemesi davranışı tamamen değiştirir: ORDER BY varsa kümülatif, yoksa tüm partition
Sıkça Yapılan Hatalar
ORDER BY eklemeyi unutmak —
SUM() OVER (PARTITION BY dept)toplam verir,SUM() OVER (PARTITION BY dept ORDER BY date)kümülatif verir. İkisi çok farklı sonuçlar üretir.SUM(SUM()) kalıbını anlamamak — GROUP BY ile window function birlikte kullanıldığında, iç fonksiyon GROUP BY'ın aggregate'i, dış fonksiyon window function'dır. İç içe yazılması kafa karıştırıcı ama mantıksal olarak iki ayrı adım.
Moving average'da ilk satırları göz ardı etmek —
ROWS BETWEEN 2 PRECEDING AND CURRENT ROWilk 2 satırda yeterli veri olmadığı için mevcut satırlarla hesaplar. Sonuçlar ilk birkaç satırda yanıltıcı olabilir.COUNT(DISTINCT) OVER kullanmaya çalışmak — MySQL 8.0'da desteklenmez. Subquery veya CTE ile çöz.
Yüzde hesabında sıfıra bölme —
SUM() OVER ()sonucu sıfır olabilir (boş sonuç kümesi). NULLIF ile korunmayı unutma.
AI Asistan
Sorularını yanıtlamaya hazır