← Kursa Dön
📄 Text · 35 min

Window Function Nedir? OVER() Temelleri

Giriş — GROUP BY'ın Çözemediği Problem

Şimdiye kadar aggregate fonksiyonları (SUM, COUNT, AVG) öğrendin ve GROUP BY ile gruplar oluşturup bu grupların özetini çıkardın. Ama bir sorun var: GROUP BY kullandığında, her grup tek bir satıra indirgenir — bireysel satır detaylarını kaybedersin.

Şöyle bir senaryo düşün: e-ticaret sisteminde her çalışanın maaşını ve departman ortalamasını yan yana görmek istiyorsun. GROUP BY ile departman ortalamasını hesaplarsın ama artık bireysel çalışan satırlarını göremezsin. Ya da her siparişin tutarını ve o müşterinin toplam harcamasını aynı satırda görmek istiyorsun — yine aynı problem.

-- ❌ GROUP BY ile: bireysel satırlar kaybolur
SELECT department_id, AVG(salary) AS avg_salary
FROM employees
GROUP BY department_id;
-- Sonuç: 5 departman, 5 satır — bireysel çalışanlar yok

-- Peki hem çalışan bilgisini hem departman ortalamasını istiyorsak?
-- Subquery ile çözülebilir ama çirkin ve yavaş:
SELECT e.first_name, e.salary, 
    (SELECT AVG(salary) FROM employees e2 WHERE e2.department_id = e.department_id) AS dept_avg
FROM employees e;
-- Her satır için subquery çalışır — N+1 problemi!

İşte window function'lar tam olarak bu sorunu çözmek için var. Satırları gruplamadan, her satırda bir hesaplama yapmanı sağlar.

-- ✅ Window function ile: hem bireysel satır hem grup hesaplaması
SELECT first_name, department_id, salary,
       AVG(salary) OVER (PARTITION BY department_id) AS dept_avg
FROM employees;
+------------+---------------+--------+----------+
| first_name | department_id | salary | dept_avg |
+------------+---------------+--------+----------+
| Ali        |             1 |   8000 |  8500.00 |
| Zeynep     |             1 |   9000 |  8500.00 |
| Mehmet     |             2 |  12000 | 11000.00 |
| Ayşe       |             2 |  10000 | 11000.00 |
+------------+---------------+--------+----------+

Her satır bireysel olarak orada duruyor ve departman ortalaması da yanında. Satır sayısı azalmadı, gruplanma yok — sadece ek bir hesaplama sütunu eklendi.

🎯 Analoji: Window function'ı bir tren yolculuğu gibi düşün. Tren (sorgunun satırları) ilerliyor, her istasyonda (her satırda) duruyor. Sen pencereden (window) dışarı bakıyorsun ve çevrendeki birkaç istasyonu (ilişkili satırları) görüyorsun. Her istasyonda manzara farklı ama trenin rotası değişmiyor — satırlar yerinde kalıyor, sadece pencereden gördüklerin her satırda farklı.


OVER() — Her Şeyin Başladığı Yer

Window function'ın temel syntax'ı şudur:

fonksiyon() OVER (pencere_tanımı)

OVER() kelimesi, "bu fonksiyonu bir pencere (window) üzerinde çalıştır" demektir. OVER() içi boş bırakılırsa, tüm tablo pencere olarak kullanılır.

-- OVER() boş: tüm tablo üzerinde hesaplama
SELECT 
    first_name, 
    salary,
    SUM(salary) OVER () AS total_salary,
    COUNT(*) OVER () AS total_employees,
    AVG(salary) OVER () AS avg_salary
FROM employees;
+------------+--------+--------------+-----------------+------------+
| first_name | salary | total_salary | total_employees | avg_salary |
+------------+--------+--------------+-----------------+------------+
| Ali        |   8000 |       147000 |              15 |    9800.00 |
| Zeynep     |   9000 |       147000 |              15 |    9800.00 |
| Mehmet     |  12000 |       147000 |              15 |    9800.00 |
| Ayşe       |  10000 |       147000 |              15 |    9800.00 |
| ...        |    ... |       147000 |              15 |    9800.00 |
+------------+--------+--------------+-----------------+------------+

