← Kursa Dön
📄 Text · 30 min

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

KombinasyonAggregate DavranışıRanking DavranışıKullanım Alanı
OVER ()Tüm tablo toplamı (her satırda aynı)AnlamsızYüzdelik pay
OVER (PARTITION BY x)Grup toplamı (her satırda aynı)AnlamsızGrup bazlı karşılaştırma
OVER (ORDER BY y)Kümülatif toplam (tüm tablo)Genel sıralamaRunning total, genel ranking
OVER (PARTITION BY x ORDER BY y)Kümülatif toplam (grup içi)Grup içi sıralamaGrup 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:

  1. PARTITION BY hafta ORDER BY gelir — haftalık sıralama

  2. PARTITION BY kategori ORDER BY hafta — kategorinin kümülatif geliri

  3. PARTITION 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

  1. ORDER BY'ın aggregate davranışını değiştirdiğini unutmakSUM() OVER (PARTITION BY dept) toplam verir, SUM() OVER (PARTITION BY dept ORDER BY date) kümülatif verir. Bu fark kritik.

  2. Ç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.

  3. 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.

  4. 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.

  5. 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.