← Kursa Dön
📄 Text · 30 min

ROW_NUMBER, RANK, DENSE_RANK

Giriş — Neden Sıralama Fonksiyonlarına İhtiyacımız Var?

"Her departmanın en yüksek maaşlı çalışanını bul." Bu basit gibi görünen soru, window function öncesi dönemde SQL'de cevaplamak oldukça zordu. Subquery iç içe subquery, self-join derken sorgu bir spagetti haline gelirdi.

Ya da şöyle bir senaryo: bir e-ticaret sitesinde her kategorinin en çok satan ilk 3 ürününü listele. Veya her müşterinin son 5 siparişini getir. Bu tür "her grup için ilk N" sorguları, günlük işlerde sürekli karşına çıkar.

İşte sıralama fonksiyonları (ranking functions) tam olarak bunun için var. Her satıra bir sıra numarası atarlar ve bu numarayla filtreleme yapabilirsin.


ROW_NUMBER() — Benzersiz Sıra Numarası

ROW_NUMBER() her satıra benzersiz ve ardışık bir numara atar. Eşit değerler olsa bile her satır farklı bir numara alır.

-- Tüm çalışanlara maaşa göre sıra numarası ver
SELECT 
    first_name,
    department_id,
    salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num
FROM employees;
+------------+---------------+--------+---------+
| first_name | department_id | salary | row_num |
+------------+---------------+--------+---------+
| Can        |             3 |  15000 |       1 |
| Ece        |             3 |  14000 |       2 |
| Deniz      |             3 |  13000 |       3 |
| Mehmet     |             2 |  12000 |       4 |
| Fatma      |             1 |  12000 |       5 |  ← Aynı maaş, farklı numara!
| Ayşe       |             2 |  10000 |       6 |
+------------+---------------+--------+---------+

Dikkat: Mehmet ve Fatma aynı maaşı alıyor (12000) ama farklı row_number aldı (4 ve 5). ROW_NUMBER eşitlik durumunda rastgele sıra atar — hangi satırın 4, hangisinin 5 olacağı garanti edilmez. Bu "belirsizliği" istemiyorsan, ORDER BY'a ikinci bir sütun ekle:

-- Belirleyici (deterministic) sıralama
ROW_NUMBER() OVER (ORDER BY salary DESC, first_name ASC) AS row_num
-- Aynı maaşta isim sırasına göre numara alır

PARTITION BY ile ROW_NUMBER

En güçlü kullanımı: her grup için sıra numarası:

-- Her departmanda maaşa göre sıralama
SELECT 
    first_name,
    department_id,
    salary,
    ROW_NUMBER() OVER (
        PARTITION BY department_id 
        ORDER BY salary DESC
    ) AS dept_rank
FROM employees;
+------------+---------------+--------+-----------+
| first_name | department_id | salary | dept_rank |
+------------+---------------+--------+-----------+
| Zeynep     |             1 |   9000 |         1 |  ← Dept 1'in 1.si
| Ali        |             1 |   8000 |         2 |
| Mehmet     |             2 |  12000 |         1 |  ← Dept 2'nin 1.si (sıfırlandı!)
| Ayşe       |             2 |  10000 |         2 |
| Can        |             3 |  15000 |         1 |  ← Dept 3'ün 1.si
| Ece        |             3 |  14000 |         2 |
| Deniz      |             3 |  13000 |         3 |
+------------+---------------+--------+-----------+

Her department için numaralama sıfırlanıyor ve baştan başlıyor. Bu, "her gruptaki ilk N" problemini çözmek için mükemmel.

Pratik Kullanım: Her Departmanın En Yüksek Maaşlısı

-- Her departmanın en yüksek maaşlı çalışanı
SELECT * FROM (
    SELECT 
        first_name, 
        department_id, 
        salary,
        ROW_NUMBER() OVER (
            PARTITION BY department_id 
            ORDER BY salary DESC
        ) AS rn
    FROM employees
) ranked
WHERE rn = 1;

Window function WHERE'de kullanılamadığı için subquery gerekiyor. Bu kalıp (pattern) çok yaygın — ezberle.

💡 İpucu: Bu kalıbı "Top-N per group" (her grup için ilk N) olarak bilirsin. Veritabanı mülakat sorularının yarısı bu kalıba dayanır.


RANK() — Eşitlikte Boşluk Bırakan Sıralama

