Mengambil Top N per Grup di SQL (2026)
TL;DR
Buat ngambil top N per grup di SQL, pakai ROW_NUMBER() OVER (PARTITION BY grup ORDER BY nilai DESC) buat ngasih nomor urut di dalam tiap grup, lalu bungkus di subquery dan filter WHERE nomor urut lebih kecil atau sama dengan N. PARTITION BY yang bikin penomoran reset di tiap grup. Kalau nilai seri boleh dapat peringkat sama, ganti ROW_NUMBER pakai RANK atau DENSE_RANK sesuai kebutuhan.
Buat ngambil top N per grup di SQL, pakai ROW_NUMBER() dengan PARTITION BY, lalu filter nomor urutnya.
Pertanyaannya bukan "produk apa yang paling laku", tapi "produk apa yang paling laku di tiap kategori". Satu juara umum gak cukup. Kamu mau juara per grup.
Di tutorial ini kamu bakal bikin query top 3 per kategori, ngerti kenapa harus dibungkus subquery, dan tau kapan pakai RANK ketimbang ROW_NUMBER.
Kenapa GROUP BY biasa gak cukup?
GROUP BY bisa ngasih satu nilai agregat per grup, misalnya total penjualan per kategori. Tapi dia gak bisa ngasih "3 baris teratas per kategori". Begitu kamu mau beberapa baris detail per grup, kamu butuh window function.
Window function ngasih nomor atau peringkat ke tiap baris tanpa ngeciutin datanya jadi satu baris per grup. Itu bedanya dari GROUP BY.
Gimana cara pakai ROW_NUMBER dengan PARTITION BY?
Kuncinya ROW_NUMBER() OVER (PARTITION BY grup ORDER BY nilai DESC). PARTITION BY mecah data per grup, ORDER BY nentuin urutan di dalam grup, ROW_NUMBER ngasih nomor 1, 2, 3, dan reset tiap ganti grup.
WITH bernomor AS (
SELECT
kategori,
produk,
SUM(qty) AS total_qty,
ROW_NUMBER() OVER (
PARTITION BY kategori
ORDER BY SUM(qty) DESC
) AS urutan
FROM penjualan
GROUP BY kategori, produk
)
SELECT kategori, produk, total_qty
FROM bernomor
WHERE urutan <= 3
ORDER BY kategori, urutan;
Nomor urut dikasih di dalam CTE, lalu di luar CTE kita filter urutan <= 3. Hasilnya: 3 produk terlaris di setiap kategori. ROW_NUMBER adalah salah satu window function yang paling sering kepakai.
Kenapa harus dibungkus subquery atau CTE?
SQL gak ngizinin kamu nyaring window function langsung di WHERE. Alasannya urutan eksekusi: WHERE jalan sebelum ROW_NUMBER dihitung. Jadi WHERE ROW_NUMBER() <= 3 bakal error.
Solusinya bungkus dulu penomorannya di CTE atau subquery, baru filter di lapisan luar. Di lapisan luar, kolom urutan udah jadi kolom biasa yang bisa difilter.
-- Salah: window function gak bisa di WHERE langsung
-- SELECT ... WHERE ROW_NUMBER() OVER (...) <= 3
-- Benar: bungkus dulu, filter di luar
SELECT * FROM bernomor WHERE urutan <= 3;
Kapan pakai RANK atau DENSE_RANK?
ROW_NUMBER selalu ngasih nomor unik, walau nilainya seri. Kalau dua produk sama-sama laku 100, salah satu tetap dapat urutan 1 dan satunya 2, dipilih sembarang. Buat menangani seri, pakai RANK atau DENSE_RANK.
| Fungsi | Kalau ada seri di posisi 1 | Nomor berikutnya |
|---|---|---|
| ROW_NUMBER | 1, 2 (dipilih sembarang) | 3 |
| RANK | 1, 1 | 3 (lompat) |
| DENSE_RANK | 1, 1 | 2 (gak lompat) |
Pakai ROW_NUMBER kalau kamu mau tepat N baris, gak peduli seri. Pakai RANK kalau seri boleh dapat peringkat sama dan kamu gak masalah hasilnya bisa lebih dari N baris.
Contoh kasus: produk terlaris per kategori toko_berkah
Dataset toko_berkah punya Ngulik Data isinya 8.400 transaksi warung kelontong di Semarang, 142 produk, 5 kategori.
Pemiliknya mau tau 3 produk terlaris di tiap kategori, biar rak per kategori diisi barang yang bener. Query top 3 tadi ngasih hasil ini buat kategori Minuman:
| Urutan | Produk | Total qty |
|---|---|---|
| 1 | Teh kotak | 1.240 |
| 2 | Air mineral 600ml | 1.190 |
| 3 | Kopi sachet | 870 |
Yang menarik: kalau pemiliknya cuma lihat top 5 produk keseluruhan, tiga slot teratas semuanya Minuman. Kategori Sembako gak kelihatan sama sekali, padahal produk terlaris Sembako (beras 5kg) nyumbang 18% dari total omzet warung.
Top N per grup nunjukin jagoan di tiap kategori. Top N keseluruhan cuma nunjukin kategori yang volumenya paling gede. Dua pertanyaan beda, dan yang per grup lebih kepakai buat atur rak.
Kesalahan umum waktu ambil top N per grup
Nyoba filter ROW_NUMBER di WHERE langsung. Ini error paling sering. Window function harus dibungkus CTE atau subquery dulu, baru difilter di luar.
Lupa PARTITION BY. Tanpa PARTITION BY, ROW_NUMBER ngasih nomor buat seluruh tabel, bukan per grup. Hasilnya kamu dapat top N keseluruhan, bukan per kategori.
Salah pilih ROW_NUMBER vs RANK di data yang banyak seri. Kalau nilainya sering seri dan seri harus diperlakukan sama, ROW_NUMBER bakal motong sembarang. Pakai RANK atau DENSE_RANK.
ORDER BY kebalik. Buat top N kamu butuh DESC biar yang terbesar dapat nomor 1. Kalau lupa DESC, kamu malah dapat N terkecil.
Agregat dan window dicampur salah tingkat. Kalau kamu ngerangkum pakai SUM(qty), penomorannya juga harus di ORDER BY SUM(qty), bukan qty mentah. Perhatiin level agregasinya.
FAQ
Apa bedanya ROW_NUMBER, RANK, dan DENSE_RANK?
ROW_NUMBER ngasih nomor unik ke tiap baris tanpa peduli seri, jadi dua nilai sama tetap dapat nomor beda. RANK ngasih peringkat sama buat seri, tapi nomor berikutnya lompat, misalnya 1, 1, 3. DENSE_RANK juga ngasih peringkat sama buat seri, tapi gak lompat, jadi 1, 1, 2. Pilih ROW_NUMBER buat tepat N baris, RANK atau DENSE_RANK kalau seri harus diperlakukan setara.
Kenapa WHERE ROW_NUMBER gak bisa langsung?
Gara-gara urutan eksekusi SQL. WHERE dijalanin sebelum window function dihitung, jadi di titik itu ROW_NUMBER belum ada nilainya. Makanya kamu harus bungkus penomorannya di CTE atau subquery dulu. Di lapisan luar, hasil ROW_NUMBER udah jadi kolom biasa yang bisa difilter pakai WHERE. Ini pola standar buat semua filter yang melibatkan window function.
Bisakah ambil top N per grup tanpa window function?
Bisa, tapi ribet dan lambat. Cara lama pakai subquery berkorelasi yang ngitung berapa baris lebih besar di grup yang sama buat tiap baris. Di tabel besar ini jauh lebih lambat dari ROW_NUMBER. Kalau database kamu udah dukung window function, dan hampir semua database modern udah, pakai ROW_NUMBER. Lebih cepat, lebih gampang dibaca, dan lebih gampang dirawat.
Gimana cara ambil cuma baris teratas per grup?
Sama kayak top N, tapi filternya urutan = 1, bukan urutan lebih kecil atau sama dengan N. Pakai ROW_NUMBER() OVER (PARTITION BY grup ORDER BY nilai DESC), bungkus di CTE, lalu WHERE urutan = 1. Ini pola umum buat nyari transaksi terbesar per pelanggan, order terbaru per akun, atau nilai tertinggi per kategori.
Apakah pola ini jalan di MySQL?
Jalan, mulai MySQL 8.0 yang udah dukung window function. Di MySQL 5.7 ke bawah, ROW_NUMBER belum ada, jadi kamu harus pakai trik variabel atau subquery berkorelasi. PostgreSQL, BigQuery, Snowflake, dan DuckDB semua udah dukung pola ini bertahun-tahun. Kalau kamu masih di MySQL lama, upgrade ke 8.0 bikin query semacam ini jauh lebih sederhana.
Penutup
Top N per grup itu pola yang kepakai terus: produk terlaris per kategori, transaksi terbesar per pelanggan, order terbaru per akun.
Ingat tiga hal: PARTITION BY buat mecah per grup, ORDER BY DESC biar yang terbesar dapat nomor 1, dan bungkus di CTE sebelum filter.
Detail cara kerja window function ada di tutorial resmi PostgreSQL.
Semua query di atas udah dites dan siap kamu copy-paste. Lanjut baca distribusi kumulatif di SQL buat window function yang lain, dan segmentasi pelanggan dengan SQL buat ngelompokin pelanggan dari data transaksi.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Artikel terkait
Composite Index di SQL: Cara Kerja dan Urutan Kolom
Composite index itu index atas beberapa kolom sekaligus. Urutan kolomnya nentuin query mana yang kebantu, lewat aturan leftmost prefix.
Menghitung Stickiness (DAU/MAU) di SQL
Stickiness ngukur seberapa sering user balik lagi. Ini cara hitung DAU, MAU, dan rasio stickiness pakai SQL, plus contoh aplikasi UMKM.
Menghitung DAU dan MAU dengan SQL
DAU dan MAU dua metrik wajib buat produk digital. Ini cara hitungnya pakai SQL, lengkap query DISTINCT, per bulan, sampai rasio stickiness.