Her satırda aynı toplam, aynı ortalama görünüyor — çünkü pencere tüm tablo. Her çalışan kendi maaşını görüyor ve yanında şirket genelinin toplamı, sayısı, ortalaması var. GROUP BY yapsaydık tek satır görürdük — burada 15 satırın hepsi duruyor.

OVER() ile GROUP BY Farkı — Kritik Ayrım

Bu farkı net anlamak çok önemli:

-- GROUP BY: Satırlar gruplanır, her grup TEK satır olur
SELECT department_id, SUM(salary) 
FROM employees 
GROUP BY department_id;
-- Sonuç: 5 satır (5 departman)

-- Window function: Satırlar yerinde kalır, hesaplama ek sütun olarak eklenir
SELECT first_name, department_id, salary, 
       SUM(salary) OVER (PARTITION BY department_id) AS dept_total
FROM employees;
-- Sonuç: 15 satır (15 çalışan) — her birinde departman toplamı da var
ÖzellikGROUP BYWindow Function
Satır sayısıAzalır (grup başına 1)Değişmez
Bireysel satır detayıKaybolurKorunur
Aggregate fonksiyonZorunluİsteğe bağlı
Birden fazla seviyeMümkün değil (tek seferde)Aynı sorguda farklı pencereler

PARTITION BY — Pencereyi Bölümlere Ayır

PARTITION BY, GROUP BY'ın window function versiyonu gibidir — satırları gruplara (bölümlere/partition) ayırır ama satırları indirgemez.

SELECT 
    first_name, 
    department_id, 
    salary,
    AVG(salary) OVER (PARTITION BY department_id) AS dept_avg,
    SUM(salary) OVER (PARTITION BY department_id) AS dept_total,
    COUNT(*) OVER (PARTITION BY department_id) AS dept_count
FROM employees;
+------------+---------------+--------+----------+------------+------------+
| first_name | department_id | salary | dept_avg | dept_total | dept_count |
+------------+---------------+--------+----------+------------+------------+
| Ali        |             1 |   8000 |  8500.00 |      17000 |          2 |
| Zeynep     |             1 |   9000 |  8500.00 |      17000 |          2 |
| Mehmet     |             2 |  12000 | 11000.00 |      22000 |          2 |
| Ayşe       |             2 |  10000 | 11000.00 |      22000 |          2 |
| Can        |             3 |  15000 | 14000.00 |      42000 |          3 |
| Deniz      |             3 |  13000 | 14000.00 |      42000 |          3 |
| Ece        |             3 |  14000 | 14000.00 |      42000 |          3 |
+------------+---------------+--------+----------+------------+------------+

Department 1'deki çalışanlar kendi departmanlarının ortalamasını, department 2'dekiler kendi departmanlarının ortalamasını görüyor. Her çalışan bireysel olarak orada ama departman bazlı hesaplamalar da yanlarında.

Birden Fazla PARTITION BY

Aynı sorguda farklı pencereler tanımlayabilirsin:

SELECT 
    first_name,
    department_id,
    salary,
    AVG(salary) OVER () AS company_avg,                          -- Şirket ortalaması
    AVG(salary) OVER (PARTITION BY department_id) AS dept_avg,   -- Departman ortalaması
    salary - AVG(salary) OVER (PARTITION BY department_id) AS diff_from_dept  -- Fark
FROM employees;

Bu, GROUP BY ile imkansız olan bir şey: aynı sorguda hem şirket geneli hem departman bazlı hesaplama, hem de bireysel satır detayı.


ORDER BY — Pencere İçinde Sıralama