RANK() eşit değerlere aynı sıra numarası verir ama sonraki sıraya geçerken boşluk bırakır:

SELECT 
    first_name,
    salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
    RANK() OVER (ORDER BY salary DESC) AS rank_num
FROM employees;
+------------+--------+---------+----------+
| first_name | salary | row_num | rank_num |
+------------+--------+---------+----------+
| Can        |  15000 |       1 |        1 |
| Ece        |  14000 |       2 |        2 |
| Mehmet     |  12000 |       3 |        3 |
| Fatma      |  12000 |       4 |        3 |  ← Aynı maaş, aynı rank!
| Ayşe       |  10000 |       5 |        5 |  ← 4 atlandı, 5'e geçti!
| Ali        |   8000 |       6 |        6 |
+------------+--------+---------+----------+

Mehmet ve Fatma ikisi de 3. sırada. Sonraki kişi (Ayşe) 4 değil 5 numarasını alıyor — çünkü 3. sırada 2 kişi var, sıra 4 "kullanılmış" sayılıyor.

🎯 Analoji: Olimpiyatlardaki madalya sıralaması gibi düşün. İki atlet aynı sürede koşup birlikte altın madalya alırsa, gümüş madalya verilmez — sonraki atlet doğrudan bronz alır. 1, 1, 3 sıralaması olur, 2 atlanır. İşte RANK() tam olarak bunu yapar.

RANK() Ne Zaman Kullanılır?

  • Spor sıralamaları (eşit performans = eşit sıra)

  • Satış performansı raporları (eşit satış = eşit pozisyon)

  • Sınav sonuçları (eşit puan = eşit sıra)

-- E-ticarette: En çok harcayan müşteriler sıralaması
SELECT 
    c.first_name,
    c.last_name,
    SUM(o.total_amount) AS total_spent,
    RANK() OVER (ORDER BY SUM(o.total_amount) DESC) AS spending_rank
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.first_name, c.last_name
ORDER BY spending_rank;
+------------+-----------+-------------+---------------+
| first_name | last_name | total_spent | spending_rank |
+------------+-----------+-------------+---------------+
| Ali        | Yılmaz    |       45000 |             1 |
| Zeynep     | Demir     |       38000 |             2 |
| Mehmet     | Kaya      |       38000 |             2 |  ← Aynı harcama, aynı sıra
| Ayşe       | Çelik     |       25000 |             4 |  ← 3 atlandı!
+------------+-----------+-------------+---------------+

DENSE_RANK() — Eşitlikte Boşluk Bırakmayan Sıralama

DENSE_RANK() eşit değerlere aynı sıra numarası verir ama boşluk bırakmaz — sıra numaraları her zaman ardışıktır:

SELECT 
    first_name,
    salary,
    ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
    RANK() OVER (ORDER BY salary DESC) AS rank_num,
    DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank_num
FROM employees;
+------------+--------+---------+----------+----------------+
| first_name | salary | row_num | rank_num | dense_rank_num |
+------------+--------+---------+----------+----------------+
| Can        |  15000 |       1 |        1 |              1 |
| Ece        |  14000 |       2 |        2 |              2 |
| Mehmet     |  12000 |       3 |        3 |              3 |
| Fatma      |  12000 |       4 |        3 |              3 |  ← Aynı rank ve dense_rank
| Ayşe       |  10000 |       5 |        5 |              4 |  ← RANK: 5, DENSE_RANK: 4!
| Ali        |   8000 |       6 |        6 |              5 |
+------------+--------+---------+----------+----------------+

Fark: Ayşe'nin RANK'ı 5, DENSE_RANK'ı 4. DENSE_RANK boşluk bırakmadı — 3'ten sonra 4 geldi.

Üçünü Karşılaştıralım

SenaryoROW_NUMBERRANKDENSE_RANK
1. sıra (15000)111
2. sıra (14000)222
3. sıra (12000) — 1. eşit333
3. sıra (12000) — 2. eşit433
4. sıra (10000)554

Ne zaman hangisini kullanmalı?

  • ROW_NUMBER: Her satıra benzersiz numara lazım (pagination, top-N per group, deduplication)

  • RANK: Olimpiyat tarzı sıralama (eşitlikte boşluk olsun — "kaçıncı olduğun" net olsun)

  • DENSE_RANK: "Kaç farklı seviye var" sorusu önemli (en yüksek 3 farklı maaşı bul)


