Kompetisi nyata penuh hasil seri: dua peserta dengan skor sama sama-sama berhak menjadi juara pertama, dan peserta berikutnya tidak otomatis menjadi juara kedua. ROW_NUMBER() dari chapter sebelumnya tidak menghormati situasi itu — pasangan Siti dan Andi yang bergaji sama dipaksa menerima nomor berbeda. MySQL menyediakan dua fungsi ranking yang memperlakukan seri secara jujur: RANK() memberi peringkat sama lalu melompat, sedangkan DENSE_RANK() memberi peringkat sama tanpa melompat. Pada chapter ini kita mengadu ketiganya pada data yang sama dengan dua pasang nilai seri, membaca pola angka 1, 1, 3 melawan 1, 1, 2, lalu menerapkannya pada kasus nyata: papan juara, filter “tiga besar”, dan peringkat teratas per grup.

Menyiapkan Tabel Latihan #

Kita pakai database latihan belajar_window dengan tabel karyawan sepuluh baris. Dua pasang gaji sengaja dibuat sama persis: Siti dan Andi sama-sama 15000000.00 di puncak, Rina dan Sari sama-sama 9000000.00 di dasar:

sql
DROP DATABASE IF EXISTS belajar_window;
CREATE DATABASE belajar_window;
USE belajar_window;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE karyawan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  departemen VARCHAR(30) NOT NULL,
  nama VARCHAR(50) NOT NULL,
  gaji DECIMAL(10,2) NOT NULL
);

INSERT INTO karyawan (departemen, nama, gaji) VALUES
  ('IT',        'Budi',  12000000.00),
  ('IT',        'Siti',  15000000.00),
  ('IT',        'Andi',  15000000.00),
  ('IT',        'Rina',   9000000.00),
  ('Keuangan',  'Dewi',  11000000.00),
  ('Keuangan',  'Tono',  13000000.00),
  ('Keuangan',  'Sari',   9000000.00),
  ('Pemasaran', 'Joko',   8500000.00),
  ('Pemasaran', 'Nurul', 12500000.00),
  ('Pemasaran', 'Rizky',  9500000.00);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 10 rows affected (0.00 sec)
Records: 10  Duplicates: 0  Warnings: 0

Urutan gaji dari tertinggi akan berbunyi 15000000, 15000000, 13000000, 12500000, 12000000, 11000000, 9500000, 9000000, 9000000, 8500000. Posisi pasangan Siti–Andi di puncak dan Rina–Sari di dasar memberi kita dua titik pengamatan perilaku tiap fungsi: di awal daftar dan menjelang akhir.

Tiga Fungsi Berdampingan #

Cara terbaik memahami ketiganya adalah menjalankannya pada data yang sama dalam satu query. Ketiga kolom memakai spesifikasi jendela yang sama — ORDER BY gaji DESC — sehingga satu-satunya pembeda adalah perilaku fungsinya:

sql
SELECT nama, departemen, gaji,
       ROW_NUMBER() OVER (ORDER BY gaji DESC, nama) AS nomor_urut,
       RANK()       OVER (ORDER BY gaji DESC)       AS peringkat,
       DENSE_RANK() OVER (ORDER BY gaji DESC)       AS peringkat_padat
FROM karyawan
ORDER BY gaji DESC, nama;

Output yang Dihasilkan:

text
+-------+------------+-------------+------------+-----------+-----------------+
| nama  | departemen | gaji        | nomor_urut | peringkat | peringkat_padat |
+-------+------------+-------------+------------+-----------+-----------------+
| Andi  | IT         | 15000000.00 |          1 |         1 |               1 |
| Siti  | IT         | 15000000.00 |          2 |         1 |               1 |
| Tono  | Keuangan   | 13000000.00 |          3 |         3 |               2 |
| Nurul | Pemasaran  | 12500000.00 |          4 |         4 |               3 |
| Budi  | IT         | 12000000.00 |          5 |         5 |               4 |
| Dewi  | Keuangan   | 11000000.00 |          6 |         6 |               5 |
| Rizky | Pemasaran  |  9500000.00 |          7 |         7 |               6 |
| Rina  | IT         |  9000000.00 |          8 |         8 |               7 |
| Sari  | Keuangan   |  9000000.00 |          9 |         8 |               7 |
| Joko  | Pemasaran  |  8500000.00 |         10 |        10 |               8 |
+-------+------------+-------------+------------+-----------+-----------------+
10 rows in set (0.00 sec)