OVER() içinde ORDER BY kullanmak, window function'ın davranışını önemli ölçüde değiştirir. Özellikle sıralama fonksiyonları (ROW_NUMBER, RANK) ve kümülatif hesaplamalar (running total) için kritiktir.

-- ORDER BY olmadan: tüm partition'daki toplam
SELECT 
    first_name, salary,
    SUM(salary) OVER (PARTITION BY department_id) AS dept_total
FROM employees;
-- Her satırda aynı toplam

-- ORDER BY ile: kümülatif toplam (running total)
SELECT 
    first_name, salary,
    SUM(salary) OVER (PARTITION BY department_id ORDER BY salary) AS running_total
FROM employees;
+------------+--------+---------------+
| first_name | salary | running_total |
+------------+--------+---------------+
| Ali        |   8000 |          8000 |  ← sadece Ali
| Zeynep     |   9000 |         17000 |  ← Ali + Zeynep
| Ayşe       |  10000 |         10000 |  ← yeni partition (dept 2)
| Mehmet     |  12000 |         22000 |  ← Ayşe + Mehmet
+------------+--------+---------------+

ORDER BY eklendiğinde, pencere "o satıra kadar olan satırlar" olarak daraltılır. Bu davranışa frame (çerçeve) denir ve ileriki derslerde detaylı göreceğiz. Şimdilik bilmen gereken: ORDER BY eklenince kümülatif hesaplama yapılır.


Window Function Türleri

Window function'lar üç ana kategoriye ayrılır:

1. Sıralama Fonksiyonları (Ranking Functions)

ROW_NUMBER()  -- Her satıra benzersiz sıra numarası
RANK()        -- Sıralama (eşit değerlerde aynı sıra, boşluk bırakır)
DENSE_RANK()  -- Sıralama (eşit değerlerde aynı sıra, boşluk bırakmaz)
NTILE(n)      -- Satırları n eşit gruba böler

2. Değer Fonksiyonları (Value Functions)

LAG(col, n)        -- n satır önceki değer
LEAD(col, n)       -- n satır sonraki değer
FIRST_VALUE(col)   -- Penceredeki ilk değer
LAST_VALUE(col)    -- Penceredeki son değer
NTH_VALUE(col, n)  -- Penceredeki n. değer

3. Aggregate Fonksiyonlar (OVER ile)

SUM(col) OVER (...)    -- Pencere içinde toplam
AVG(col) OVER (...)    -- Pencere içinde ortalama
COUNT(col) OVER (...)  -- Pencere içinde sayım
MIN(col) OVER (...)    -- Pencere içinde minimum
MAX(col) OVER (...)    -- Pencere içinde maximum

Bu fonksiyonların her birini ilerleyen derslerde detaylı öğreneceğiz.


Gerçek Dünya Örnekleri

Örnek 1: Her Ürünün Fiyatı ve Kategori Ortalaması

SELECT 
    p.product_name,
    c.category_name,
    p.price,
    ROUND(AVG(p.price) OVER (PARTITION BY p.category_id), 2) AS category_avg,
    ROUND(p.price - AVG(p.price) OVER (PARTITION BY p.category_id), 2) AS diff
FROM products p
JOIN categories c ON p.category_id = c.category_id
ORDER BY c.category_name, p.price DESC;
+-------------------+---------------+--------+--------------+--------+
| product_name      | category_name | price  | category_avg | diff   |
+-------------------+---------------+--------+--------------+--------+
| MacBook Pro       | Elektronik    | 45000  |    22500.00  | 22500  |
| iPhone 15         | Elektronik    | 55000  |    22500.00  | 32500  |
| USB Kablo         | Elektronik    |   150  |    22500.00  |-22350  |
| SQL Kitabı        | Kitap         |   120  |      165.00  |   -45  |
| Python Kitabı     | Kitap         |   210  |      165.00  |    45  |
+-------------------+---------------+--------+--------------+--------+