Gerçek Dünya Örnekleri

Örnek 1: Her Kategoride En Pahalı 3 Ürün

SELECT * FROM (
    SELECT 
        p.product_name,
        c.category_name,
        p.price,
        ROW_NUMBER() OVER (
            PARTITION BY p.category_id 
            ORDER BY p.price DESC
        ) AS price_rank
    FROM products p
    JOIN categories c ON p.category_id = c.category_id
) ranked
WHERE price_rank <= 3;
+-------------------+---------------+--------+------------+
| product_name      | category_name | price  | price_rank |
+-------------------+---------------+--------+------------+
| MacBook Pro       | Elektronik    |  45000 |          1 |
| iPhone 15         | Elektronik    |  35000 |          2 |
| Samsung Galaxy    | Elektronik    |  28000 |          3 |
| Python Mastery    | Kitap         |    210 |          1 |
| SQL Deep Dive     | Kitap         |    190 |          2 |
| Clean Code        | Kitap         |    180 |          3 |
+-------------------+---------------+--------+------------+

Örnek 2: Müşterilerin Son 3 Siparişi

SELECT * FROM (
    SELECT 
        c.first_name,
        o.order_id,
        o.order_date,
        o.total_amount,
        ROW_NUMBER() OVER (
            PARTITION BY o.customer_id 
            ORDER BY o.order_date DESC
        ) AS order_rank
    FROM orders o
    JOIN customers c ON o.customer_id = c.customer_id
) recent
WHERE order_rank <= 3;

Bu sorgu, her müşterinin en son 3 siparişini getirir. E-ticaret sitesinde "son siparişleriniz" panelinde tam ihtiyacın olan sorgu.

Örnek 3: Duplicate Satırları Temizleme (Deduplication)

ROW_NUMBER'ın çok kullanışlı bir uygulaması: tekrarlayan kayıtları tespit etme ve temizleme.

-- Aynı müşteri aynı gün aynı tutarda birden fazla sipariş vermiş (duplicate)
-- İlkini tut, diğerlerini bul
SELECT * FROM (
    SELECT 
        order_id,
        customer_id,
        order_date,
        total_amount,
        ROW_NUMBER() OVER (
            PARTITION BY customer_id, order_date, total_amount
            ORDER BY order_id
        ) AS dup_num
    FROM orders
) dupes
WHERE dup_num > 1;
-- dup_num > 1 olanlar duplicate — silinebilir
-- Duplicate'leri silmek için:
DELETE FROM orders WHERE order_id IN (
    SELECT order_id FROM (
        SELECT 
            order_id,
            ROW_NUMBER() OVER (
                PARTITION BY customer_id, order_date, total_amount
                ORDER BY order_id
            ) AS dup_num
        FROM orders
    ) dupes
    WHERE dup_num > 1
);

Örnek 4: En Yüksek 3 Farklı Maaş Seviyesi (DENSE_RANK)

-- En yüksek 3 farklı maaş seviyesindeki çalışanlar
SELECT * FROM (
    SELECT 
        first_name,
        department_id,
        salary,
        DENSE_RANK() OVER (ORDER BY salary DESC) AS salary_level
    FROM employees
) levels
WHERE salary_level <= 3;
+------------+---------------+--------+--------------+
| first_name | department_id | salary | salary_level |
+------------+---------------+--------+--------------+
| Can        |             3 |  15000 |            1 |
| Ece        |             3 |  14000 |            2 |
| Deniz      |             3 |  13000 |            3 |
| Mehmet     |             2 |  13000 |            3 |  ← Aynı maaş, aynı seviye
+------------+---------------+--------+--------------+

DENSE_RANK kullandık çünkü "3 farklı maaş seviyesi" istiyoruz. RANK kullansaydık, 2 kişi aynı maaştaysa 3. seviye atlanırdı ve sadece 2 farklı seviye gelirdi.

Örnek 5: Aylık Satış Sıralaması

SELECT 
    DATE_FORMAT(o.order_date, '%Y-%m') AS month,
    c.category_name,
    SUM(oi.quantity * oi.unit_price) AS monthly_revenue,
    RANK() OVER (
        PARTITION BY DATE_FORMAT(o.order_date, '%Y-%m')
        ORDER BY SUM(oi.quantity * oi.unit_price) DESC
    ) AS revenue_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
