SQL ROW_NUMBER() ile her gruptan son kaydı seçmek için önce her grup içindeki satırlara bir sıra numarası verilir. Ardından dış sorguda rn = 1 koşuluyla her gruptaki tek kayıt, rn <= N koşuluyla ise her gruptaki ilk N kayıt alınır. Bu yaklaşım, seçilen kaydın yalnızca tarihini değil, ilgili satırdaki tüm sütunları korur.
Sağlıklı bir sorgu kurmak için beş kararı netleştirmelisin: Bir satır neyi temsil ediyor, grup hangi sütunla tanımlanıyor, “son” veya “en yüksek” kayıt hangi ölçüte göre belirleniyor, eşitlik durumunda hangi benzersiz sütun kullanılacak ve her gruptan kaç kayıt döndürülecek?
SQL ROW_NUMBER() ile her gruptan son kayıt seçmenin temel mantığı
Bir veri kümesinde her müşterinin son siparişini, her kategorinin en yüksek ürününü veya her grubun ilk birkaç satırını seçmek istediğinde temel işlem iki aşamalıdır:
- Her grup içindeki satırları belirlenen ölçüte göre sırala.
- Bu sıralamaya göre satırlara sıra numarası ver ve dış sorguda gerekli sıra numaralarını filtrele.
Örneğin her müşterinin son siparişini seçiyorsan, satırın “sipariş” olduğunu; grubun customer_id ile tanımlandığını; son kaydın order_date değerine göre belirlendiğini ve her gruptan yalnızca bir kayıt döneceğini önceden kararlaştırmalısın.
Bu karar çerçevesini şu beş soruyla uygulayabilirsin:
- Satır granülerliği: Bir satır müşteri, sipariş, ürün veya başka bir varlığı mı temsil ediyor?
- Grup tanımı: Satırları hangi sütun aynı grupta topluyor? Örneğin
customer_idveyacategory_id. - Sıralama ölçütü: “Son” kayıt tarih azalan, “en yüksek” ürün fiyat ya da puan azalan sıralanarak mı bulunacak?
- Eşitlik bağlayıcısı: İki satırın ana ölçütü aynıysa, sonucu belirlemek için hangi benzersiz sütun kullanılacak?
- Döndürülecek kayıt sayısı: Her gruptan tek satır mı, yoksa ilk iki veya ilk N satır mı alınacak?
GROUP BY ile MAX() kullanmak grupları özetlemek için uygundur; örneğin her müşterinin en son sipariş tarihini bulabilirsin. Ancak bu yöntem, o tarihe ait sipariş numarası, tutar veya durum gibi diğer sütunları kendiliğinden aynı satırla birlikte getirmez. ROW_NUMBER() ise satırları birleştirmek yerine her satıra sıra numarası ekler. Böylece dış sorguda ilk satırı seçtiğinde ilgili kaydın tüm sütunlarını koruyabilirsin.
OVER, PARTITION BY ve ORDER BY satır satır nasıl çalışır?
Her müşterinin son siparişini seçmek için kullanılan temel ifade şöyledir:
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC
) AS rn
Bu ifadeyi parçalara ayırarak okuyalım:
ROW_NUMBER(): Her grup içinde 1’den başlayan ardışık sıra numarası üretir. İlk sıradaki satırın değeri 1, sonraki satırın değeri 2 olur.OVER (...): Sıra numarasının hangi pencere kurallarına göre hesaplanacağını belirtir.PARTITION BY customer_id: Satırları müşteri numarasına göre ayrı gruplara böler. Buradaki işlem satırları birleştirmez veya azaltmaz; her sipariş satırı sonuçta varlığını korur.ORDER BY order_date DESC: Her müşterinin siparişlerini tarih bakımından yeniden eskiye sıralar. Böylece en yeni sipariş önce gelir vern = 1koşuluyla seçilebilir.order_id DESC: Sipariş tarihleri eşitse ikinci sıralama ölçütü olarak kullanılır. Benzersiz ve tutarlı bir bağlayıcı sütun, hangi satırın önce geleceğini belirginleştirir.
Burada DESC yönü “en son” veya “en yüksek” değeri ilk sıraya taşımak için kullanılır. En eski ya da en düşük kaydı seçmek istiyorsan ilgili ölçütü ASC yönünde sıralayabilirsin. Ayrıca pencere içindeki ORDER BY, yalnızca sıra numarasının nasıl üretileceğini belirler; nihai sonuç kümesini otomatik olarak düzenlemez. Sonuçları ekranda belirli bir sırada görmek istiyorsan dış sorguya ayrıca ORDER BY eklemelisin.
Grup, sıralama ve bağlayıcı sütun kararlarını uygulamalı olarak pekiştirmek için algoritmik düşünme bilgi testini kullanabilirsin. Çünkü ROW_NUMBER() kullanımı, yalnızca sözdizimini bilmekten çok, “hangi satır hangi ölçüte göre seçilmeli?” sorusunu doğru kurmayı gerektirir.
CTE ve alt sorguyla ROW_NUMBER() sonucu nasıl filtrelenir?