Bu sorgu ile her ürünün kendi kategorisindeki ortalamadan ne kadar saptığını görüyorsun. Fiyat stratejisi belirlemek için çok kullanışlı.

Örnek 2: Her Siparişin Müşterinin Toplam Harcamasına Oranı

SELECT 
    o.order_id,
    c.first_name,
    o.total_amount,
    SUM(o.total_amount) OVER (PARTITION BY o.customer_id) AS customer_total,
    ROUND(
        o.total_amount * 100.0 / SUM(o.total_amount) OVER (PARTITION BY o.customer_id), 
        1
    ) AS pct_of_total
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
ORDER BY c.first_name, o.order_date;
+----------+------------+--------------+----------------+--------------+
| order_id | first_name | total_amount | customer_total | pct_of_total |
+----------+------------+--------------+----------------+--------------+
|     1001 | Ali        |         1500 |           5200 |         28.8 |
|     1045 | Ali        |         2200 |           5200 |         42.3 |
|     1089 | Ali        |         1500 |           5200 |         28.8 |
|     1002 | Zeynep     |         3400 |           8100 |         42.0 |
|     1056 | Zeynep     |         4700 |           8100 |         58.0 |
+----------+------------+--------------+----------------+--------------+

Örnek 3: Tüm Tablo Üzerinde Yüzdelik

SELECT 
    first_name,
    salary,
    SUM(salary) OVER () AS total_payroll,
    ROUND(salary * 100.0 / SUM(salary) OVER (), 2) AS salary_pct
FROM employees
ORDER BY salary DESC;
+------------+--------+---------------+------------+
| first_name | salary | total_payroll | salary_pct |
+------------+--------+---------------+------------+
| Can        |  15000 |        147000 |      10.20 |
| Ece        |  14000 |        147000 |       9.52 |
| Deniz      |  13000 |        147000 |       8.84 |
| Mehmet     |  12000 |        147000 |       8.16 |
| ...        |    ... |        147000 |        ... |
+------------+--------+---------------+------------+

Her çalışanın toplam maaş bütçesinin yüzde kaçını aldığını görüyorsun. Bu, GROUP BY ile yapılamaz — hem bireysel maaş hem toplam maaş aynı satırda olmalı.


WINDOW Clause — Pencere Tanımını Yeniden Kullanma

Aynı pencere tanımını birden fazla fonksiyonda kullanıyorsan, WINDOW clause ile bir kez tanımlayabilirsin:

-- ❌ Tekrarlı pencere tanımı
SELECT 
    first_name, department_id, salary,
    AVG(salary) OVER (PARTITION BY department_id) AS dept_avg,
    MIN(salary) OVER (PARTITION BY department_id) AS dept_min,
    MAX(salary) OVER (PARTITION BY department_id) AS dept_max,
    COUNT(*) OVER (PARTITION BY department_id) AS dept_count
FROM employees;

-- ✅ WINDOW clause ile temiz yazım
SELECT 
    first_name, department_id, salary,
    AVG(salary) OVER w AS dept_avg,
    MIN(salary) OVER w AS dept_min,
    MAX(salary) OVER w AS dept_max,
    COUNT(*) OVER w AS dept_count
FROM employees
WINDOW w AS (PARTITION BY department_id);

Bu sadece okunabilirlik için değil — optimizer da aynı pencere tanımını paylaşan fonksiyonları optimize edebilir.

💡 İpucu: WINDOW clause MySQL 8.0+ ve PostgreSQL'de desteklenir. Daha eski MySQL sürümlerinde kullanılamaz.


Window Function'ların Çalışma Sırası

SQL sorgusunun işlenme sırası window function'ları etkiler:

1. FROM / JOIN     → Tablolar birleştirilir
2. WHERE           → Satırlar filtrelenir
3. GROUP BY        → Gruplar oluşturulur
4. HAVING          → Gruplar filtrelenir
5. Window Function → OVER() hesaplamaları yapılır  ← BURADA
6. SELECT          → Sütunlar seçilir
7. DISTINCT        → Tekrar eden satırlar çıkarılır
8. ORDER BY        → Sonuçlar sıralanır
9. LIMIT           → Satır sayısı sınırlanır