WHERE o.order_date >= '2024-01-01'
GROUP BY DATE_FORMAT(o.order_date, '%Y-%m'), c.category_id, c.category_name
ORDER BY month, revenue_rank;

Bu sorgu, her ay hangi kategorinin en çok satış yaptığını sıralar. Dikkat: GROUP BY ile window function birlikte kullanılıyor — window function, GROUP BY'dan sonra çalışır.


Pagination İçin ROW_NUMBER

Keyset pagination yapamadığın durumlarda ROW_NUMBER ile sayfalama:

-- Sayfa 3'ü getir (her sayfada 20 ürün)
SELECT * FROM (
    SELECT 
        product_id,
        product_name,
        price,
        ROW_NUMBER() OVER (ORDER BY product_id) AS rn
    FROM products
) paged
WHERE rn BETWEEN 41 AND 60;  -- Sayfa 3: 41-60 arası

Bu yöntem OFFSET'e göre daha esnek çünkü karmaşık sıralama kriterlerinde de çalışır. Ama büyük tablolarda yine de keyset pagination daha performanslı.


Performans Notları

Sıralama fonksiyonları performans açısından dikkat gerektirir:

-- Her partition için ayrı sıralama yapılır
-- Büyük tablolarda ORDER BY maliyetli olabilir

-- ✅ Index ile destekle
CREATE INDEX idx_emp_dept_salary ON employees(department_id, salary DESC);
-- PARTITION BY department_id ORDER BY salary DESC — index tam uyumlu

-- ❌ Gereksiz sıralama yapma
-- Sadece ROW_NUMBER lazımsa RANK/DENSE_RANK da hesaplama — sadece ihtiyacın olanı kullan

-- ✅ PARTITION BY sütunlarında index olsun
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date DESC);
-- PARTITION BY customer_id ORDER BY order_date DESC

PostgreSQL Farklılıkları

-- PostgreSQL'de DISTINCT ON ile top-1 per group daha kolay:
SELECT DISTINCT ON (department_id) 
    first_name, department_id, salary
FROM employees
ORDER BY department_id, salary DESC;
-- MySQL'de DISTINCT ON yok — ROW_NUMBER ile yap

-- PostgreSQL'de FETCH FIRST ile LIMIT:
SELECT * FROM (
    SELECT *, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rn
    FROM employees
) t
FETCH FIRST 5 ROWS ONLY;
-- MySQL'de LIMIT 5 kullanırsın

Özet

  • ROW_NUMBER(): Her satıra benzersiz sıra numarası — eşitlikte bile farklı numara verir

  • RANK(): Eşit değerlere aynı numara — sonraki sırada boşluk bırakır (1, 1, 3)

  • DENSE_RANK(): Eşit değerlere aynı numara — boşluk bırakmaz (1, 1, 2)

  • PARTITION BY ile her grup için ayrı sıralama yapılır

  • "Top-N per group" kalıbı: subquery + WHERE rn <= N — en yaygın kullanım

  • ROW_NUMBER ile deduplication, pagination ve "son N kayıt" sorguları çözülür

  • ORDER BY'da belirsizlikten kaçın — aynı değerlerde ikinci sıralama kriteri ekle

Sıkça Yapılan Hatalar

  1. ROW_NUMBER'da ORDER BY'ı belirleyici yapmamak — Aynı değerde hangi satırın önce geleceği garanti edilmez. İkinci sıralama kriteri ekle (genellikle PK).

  2. RANK ve DENSE_RANK'ı karıştırmak — "İlk 3" istiyorsan RANK kullanırken, eşitlik durumunda 3'ten fazla sonuç gelebilir (1,1,3 ama 3. sırada 2 kişi). "İlk 3 farklı seviye" istiyorsan DENSE_RANK kullan.

  3. WHERE'de window function kullanmaya çalışmakWHERE rn <= 3 doğrudan yazılamaz. Subquery veya CTE gerekir.

  4. Büyük tablolarda ORDER BY maliyetini göz ardı etmek — ROW_NUMBER tüm partition'ı sıralamak zorunda. PARTITION BY ve ORDER BY sütunlarında composite index ekle.

  5. ROW_NUMBER'ı pagination için OFFSET yerine kullanıp performans beklemek — ROW_NUMBER da tüm satırları numaralamak zorunda. Keyset pagination hâlâ daha performanslı.