ROW_NUMBER() ile üretilen rn değerini, bu değerin oluşturulduğu sorgu katmanının dışından filtrelemek en anlaşılır yaklaşımdır. CTE ile kullanılan taşınabilir kalıp şöyledir:
WITH ranked AS (
SELECT
order_id,
customer_id,
order_date,
total_amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC
) AS rn
FROM orders
)
SELECT
order_id,
customer_id,
order_date,
total_amount
FROM ranked
WHERE rn = 1;
Bu sorguda iç katman, orders tablosundaki tüm satırları alır ve her müşterinin siparişlerine sıra numarası verir. Dış katman ise yalnızca rn = 1 olan satırları bırakır. Sonuç olarak her müşteriden bir sipariş döner.
Aynı işlem CTE yerine alt sorguyla da yazılabilir:
SELECT
order_id,
customer_id,
order_date,
total_amount
FROM (
SELECT
order_id,
customer_id,
order_date,
total_amount,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC
) AS rn
FROM orders
) AS ranked
WHERE rn <= 2;
Buradaki rn <= 2 koşulu, her müşteriden sıralamadaki ilk iki siparişi getirir. rn = 1 her gruptan tek kayıt seçerken, rn <= N her gruptan ilk N kaydı seçer. Örneklerdeki tablo ve sütun adlarını kendi veri tabanındaki yapıya göre uyarlamalısın.
Örnek: Her müşterinin en son siparişini tüm sütunlarıyla seçme
Her müşterinin yalnızca en son siparişini almak için önce müşteriyi grup olarak tanımlar, sonra siparişleri tarihe göre tersten sıralarız. Aynı tarihe sahip siparişlerde hangi satırın seçileceğini belirlemek için ikinci bir sıralama ölçütü, yani order_id DESC, eklemek gerekir.
Bu örnekte karar çerçevesi şöyledir:
- Sonuç granülerliği: Her müşteri için bir satır.
- Grup tanımı:
customer_id. - Son kayıt ölçütü:
order_date DESC. - Eşitlik bağlayıcısı: Aynı tarihte daha büyük
order_idöne alınacak. - Döndürülecek kayıt sayısı: Her grup için
1.
Aşağıdaki orders tablosu tamamen örnek amaçlıdır:
| order_id | customer_id | order_date | status | item_count |
|---|---|---|---|---|
| 101 | 1 | 2025-04-10 | shipped | 3 |
| 102 | 1 | 2025-04-15 | pending | 1 |
| 103 | 2 | 2025-04-20 | delivered | 2 |
| 104 | 2 | 2025-04-20 | cancelled | 5 |
customer_id = 2 için 103 ve 104 numaralı siparişlerin tarihleri aynıdır. Bu nedenle yalnızca order_date DESC yazmak, eşitlik durumunu çözmez. Bu örnekte daha büyük sipariş numarasını seçmek istediğimiz için order_id DESC bağlayıcısını ekliyoruz.
WITH ranked_orders AS (
SELECT
o.*,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY order_date DESC, order_id DESC
) AS rn
FROM orders AS o
)
SELECT
order_id,
customer_id,
order_date,
status,
item_count
FROM ranked_orders
WHERE rn = 1
ORDER BY customer_id;
CTE içindeki o.*, siparişin tüm iş sütunlarını korur. ROW_NUMBER() ise her müşteri grubunda yeniden başlayan bir sıra numarası üretir. Dış sorguda rn = 1 koşulu uygulandığında her müşterinin yalnızca ilk sıradaki, yani belirlediğimiz kurala göre en son siparişi kalır.
Beklenen sonuç aşağıdaki gibidir:
| order_id | customer_id | order_date | status | item_count |
|---|---|---|---|---|
| 102 | 1 | 2025-04-15 | pending | 1 |
| 104 | 2 | 2025-04-20 | cancelled | 5 |
Sonuçta yalnızca tarih değil, seçilen siparişe ait status ve item_count gibi diğer sütunlar da korunur. Müşteri 2 için iki tarih eşit olduğu hâlde order_id DESC kuralı 104 numaralı siparişi seçmiştir.
Örnek: Her kategorinin en yüksek iki ürününü seçme
Aynı yaklaşım, her kategori içinden en yüksek puanlı iki ürünü seçmek için de kullanılabilir. Bu kez gruplar category_id, ana sıralama ölçütü product_score DESC, eşitlik bağlayıcısı ise daha küçük ürün numarasını öne alan product_id ASC olacaktır.
Örnek products verisi:
| product_id | category_id | product_name | product_score |
|---|---|---|---|
| 11 | A | Klavye | 98 |
| 12 | A | Monitör | 95 |
| 13 | A | Mikrofon | 95 |
| 14 | A | Web Kamerası | 90 |
| 21 | B | SSD | 99 |
| 22 | B | RAM | 94 |
| 23 | B | Kasa | 88 |
Kategori A'da Monitör ve Mikrofon aynı puana sahiptir. product_id ASC bağlayıcısı sayesinde 12 numaralı ürün ikinci, 13 numaralı ürün üçüncü sıraya yerleşir.
WITH ranked_products AS (
SELECT
p.*,
ROW_NUMBER() OVER (
PARTITION BY category_id
ORDER BY product_score DESC, product_id ASC
) AS rn
FROM products AS p
)
SELECT
category_id,
product_name,
product_score,
rn
FROM ranked_products
WHERE rn <= 2
ORDER BY category_id, rn;
Beklenen sonuç:
| Kategori | Ürün adı | Skor | Sıra numarası |
|---|---|---|---|
| A | Klavye | 98 | 1 |
| A | Monitör | 95 | 2 |
| B | SSD | 99 | 1 |
| B | RAM | 94 | 2 |
rn <= 2 her kategori için tam iki satır döndürür. Kategori A'daki iki ürünün puanı aynı olsa da product_id ASC sıralaması, hangisinin ikinci sırada olacağını belirler. Benzer sorgu mantıklarını farklı senaryolarla incelemek için SQL ve veri tabanı yazılarına göz atabilirsiniz.
GROUP BY MAX() neden tüm sütunları getiremez? ROW_NUMBER() ile RANK() farkı nedir?
GROUP BY ve MAX() yaklaşımı, her müşterinin en büyük sipariş tarihini bulmak için yeterlidir:
SELECT
customer_id,
MAX(order_date) AS last_date
FROM orders
GROUP BY customer_id;
Bu sorgu müşteri başına yalnızca customer_id ve en büyük tarihi döndürür. order_id, status veya item_count gibi sütunları aynı sipariş satırından otomatik olarak taşımaz. Çünkü MAX() bir satırı seçmez; her grup içindeki tek bir değerin en büyüğünü hesaplar. Belirli satırın tüm sütunlarını almak gerektiğinde ROW_NUMBER(), hem sıralama hem de satır seçme işlemini aynı akışta ifade eder.
ROW_NUMBER() ile RANK() arasındaki temel fark eşit değerlerin sıralanışında ortaya çıkar. Aynı ürün verisinde RANK() yalnızca puana göre çalıştırılırsa Kategori A'da 98 puanlı ürün 1. dereceyi, 95 puanlı iki ürün ise aynı 2. dereceyi alır. Bu nedenle rank <= 2 koşulu üç ürün döndürebilir. Sonraki derece, arada iki ürün bulunduğu için 4 olur.
SELECT
category_id,
product_name,
product_score,
RANK() OVER (
PARTITION BY category_id
ORDER BY product_score DESC
) AS score_rank
FROM products;
Karar ölçütü nettir: Eşit skora sahip kayıtların tamamı isteniyorsa RANK() kullanılabilir. Her kategori için tam olarak iki satır isteniyorsa, eşitlik bağlayıcısı eklenmiş deterministik ROW_NUMBER() tercih edilmelidir.
SQL motorlarında ROW_NUMBER() filtreleme sözdizimi nasıl değişir?
Kısa cevap: En taşınabilir yaklaşım, ROW_NUMBER() sonucunu bir CTE veya alt sorguda üretip rn takma adını dış sorgunun WHERE koşulunda filtrelemektir. Çünkü pencere fonksiyonu sonucu, aynı sorgu düzeyindeki WHERE aşamasından sonra hesaplanır. Resmî PostgreSQL belgeleri de pencere fonksiyonlarının doğrudan WHERE içinde kullanılamadığını ve bu durumda dış sorgu gerektiğini belirtir.
Taşınabilir ana kalıp: CTE ve dış WHERE
WITH sirali_siparisler AS (
SELECT
musteri_id,
siparis_id,
siparis_tarihi,
ROW_NUMBER() OVER (
PARTITION BY musteri_id
ORDER BY siparis_tarihi DESC, siparis_id DESC
) AS rn
FROM siparisler
)
SELECT musteri_id, siparis_id, siparis_tarihi
FROM sirali_siparisler
WHERE rn = 1;
Burada rn, CTE’nin ürettiği sonuç kümesinin bir sütunu hâline gelir. Böylece dış sorgu onu normal bir sütun gibi filtreleyebilir. Aynı mantık, CTE yerine parantez içindeki alt sorgu ve dış WHERE ile de yazılabilir. CTE yazımı ve pencere fonksiyonu desteği motor ile sürüme göre ayrıca kontrol edilmelidir; SQL Server ve MySQL’in resmî belgelerinde bu yapı için ayrı sözdizimi ve kullanım kuralları bulunur.
QUALIFY: Daha kısa, fakat taşınabilir olmayan yaklaşım
QUALIFY destekleyen motorlarda pencere fonksiyonunun sonucu aynı sorgu içinde daha kısa biçimde filtrelenebilir:
SELECT
musteri_id,
siparis_id,
siparis_tarihi
FROM siparisler
QUALIFY ROW_NUMBER() OVER (
PARTITION BY musteri_id
ORDER BY siparis_tarihi DESC, siparis_id DESC
) = 1;
BigQuery ve Snowflake resmî belgeleri, QUALIFY ifadesinin pencere fonksiyonu sonuçlarını filtrelemek için kullanılabildiğini gösterir. Ancak bu söz dizimi her SQL motorunda veya her sürümde varsayılamaz. Ayrıca bazı motorlarda pencere sonucu takma adının QUALIFY içinde kullanılıp kullanılamadığı farklı olabilir. Bu nedenle hedef motorun ve sürümün resmî belgesi kontrol edilmeden kesin destek iddiasında bulunulmamalıdır.
| Yaklaşım | Kullanım biçimi | Taşınabilirlik notu | Kontrol edilmesi gereken nokta |
|---|---|---|---|
| CTE + dış WHERE | WITH ... AS (...) içinde rn üret, dış sorguda filtrele. |
Genellikle okunabilir ve lehçeye bağımlılığı azaltan ana kalıptır. | CTE desteği, WITH sözdizimi ve takma adın dış sorguya aktarılması. |
| Alt sorgu + dış WHERE | FROM (... ) AS sirali üzerinden rn değerini filtrele. |
CTE kullanılamayan veya tercih edilmeyen durumlarda uygulanabilir. | Alt sorguya verilen ad ve iç sorgudan rn sütununun dışarı taşınması. |
| QUALIFY | Pencere ifadesini ya da destekleniyorsa ilgili takma adı doğrudan filtrele. | Kısa, fakat belirli motorlara ve sürümlere bağlıdır. | QUALIFY desteği, ifade sırası ve takma ad kullanım kuralları. |
Bu yaklaşımlardan birinin her durumda daha hızlı olduğu söylenemez. Performans değerlendirmesi, kullanılan motorun çalışma planı, veri dağılımı ve sorgu koşullarıyla birlikte ölçülmelidir.
ROW_NUMBER() sorgusunu çalıştırmadan önce karar kontrol listesi
Sorguyu yazmadan önce kararı beş parçaya ayır: sonuçta bir satırın neyi temsil ettiği, grubun nasıl tanımlandığı, “son” veya “en yüksek” kaydın hangi ölçütle belirlendiği, eşitliklerin nasıl çözüleceği ve her gruptan kaç kayıt döndürüleceği.
- Sonuçta bir satır neyi temsil ediyor: müşteri, sipariş, ürün veya başka bir kayıt mı?
PARTITION BYgerçekten doğru grup anahtarını mı kullanıyor?ORDER BYölçütü ve yönü “son” ya da “en yüksek” anlamıyla uyumlu mu?- Eşit değerlerde benzersiz ve mantıklı bir bağlayıcı sütun var mı?
- Her gruptan bir kayıt mı, yoksa ilk N kayıt mı seçiliyor?
rnfiltresi dış sorguda mı uygulanıyor?- Son
SELECT, seçilen satır için ihtiyaç duyulan tüm sütunları taşıyor mu? - Kullanılan SQL motorunun ve sürümünün sözdizimi resmî belgelerle doğrulandı mı?
Üç kısa hata teşhisi
- Her gruptan birden fazla satır geliyorsa: bölümleme anahtarını ve
rnfiltre koşulunu kontrol et. - Yanlış kayıt geliyorsa:
ORDER BYyönünü ve eşit değerleri çözen bağlayıcı sütunu kontrol et. - Beklenenden fazla eşit kayıt geliyorsa:
ROW_NUMBER()yerineRANK()kullanılıp kullanılmadığını kontrol et.
Sık Sorulan Sorular
Tarih değerleri eşitse ROW_NUMBER() hangi siparişi son kayıt olarak seçer?
Yalnızca tarih sütununa göre sıralama yapıldığında eşit tarihler arasındaki seçim belirli olmayabilir. Kararlı bir sonuç için ORDER BY siparis_tarihi DESC, siparis_id DESC gibi benzersiz bir bağlayıcı ekle.
Her gruptan ilk N kaydı seçmek için ROW_NUMBER() filtresi nasıl yazılır?
Önce ROW_NUMBER() değerini CTE veya alt sorguda üret, ardından dış sorguda WHERE rn <= N koşulunu kullan. Böylece her bölümün ilk N satırı alınır.
SQL motoru QUALIFY desteklemiyorsa ROW_NUMBER() sonucu nasıl filtrelenir?
ROW_NUMBER() sonucunu bir CTE veya alt sorguya taşı ve dış sorguda rn = 1 ya da rn <= N ile filtrele. Bu, pencere sonucu takma adını ayrı bir sorgu düzeyinde kullanılabilir hâle getirir.
ROW_NUMBER() yerine RANK() kullanıldığında neden beklenenden fazla satır dönebilir?
RANK(), sıralama değerleri eşit olan satırlara aynı sıra numarasını verir. Bu nedenle rank <= N filtresi, sınırdaki eşit kayıtların tamamını döndürerek grup başına N değerinden fazla satır üretebilir.
Doğru sonucu almak için önce grup, sıralama ve eşitlik kararlarını netleştir; ardından seçtiğin SQL motorunun resmî sözdizimiyle sorgunu doğrula.