Bu sıranın önemli sonuçları var:

-- ✅ WHERE'de window function KULLANILMAZ (WHERE daha önce çalışır)
-- ❌ Bu hata verir:
SELECT first_name, salary, 
       ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
FROM employees
WHERE rn <= 5;  -- HATA! rn henüz hesaplanmadı

-- ✅ Subquery veya CTE ile çöz:
SELECT * FROM (
    SELECT first_name, salary, 
           ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
    FROM employees
) ranked
WHERE rn <= 5;

⚠️ Dikkat: Window function'ları WHERE veya HAVING'de doğrudan kullanamazsın. Önce subquery/CTE'de hesapla, dış sorguda filtrele.


PostgreSQL Farklılıkları

Window function'lar SQL standardının bir parçasıdır ve MySQL 8.0+ ile PostgreSQL'de neredeyse aynı syntax'la çalışır. Ancak birkaç fark var:

-- PostgreSQL: FILTER clause destekler (MySQL'de yok)
SELECT 
    department_id,
    COUNT(*) OVER (PARTITION BY department_id) AS total,
    COUNT(*) FILTER (WHERE salary > 10000) 
        OVER (PARTITION BY department_id) AS high_salary_count
FROM employees;
-- MySQL'de FILTER yok — CASE WHEN ile çözersin:
SELECT 
    department_id,
    COUNT(*) OVER (PARTITION BY department_id) AS total,
    SUM(CASE WHEN salary > 10000 THEN 1 ELSE 0 END) 
        OVER (PARTITION BY department_id) AS high_salary_count
FROM employees;

-- PostgreSQL: array_agg() window function olarak kullanılabilir
SELECT first_name, department_id,
       array_agg(first_name) OVER (PARTITION BY department_id) AS dept_members
FROM employees;
-- MySQL'de doğrudan window function olarak GROUP_CONCAT kullanılamaz

Özet

  • Window function, satırları gruplamadan her satırda hesaplama yapmanı sağlar — GROUP BY'dan farklı olarak satır sayısı değişmez

  • OVER() boş kullanılırsa tüm tablo üzerinde çalışır

  • PARTITION BY pencereyi gruplara böler — her grup kendi içinde hesaplanır

  • ORDER BY (OVER içinde) kümülatif hesaplama yapar (running total gibi)

  • Window function'lar WHERE'den sonra çalışır — WHERE'de kullanılamaz, subquery/CTE gerekir

  • WINDOW clause ile aynı pencere tanımını birden fazla fonksiyonda paylaşabilirsin

  • Üç kategori: sıralama (ROW_NUMBER, RANK), değer (LAG, LEAD), aggregate (SUM, AVG OVER)

Sıkça Yapılan Hatalar

  1. WHERE'de window function kullanmaya çalışmak — Window function, WHERE'den sonra çalışır. Filtreleme için subquery veya CTE kullan.

  2. GROUP BY ile karıştırmak — GROUP BY satır sayısını azaltır, window function azaltmaz. İkisi birlikte kullanılabilir ama dikkatli ol — window function, GROUP BY'dan sonra çalışır.

  3. OVER() içinde ORDER BY'ın etkisini bilmemek — ORDER BY eklendiğinde aggregate fonksiyonlar kümülatif çalışır. SUM(salary) OVER (ORDER BY salary) toplam maaşı değil, kümülatif toplamı verir.

  4. PARTITION BY'ı GROUP BY sanmak — PARTITION BY sadece pencereyi tanımlar, satırları indirgemez. İkisi farklı kavramlar.

  5. MySQL 5.7'de window function kullanmaya çalışmak — Window function'lar MySQL 8.0+ ile geldi. 5.7 kullanıyorsan subquery veya self-join ile çözmek zorundasın.