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ırPARTITION 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
| Senaryo | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 1. sıra (15000) | 1 | 1 | 1 |
| 2. sıra (14000) | 2 | 2 | 2 |
| 3. sıra (12000) — 1. eşit | 3 | 3 | 3 |
| 3. sıra (12000) — 2. eşit | 4 | 3 | 3 |
| 4. sıra (10000) | 5 | 5 | 4 |
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 DESCPostgreSQL 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
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).
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.
WHERE'de window function kullanmaya çalışmak —
WHERE rn <= 3doğrudan yazılamaz. Subquery veya CTE gerekir.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.
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ı.
AI Asistan
Sorularını yanıtlamaya hazır