PARTITION BY ve ORDER BY Kombinasyonları
Giriş — İki Güçlü Araç, Sonsuz Kombinasyon
Şimdiye kadar PARTITION BY ve ORDER BY'ı ayrı ayrı gördün. PARTITION BY pencereyi gruplara böler, ORDER BY pencere içinde sıralama yapar. Ama asıl güç, bu ikisinin birlikte kullanılmasından gelir.
Bu ders, PARTITION BY ve ORDER BY kombinasyonlarının farklı senaryolarda nasıl davrandığını, hangi kombinasyonun hangi iş problemini çözdüğünü ve sık düşülen hataları derinlemesine ele alıyor. Daha önce öğrendiğin tüm window function'ları bu kombinasyonlarla pekiştireceksin.
🎯 Analoji: PARTITION BY ve ORDER BY'ı bir sınıf listesi gibi düşün. PARTITION BY "sınıflara ayır" — A şubesi, B şubesi, C şubesi. ORDER BY "her sınıf içinde not sırasına göre diz." İkisi birlikte: "her şubede, notlara göre sıralama yap." Birini kullanmadan diğeri farklı anlam ifade eder.
Dört Temel Kombinasyon
Window function'larda OVER() içinde dört farklı kombinasyon mümkün. Her birinin davranışı tamamen farklı:
1. OVER () — Boş Pencere
-- Tüm tablo tek pencere
SELECT
first_name, department_id, salary,
SUM(salary) OVER () AS total_payroll,
ROUND(salary * 100.0 / SUM(salary) OVER (), 1) AS salary_pct
FROM employees;+------------+---------------+--------+---------------+------------+
| first_name | department_id | salary | total_payroll | salary_pct |
+------------+---------------+--------+---------------+------------+
| Ali | 1 | 8000 | 147000 | 5.4 |
| Zeynep | 1 | 9000 | 147000 | 6.1 |
| Can | 3 | 15000 | 147000 | 10.2 |
+------------+---------------+--------+---------------+------------+Her satırda aynı toplam. Yüzdelik pay, normalleştirme gibi "her satırı toplama oranla" hesaplamalarda kullanılır.
2. OVER (PARTITION BY x) — Sadece Bölümleme
-- Her departman kendi penceresi
SELECT
first_name, department_id, salary,
SUM(salary) OVER (PARTITION BY department_id) AS dept_total,
COUNT(*) OVER (PARTITION BY department_id) AS dept_count,
ROUND(AVG(salary) OVER (PARTITION BY department_id), 0) AS dept_avg
FROM employees;+------------+---------------+--------+------------+------------+----------+
| first_name | department_id | salary | dept_total | dept_count | dept_avg |
+------------+---------------+--------+------------+------------+----------+
| Ali | 1 | 8000 | 29000 | 3 | 9667 |
| Zeynep | 1 | 9000 | 29000 | 3 | 9667 |
| Fatma | 1 | 12000 | 29000 | 3 | 9667 |
| Ayşe | 2 | 10000 | 22000 | 2 | 11000 |
| Mehmet | 2 | 12000 | 22000 | 2 | 11000 |
+------------+---------------+--------+------------+------------+----------+Her partition içinde aynı değer. ORDER BY yok — kümülatif hesaplama yok, tüm partition'ın toplamı/ortalaması.
3. OVER (ORDER BY x) — Sadece Sıralama
-- Tüm tablo üzerinde kümülatif hesaplama
SELECT
first_name, salary,
SUM(salary) OVER (ORDER BY salary) AS running_total,
COUNT(*) OVER (ORDER BY salary) AS running_count,
ROUND(AVG(salary) OVER (ORDER BY salary), 0) AS running_avg
FROM employees;+------------+--------+---------------+---------------+-------------+
| first_name | salary | running_total | running_count | running_avg |
+------------+--------+---------------+---------------+-------------+
| Ali | 8000 | 8000 | 1 | 8000 |
| Zeynep | 9000 | 17000 | 2 | 8500 |
| Ayşe | 10000 | 27000 | 3 | 9000 |
| Mehmet | 12000 | 51000 | 5 | 10200 |
| Fatma | 12000 | 51000 | 5 | 10200 |
| Deniz | 13000 | 64000 | 6 | 10667 |
+------------+--------+---------------+---------------+-------------+PARTITION BY yok — tüm tablo tek pencere. ORDER BY var — kümülatif hesaplama. Dikkat: Mehmet ve Fatma aynı maaşta, ikisinin running_total'ı da aynı (51000) — çünkü varsayılan frame RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, eşit değerleri birlikte işler.
4. OVER (PARTITION BY x ORDER BY y) — Her İkisi
-- Her departmanda kümülatif maaş toplamı
SELECT
first_name, department_id, salary,
SUM(salary) OVER (
PARTITION BY department_id
ORDER BY salary
) AS dept_running_total,
ROW_NUMBER() OVER (
PARTITION BY department_id
ORDER BY salary DESC
) AS dept_rank
FROM employees;+------------+---------------+--------+--------------------+-----------+
| first_name | department_id | salary | dept_running_total | dept_rank |
+------------+---------------+--------+--------------------+-----------+
| Ali | 1 | 8000 | 8000 | 3 |
| Zeynep | 1 | 9000 | 17000 | 2 |
| Fatma | 1 | 12000 | 29000 | 1 |
| Ayşe | 2 | 10000 | 10000 | 2 | ← Reset!
| Mehmet | 2 | 12000 | 22000 | 1 |
+------------+---------------+--------+--------------------+-----------+PARTITION BY ile gruplama, ORDER BY ile her grup içinde kümülatif hesaplama. En sık kullanılan ve en güçlü kombinasyon.
Kombinasyon Özet Tablosu
| Kombinasyon | Aggregate Davranışı | Ranking Davranışı | Kullanım Alanı |
|---|---|---|---|
OVER () | Tüm tablo toplamı (her satırda aynı) | Anlamsız | Yüzdelik pay |
OVER (PARTITION BY x) | Grup toplamı (her satırda aynı) | Anlamsız | Grup bazlı karşılaştırma |
OVER (ORDER BY y) | Kümülatif toplam (tüm tablo) | Genel sıralama | Running total, genel ranking |
OVER (PARTITION BY x ORDER BY y) | Kümülatif toplam (grup içi) | Grup içi sıralama | Grup bazlı running total, top-N |
Aynı Sorguda Farklı Pencereler
En güçlü kullanım: aynı sorguda birden fazla farklı pencere tanımı:
SELECT
first_name,
department_id,
salary,
-- 1. Şirket geneli
ROUND(AVG(salary) OVER (), 0) AS company_avg,
-- 2. Departman toplamı
SUM(salary) OVER (PARTITION BY department_id) AS dept_total,
-- 3. Departman içi sıralama
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS dept_rank,
-- 4. Genel sıralama
RANK() OVER (ORDER BY salary DESC) AS overall_rank,
-- 5. Departman ortalamasından fark
salary - ROUND(AVG(salary) OVER (PARTITION BY department_id), 0) AS diff_from_dept_avg
FROM employees
ORDER BY department_id, salary DESC;Bu tek sorguda 5 farklı perspektif var — şirket ortalaması, departman toplamı, departman içi sıralama, genel sıralama ve departman ortalamasından sapma. GROUP BY ile bu analizi yapmak için en az 3-4 ayrı sorgu ve JOIN gerekir.
Çoklu PARTITION BY Sütunları
PARTITION BY'da birden fazla sütun kullanabilirsin:
-- Yıl ve ay bazında, her kategorinin aylık satış sıralaması
SELECT
YEAR(o.order_date) AS year,
MONTH(o.order_date) AS month,
c.category_name,
SUM(oi.quantity * oi.unit_price) AS monthly_revenue,
RANK() OVER (
PARTITION BY YEAR(o.order_date), MONTH(o.order_date)
ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS monthly_rank
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories c ON p.category_id = c.category_id
GROUP BY YEAR(o.order_date), MONTH(o.order_date), c.category_id, c.category_name
ORDER BY year, month, monthly_rank;PARTITION BY YEAR(o.order_date), MONTH(o.order_date) — her yıl-ay kombinasyonu ayrı bir partition. Ocak 2024'ün sıralaması, Şubat 2024'ten bağımsız.
Departman ve Cinsiyet Bazlı Analiz
-- Her departmanda, cinsiyete göre maaş sıralaması
SELECT
first_name,
department_id,
gender,
salary,
ROW_NUMBER() OVER (
PARTITION BY department_id, gender
ORDER BY salary DESC
) AS rank_in_group,
AVG(salary) OVER (PARTITION BY department_id, gender) AS group_avg
FROM employees
ORDER BY department_id, gender, salary DESC;ORDER BY ile Farklı Sıralama Yönleri
-- Aynı partition'da farklı sıralama yönleri
SELECT
first_name,
department_id,
salary,
hire_date,
-- Maaşa göre büyükten küçüğe sıralama
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank,
-- İşe giriş tarihine göre eskiden yeniye sıralama
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY hire_date ASC) AS seniority_rank,
-- İşe giriş tarihine göre yeniden eskiye sıralama
ROW_NUMBER() OVER (PARTITION BY department_id ORDER BY hire_date DESC) AS newest_rank
FROM employees;Her ranking farklı perspektif — aynı çalışan maaş sıralamasında 1. ama kıdem sıralamasında 5. olabilir.
Gerçek Dünya Örnekleri
Örnek 1: Çalışan Performans Kartı
-- Her çalışan için kapsamlı performans kartı
SELECT
e.first_name,
e.last_name,
d.department_name,
e.salary,
-- Departman içi pozisyon
ROW_NUMBER() OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) AS dept_salary_rank,
COUNT(*) OVER (PARTITION BY e.department_id) AS dept_size,
-- Departman ortalamasıyla karşılaştırma
ROUND(AVG(e.salary) OVER (PARTITION BY e.department_id), 0) AS dept_avg,
ROUND((e.salary - AVG(e.salary) OVER (PARTITION BY e.department_id)) * 100.0
/ AVG(e.salary) OVER (PARTITION BY e.department_id), 1) AS pct_above_dept_avg,
-- Şirket genelindeki pozisyon
RANK() OVER (ORDER BY e.salary DESC) AS company_rank,
COUNT(*) OVER () AS total_employees,
-- Maaş bandı
CASE
WHEN e.salary >= FIRST_VALUE(e.salary) OVER (PARTITION BY e.department_id ORDER BY e.salary DESC) * 0.9
THEN 'Üst Band'
WHEN e.salary >= AVG(e.salary) OVER (PARTITION BY e.department_id) * 0.9
THEN 'Orta Band'
ELSE 'Alt Band'
END AS salary_band
FROM employees e
JOIN departments d ON e.department_id = d.department_id
ORDER BY d.department_name, e.salary DESC;Örnek 2: Sipariş Trend Analizi
-- Her müşterinin sipariş trendi
SELECT
c.first_name,
o.order_date,
o.total_amount,
-- Müşteri bazlı kümülatif
SUM(o.total_amount) OVER w_cust AS customer_lifetime_value,
COUNT(*) OVER w_cust AS order_sequence,
-- Bir önceki siparişle karşılaştırma
LAG(o.total_amount) OVER w_cust_date AS prev_order_amount,
-- Müşterinin ortalama sipariş tutarı
ROUND(AVG(o.total_amount) OVER (PARTITION BY o.customer_id), 0) AS avg_order,
-- Bu sipariş ortalamanın üstünde mi?
CASE
WHEN o.total_amount > AVG(o.total_amount) OVER (PARTITION BY o.customer_id)
THEN 'Ortalamanın Üstünde'
ELSE 'Ortalamanın Altında'
END AS vs_avg
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WINDOW
w_cust AS (PARTITION BY o.customer_id),
w_cust_date AS (PARTITION BY o.customer_id ORDER BY o.order_date)
ORDER BY c.first_name, o.order_date;WINDOW clause ile iki farklı pencere tanımladık: w_cust (sadece partition), w_cust_date (partition + sıralama). WINDOW clause tekrarı azaltır ve kodu okunabilir kılar.
Örnek 3: Kategori Bazlı Haftalık Satış Raporu
-- Haftalık satışlar, kategoriye göre partitioned, genel ranking
SELECT
YEARWEEK(o.order_date, 1) AS year_week,
c.category_name,
SUM(oi.quantity) AS units_sold,
SUM(oi.quantity * oi.unit_price) AS revenue,
-- Bu haftanın genel sıralaması
RANK() OVER (
PARTITION BY YEARWEEK(o.order_date, 1)
ORDER BY SUM(oi.quantity * oi.unit_price) DESC
) AS weekly_rank,
-- Kategorinin kümülatif geliri
SUM(SUM(oi.quantity * oi.unit_price)) OVER (
PARTITION BY c.category_id
ORDER BY YEARWEEK(o.order_date, 1)
) AS category_cumulative_revenue,
-- Kategorinin haftalık gelir trendi (önceki haftayla fark)
SUM(oi.quantity * oi.unit_price) - LAG(SUM(oi.quantity * oi.unit_price)) OVER (
PARTITION BY c.category_id
ORDER BY YEARWEEK(o.order_date, 1)
) AS revenue_change
FROM orders o
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
JOIN categories c ON p.category_id = c.category_id
GROUP BY YEARWEEK(o.order_date, 1), c.category_id, c.category_name
ORDER BY year_week DESC, weekly_rank;Bu sorguda 3 farklı window function, 3 farklı partition/order kombinasyonuyla çalışıyor:
PARTITION BY hafta ORDER BY gelir— haftalık sıralamaPARTITION BY kategori ORDER BY hafta— kategorinin kümülatif geliriPARTITION BY kategori ORDER BY hafta+ LAG — haftalık değişim
WINDOW Clause ile Named Windows
Aynı pencereyi paylaşan ama küçük farklar olan durumlar:
-- Base window tanımla, üzerine ekle
SELECT
first_name, department_id, salary,
SUM(salary) OVER w AS dept_total,
AVG(salary) OVER w AS dept_avg,
ROW_NUMBER() OVER (w ORDER BY salary DESC) AS salary_rank -- ← ORDER BY eklendi
FROM employees
WINDOW w AS (PARTITION BY department_id);⚠️ Dikkat: Named window'a ORDER BY eklenebilir ama base window'da zaten ORDER BY varsa üzerine yazılamaz. Base window'u mümkün olduğunca basit tut.
Performans İpuçları
-- ✅ Aynı PARTITION BY + ORDER BY kullanan fonksiyonlar tek seferde hesaplanır
SELECT
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk,
DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS drnk
FROM employees;
-- MySQL optimizer bunları tek partition/sort operasyonuyla çözer
-- ❌ Farklı ORDER BY kullanan fonksiyonlar ayrı sıralama gerektirir
SELECT
ROW_NUMBER() OVER (ORDER BY salary DESC) AS salary_rank,
ROW_NUMBER() OVER (ORDER BY hire_date ASC) AS seniority_rank
FROM employees;
-- İki ayrı sıralama yapılır — daha yavaş
-- ✅ Index desteği ekle
CREATE INDEX idx_emp_dept_salary ON employees(department_id, salary DESC);
-- PARTITION BY department_id ORDER BY salary DESC için idealÖzet
OVER (): Tüm tablo üzerinde hesaplama — yüzdelik pay, genel toplam
OVER (PARTITION BY x): Grup bazlı hesaplama — grup ortalaması, grup toplamı
OVER (ORDER BY y): Kümülatif hesaplama — running total, genel ranking
OVER (PARTITION BY x ORDER BY y): Grup içi kümülatif — en güçlü ve en yaygın kombinasyon
Aynı sorguda farklı pencereler kullanabilirsin — 5+ farklı perspektif tek sorguda
WINDOW clause tekrar eden pencere tanımlarını azaltır
PARTITION BY'da birden fazla sütun kullanabilirsin — daha ince gruplamalır
Performans için aynı partition/order kombinasyonunu paylaşan fonksiyonlar gruplanmalı
Sıkça Yapılan Hatalar
ORDER BY'ın aggregate davranışını değiştirdiğini unutmak —
SUM() OVER (PARTITION BY dept)toplam verir,SUM() OVER (PARTITION BY dept ORDER BY date)kümülatif verir. Bu fark kritik.Çok fazla farklı pencere tanımı kullanmak — Her farklı PARTITION BY + ORDER BY kombinasyonu ayrı bir sort operasyonu gerektirir. 5-6 farklı pencere, performansı ciddi etkileyebilir.
WINDOW clause'u bilmemek — Aynı pencere tanımını 5 kez yazmak hem okunabilirliği bozar hem hata yapma riskini artırır. Named window kullan.
GROUP BY + window function etkileşimini anlamamak — Window function, GROUP BY'dan sonra çalışır. GROUP BY sonucundaki satırlar üzerinde pencere hesaplaması yapılır — orijinal satırlar değil.
NULL değerlerin sıralamadaki etkisini unutmak — MySQL'de NULL değerler ORDER BY ASC'de en önce, DESC'de en sonda gelir. PostgreSQL'de NULLS FIRST/LAST ile kontrol edilebilir.
AI Asistan
Sorularını yanıtlamaya hazır