Mari kita baca hasilnya titik demi titik pada puncak daftar. Siti dan Andi bergaji sama, maka RANK() memberi keduanya peringkat 1 — juara bersama. Baris berikutnya, Tono, menerima peringkat 3: posisinya memang orang ketiga dalam daftar, sehingga peringkat 2 terlewati dan celah terbentang — pola 1, 1, 3. DENSE_RANK() merapatkan celah itu: Siti dan Andi sama-sama 1, lalu Tono langsung 2 — pola 1, 1, 2, karena yang dihitung adalah jumlah peringkat berbeda di depannya, bukan jumlah orang. ROW_NUMBER() mengabaikan seri sepenuhnya: kunci kedua nama memaksa Andi 1 dan Siti 2, nomor yang tidak pernah sama.

Pengamatan kedua di dasar daerah memberi konfirmasi: Rina dan Sari sama-sama bergaji 9000000.00. RANK() memberi keduanya 8 — ada tujuh orang di depan — dan Joko setelahnya menerima 10, melompati 9. DENSE_RANK() memberi Rina dan Sari 7, lalu Joko 8. ROW_NUMBER() kembali membedakan keduanya menjadi 8 dan 9. Pola ketiganya pada data ini kita rangkum dalam tabel berikut:

SituasiROW_NUMBERRANKDENSE_RANK
Dua baris seri di posisi 11 dan 2 (unik)1 dan 11 dan 1
Baris tepat setelahnya33 (celah: 2 terlewati)2 (tanpa celah)
Dua baris seri di posisi 88 dan 98 dan 87 dan 7
Baris tepat setelahnya1010 (celah: 9 terlewati)8 (tanpa celah)
Nilai terbesar yang dihasilkanjumlah barisjumlah barisjumlah nilai berbeda

Perhatikan pula perbedaan gaya jendela pada kolom nomor_urut: fungsi tersebut memakai ORDER BY gaji DESC, nama dengan kunci kedua, sedangkan RANK() dan DENSE_RANK() cukup ORDER BY gaji DESC. Peringkat hasil kedua fungsi terakhir tidak terpengaruh urutan antarbaris yang seri — nilainya sama bagaimanapun MySQL menata mereka — sehingga kunci pemecah seri tidak diperlukan untuk kepastian angka, hanya untuk kepastian tampilan baris yang dijamin klausa ORDER BY gaji DESC, nama di akhir query.

Filter Tiga Besar: Dua Interpretasi #

Kita buktikan perbedaan makna tadi dengan memfilter papan juara. Keduanya memakai pola bungkus-CTE dari chapter ROW_NUMBER, karena hasil window function tetap tidak bisa disaring langsung di WHERE:

sql
WITH papan AS (
  SELECT nama, departemen, gaji,
         RANK() OVER (ORDER BY gaji DESC) AS peringkat
  FROM karyawan
)
SELECT nama, departemen, gaji, peringkat
FROM papan
WHERE peringkat <= 3
ORDER BY peringkat, nama;

Output yang Dihasilkan:

text
+------+------------+-------------+-----------+
| nama | departemen | gaji        | peringkat |
+------+------------+-------------+-----------+
| Andi | IT         | 15000000.00 |         1 |
| Siti | IT         | 15000000.00 |         1 |
| Tono | Keuangan   | 13000000.00 |         3 |
+------+------------+-------------+-----------+
3 rows in set (0.00 sec)
sql
WITH papan AS (
  SELECT nama, departemen, gaji,
         DENSE_RANK() OVER (ORDER BY gaji DESC) AS peringkat_padat
  FROM karyawan
)
SELECT nama, departemen, gaji, peringkat_padat
FROM papan
WHERE peringkat_padat <= 3
ORDER BY peringkat_padat, nama;

