TL;DR
RANK dan DENSE_RANK sama-sama ngasih nomor urut di SQL, bedanya cuma waktu ada nilai kembar. Kalau dua baris seri di posisi 1, RANK ngasih 1, 1, lalu loncat ke 3. DENSE_RANK ngasih 1, 1, lalu 2. Nomornya rapat tanpa loncatan. Pakai RANK kalau kamu mau posisi mencerminkan berapa banyak yang di atasnya. Pakai DENSE_RANK kalau kamu mau ngitung tingkatan nilai yang unik, misalnya cari 3 harga tertinggi.
RANK dan DENSE_RANK sama-sama ngasih nomor urut di SQL. Bedanya cuma muncul waktu ada nilai kembar.
Kalau dua produk laku sama banyak dan sama-sama di posisi 1, RANK ngasih nomor berikutnya 3. DENSE_RANK ngasih 2.
Itu doang bedanya. Tapi salah pilih bisa bikin laporan "Top 3" kamu isinya cuma 2 baris.
RANK ngasih nomor sama ke baris yang seri, terus ngeloncat sesuai jumlah yang seri. Nilai 100, 100, 90 dapat rank 1, 1, 3.
DENSE_RANK ngasih nomor sama ke baris yang seri, terus lanjut tanpa loncat. Nilai 100, 100, 90 dapat rank 1, 1, 2.
Kalau gak ada nilai kembar sama sekali, dua-duanya ngasih hasil yang persis sama. Bedanya baru kelihatan begitu ada seri.
Tabel produk_terlaris dari dataset toko_berkah:
SELECT
nama_produk,
total_terjual,
ROW_NUMBER() OVER (ORDER BY total_terjual DESC) AS row_num,
RANK() OVER (ORDER BY total_terjual DESC) AS rank_biasa,
DENSE_RANK() OVER (ORDER BY total_terjual DESC) AS rank_rapat
FROM produk_terlaris
ORDER BY total_terjual DESC;
Hasilnya:
| nama_produk | total_terjual | row_num | rank_biasa | rank_rapat |
|---|---|---|---|---|
| Beras Pandan Wangi 5kg | 1.240 | 1 | 1 | 1 |
| Minyak Goreng 2L | 980 | 2 | 2 | 2 |
| Gula Pasir 1kg | 980 | 3 | 2 | 2 |
| Telur Ayam 1kg | 870 | 4 | 4 | 3 |
| Tepung Terigu 1kg | 720 | 5 | 5 | 4 |
Perhatiin baris Telur Ayam. RANK ngasih dia nomor 4 (soalnya ada 3 produk di atasnya). DENSE_RANK ngasih 3 (soalnya cuma ada 2 tingkatan nilai di atasnya).
ROW_NUMBER gak peduli seri sama sekali. Dia kasih nomor unik 1 sampai 5 apa pun yang terjadi.
Pakai RANK kalau nomor urutnya harus mencerminkan berapa banyak baris yang di atasnya.
Contoh nyata: peringkat cabang berdasarkan omzet, buat dilaporin ke manajemen. Kalau ada 2 cabang seri di posisi 2, cabang berikutnya emang posisi 4. Ada 3 cabang yang omzetnya lebih tinggi atau sama.
Ini logika yang sama kayak peringkat lomba. Dua juara 2, gak ada juara 3.
Pakai DENSE_RANK kalau kamu ngitung tingkatan nilai yang unik, bukan jumlah baris.
Contoh: "kasih diskon buat produk di 3 tier harga tertinggi". Yang kamu cari 3 tingkatan harga, bukan 3 baris.
Kalau kamu pakai RANK di sini, dan ada 2 produk yang harganya persis sama di tier 2, hasil WHERE rk <= 3 kamu bakal ngelewatin satu tier harga. Soalnya nomornya loncat dari 2 ke 4.
Pakai ROW_NUMBER kalau kamu butuh tepat satu baris per grup, dan gak peduli ada yang seri.
Kasus klasik: hapus duplikat, atau ambil transaksi terbaru per pelanggan.
WITH terbaru AS (
SELECT
id_pelanggan,
id_transaksi,
tanggal,
ROW_NUMBER() OVER (
PARTITION BY id_pelanggan
ORDER BY tanggal DESC
) AS rn
FROM transaksi
)
SELECT id_pelanggan, id_transaksi, tanggal
FROM terbaru
WHERE rn = 1;
Kalau kamu pakai RANK di sini dan ada pelanggan yang punya 2 transaksi di tanggal yang sama, kamu bakal dapet 2 baris buat pelanggan itu. ROW_NUMBER mastiin cuma 1.
Ini kasus yang paling sering muncul di kerjaan nyata. Dataset toko_berkah punya 8.400 transaksi dari 12 cabang di 6 kota.
WITH ranked AS (
SELECT
kota,
nama_produk,
SUM(total) AS omzet,
DENSE_RANK() OVER (
PARTITION BY kota
ORDER BY SUM(total) DESC
) AS rk
FROM transaksi
GROUP BY kota, nama_produk
)
SELECT kota, nama_produk, omzet, rk
FROM ranked
WHERE rk <= 3
ORDER BY kota, rk;
PARTITION BY kota bikin nomor urutnya reset tiap ganti kota. Jadi tiap kota punya top 3 sendiri.
Hasil dari dataset ngulikdata: 4 dari 6 kota punya Beras Pandan Wangi di posisi 1. Dua kota yang beda (Denpasar dan Surabaya) posisi 1-nya diisi Minyak Goreng 2L.
Insight ini yang bikin tim procurement nyetok beras beda-beda per kota.
Ini kesalahan nomor satu yang aku lihat.
-- INI ERROR
SELECT nama_produk, DENSE_RANK() OVER (ORDER BY omzet DESC) AS rk
FROM produk
WHERE DENSE_RANK() OVER (ORDER BY omzet DESC) <= 3;
Alasannya: SQL ngerjain WHERE duluan, baru window function dihitung. Waktu WHERE jalan, nilai rk belum ada.
Solusinya bungkus pakai CTE atau subquery, terus filter di luar. Persis kayak contoh top 3 per kota di atas.
Pakai RANK buat top-N padahal maunya N tingkatan. Hasil "top 3" kamu bisa cuma keluar 2 tier harga kalau ada seri.
Lupa PARTITION BY waktu butuh per-grup. Tanpa PARTITION BY, nomor urutnya jalan terus dari baris pertama sampai terakhir tanpa reset.
Lupa ORDER BY di dalam OVER. RANK dan DENSE_RANK wajib punya ORDER BY di dalam OVER. Kalau nggak, gak ada dasar buat ngurutin.
Nyoba di MySQL 5.7. Window function baru ada di MySQL 8.0. Kalau kamu masih di 5.7, query-nya bakal error syntax. Cek versinya pakai SELECT VERSION();.
RANK ngeloncat setelah nilai kembar (1, 1, 3). DENSE_RANK lanjut rapat tanpa loncat (1, 1, 2). Kalau gak ada nilai kembar, hasilnya identik.
Waktu kamu butuh nomor unik tiap baris tanpa peduli ada yang seri, misalnya buat hapus duplikat atau ambil tepat satu transaksi terbaru per pelanggan.
Bungkus DENSE_RANK plus PARTITION BY di dalam CTE, terus filter WHERE rk <= 3 di query luar. Window function gak bisa dipakai langsung di WHERE.
Jalan. PostgreSQL support sejak 8.4, MySQL sejak 8.0. Di MySQL 5.7 ke bawah gak ada window function. Kamu harus akalin pakai variabel user atau self-join.
Aturan sederhananya: RANK buat peringkat yang ngitung berapa banyak di atasnya, DENSE_RANK buat ngitung tingkatan nilai, ROW_NUMBER buat ambil tepat satu baris.
Dan window function selalu difilter dari luar, lewat CTE.
Definisi resmi ketiganya bisa kamu cek di dokumentasi PostgreSQL.
Kalau kamu belum kenal konsep window function-nya sendiri, mulai dari penjelasan window function di glossary.
Mau latihan query ranking pakai data UMKM Indonesia? Coba NgulikSQL, langsung nulis query di browser, hasilnya keluar detik itu juga.
Lanjut baca: Calculated Field Looker Studio: Bikin Metrik Sendiri.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.