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.
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.
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.
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;
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.
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.
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.
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.
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.
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.
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.
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.
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.
Nambah 30 hari ke tanggal invoice, ngurangin sebulan buat cari periode lalu, ngitung selisih hari antar order. Semua itu aritmetika tanggal, dan SQL punya operator INTERVAL buat ngerjainnya.
Ngurangin tahun sekarang sama tahun lahir kelihatan gampang, tapi hasilnya sering meleset setahun kalau ulang tahunnya belum lewat. Ini cara hitung umur yang akurat di SQL.
Cara pakai DATE_TRUNC di SQL buat memotong timestamp jadi awal bulan, minggu, atau jam. Kunci bikin laporan penjualan per periode yang rapi.