Output yang Dihasilkan:

text
+-------+------------+-------------+-----------------+
| nama  | departemen | gaji        | peringkat_padat |
+-------+------------+-------------+-----------------+
| Andi  | IT         | 15000000.00 |               1 |
| Siti  | IT         | 15000000.00 |               1 |
| Tono  | Keuangan   | 13000000.00 |               2 |
| Nurul | Pemasaran  | 12500000.00 |               3 |
+-------+------------+-------------+-----------------+
4 rows in set (0.00 sec)

Versi RANK() mengeluarkan tiga orang: Siti, Andi, dan Tono — juara satu berdua ditambah satu orang di posisi fisik ketiga, persis tiga tiket habis. Versi DENSE_RANK() mengeluarkan empat orang: Siti, Andi, Tono, dan Nurul — tiga tingkat peringkat teratas yang isinya kebetulan empat orang. Tidak ada yang salah di antara keduanya; keduanya menjawab pertanyaan yang berbeda. Papan juara kompetisi yang medali juara satu cukup satu biasanya memakai RANK(), sedangkan pemadatan kelas atau pengelompokan kuartil kinerja biasanya memakai DENSE_RANK() karena jumlah tingkatnya terkendali.

Peringkat Teratas per Grup #

Kombinasi PARTITION BY menghadirkan papan juara per departemen. Pola yang dipakai sama dengan top-N per grup, hanya fungsinya berganti:

sql
WITH papan AS (
  SELECT nama, departemen, gaji,
         RANK() OVER (PARTITION BY departemen ORDER BY gaji DESC) AS peringkat
  FROM karyawan
)
SELECT nama, departemen, gaji, peringkat
FROM papan
WHERE peringkat = 1
ORDER BY departemen;

Output yang Dihasilkan:

text
+-------+------------+-------------+-----------+
| nama  | departemen | gaji        | peringkat |
+-------+------------+-------------+-----------+
| Siti  | IT         | 15000000.00 |         1 |
| Andi  | IT         | 15000000.00 |         1 |
| Tono  | Keuangan   | 13000000.00 |         1 |
| Nurul | Pemasaran  | 12500000.00 |         1 |
+-------+------------+-------------+-----------+
4 rows in set (0.00 sec)

Empat baris keluar padahal hanya ada tiga departemen. Peringkat tertinggi IT dipegang Siti dan Andi sekaligus karena keduanya seri — RANK() memberi keduanya 1, dan filter peringkat = 1 memasukkan keduanya. Inilah perbedaan nyata terhadap pendekatan ROW_NUMBER() = 1 dari chapter sebelumnya: fungsi penomoran memilih satu orang saja, sedangkan RANK() memasukkan seluruh baris yang setara. Untuk laporan “karyawan bergaji tertinggi di tiap departemen”, jujur menyertakan kedua orang setara adalah jawaban yang benar; untuk kebutuhan “satu wakil per departemen” — misalnya satu nominasi penghargaan — ROW_NUMBER() lebih tepat karena hasilnya pasti tunggal.

Membersihkan Database Latihan #

Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:

sql
DROP DATABASE belajar_window;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

Referensi #

Peringkat kini punya tiga pilihan dengan karakter jelas: unik (ROW_NUMBER), celah (RANK), atau padat (DENSE_RANK). Ketiganya melihat posisi baris terhadap baris lain, namun belum ada yang melihat nilai baris tetangga secara langsung. Chapter berikutnya mengisi kekosongan itu lewat LAG() dan LEAD() — fungsi yang mengambil nilai baris sebelum dan sesudahnya, fondasi analisis tren deret waktu seperti pertumbuhan penjualan hari ke hari.