Banyak laporan menuntut nomor urut: nomor antrian, urutan peserta lomba, atau posisi produk terlaris. Kolom AUTO_INCREMENT bisa menipu di sini karena nilainya melekat pada baris sejak insert dan tidak pernah berubah mengikuti urutan query. Fungsi ROW_NUMBER() menomori baris saat query berjalan — dimulai dari 1 mengikuti ORDER BY yang kita tentukan di dalam jendela, dan reset ke 1 di setiap partisi. Pada chapter ini kita berlatih penomoran global dan per kelompok, membangun pola terpenting di seluruh section ini — top-N per grup — lalu memakai ROW_NUMBER() untuk deduplikasi baris dan menutup dengan perbandingan terhadap AUTO_INCREMENT.
Menyiapkan Tabel Latihan #
Kita pakai database latihan belajar_window dengan tabel karyawan sembilan baris dari tiga departemen. Ada satu pasang gaji yang sama persis — Siti dan Andi sama-sama 15000000.00 — untuk melihat bagaimana penomoran menyikapi nilai seri:
DROP DATABASE IF EXISTS belajar_window;
CREATE DATABASE belajar_window;
USE belajar_window;Output yang Dihasilkan:
Query OK, 0 rows affected, 1 warning (0.00 sec)
Query OK, 1 row affected (0.00 sec)
Database changedCREATE 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', 'Siti', 15000000.00),
('IT', 'Andi', 15000000.00),
('IT', 'Budi', 12000000.00),
('Keuangan', 'Tono', 13000000.00),
('Keuangan', 'Dewi', 11000000.00),
('Keuangan', 'Sari', 9500000.00),
('Pemasaran', 'Nurul', 12500000.00),
('Pemasaran', 'Rizky', 10500000.00),
('Pemasaran', 'Joko', 9000000.00);Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 9 rows affected (0.00 sec)
Records: 9 Duplicates: 0 Warnings: 0Kolom id sengaja kita tampilkan pada beberapa contoh agar perbedaan nomor urut query dan identitas fisik baris terlihat langsung. Nilai gaji unik kecuali pasangan Siti–Andi, sehingga kita perlu kunci kedua untuk memecah seri secara deterministik — kebiasaan penulisan yang akan selalu kita pakai pada fungsi ranking.
Nomor Urut Sederhana #
Bentuk paling dasar memberi nomor 1 sampai n pada seluruh hasil, urut menurut kunci yang kita pilih:
SELECT id, nama, departemen, gaji,
ROW_NUMBER() OVER (ORDER BY gaji DESC, nama) AS nomor_urut
FROM karyawan
ORDER BY nomor_urut;Output yang Dihasilkan:
+----+-------+------------+-------------+------------+
| id | nama | departemen | gaji | nomor_urut |
+----+-------+------------+-------------+------------+
| 2 | Andi | IT | 15000000.00 | 1 |
| 1 | Siti | IT | 15000000.00 | 2 |
| 4 | Tono | Keuangan | 13000000.00 | 3 |
| 7 | Nurul | Pemasaran | 12500000.00 | 4 |
| 3 | Budi | IT | 12000000.00 | 5 |
| 5 | Dewi | Keuangan | 11000000.00 | 6 |
| 8 | Rizky | Pemasaran | 10500000.00 | 7 |
| 6 | Sari | Keuangan | 9500000.00 | 8 |
| 9 | Joko | Pemasaran | 9000000.00 | 9 |
+----+-------+------------+-------------+------------+
9 rows in set (0.00 sec)Sembilan baris keluar bernomor 1 sampai 9 mengikuti gaji tertinggi. Mari kita bedah dua detail pentingnya: ORDER BY gaji DESC, nama di dalam OVER memerintahkan penomoran turun menurut gaji, dengan nama sebagai kunci kedua — karena Siti dan Andi bergaji sama, nama-lah yang memutuskan Andi mendapat nomor 1 dan Siti nomor 2; tanpa kunci kedua, urutan kedua baris ini ditentukan kebetulan internal MySQL dan hasilnya tidak dijamin konsisten. Kolom id menunjukkan fakta menarik kedua: dua baris teratas (Andi dan Siti) justru menyimpan id 2 dan 1 — nomor urut mengikuti urutan gaji, bukan urutan penyimpanan.
Nomor Urut per Kelompok #
Menambahkan PARTITION BY membuat penomoran berjalan mandiri di setiap kelompok — setiap departemen mendapat hitungan sendiri yang dimulai dari 1:
SELECT nama, departemen, gaji,
ROW_NUMBER() OVER (PARTITION BY departemen ORDER BY gaji DESC, nama) AS nomor_dept
FROM karyawan
ORDER BY departemen, nomor_dept;Output yang Dihasilkan:
+-------+------------+-------------+------------+
| nama | departemen | gaji | nomor_dept |
+-------+------------+-------------+------------+
| Andi | IT | 15000000.00 | 1 |
| Siti | IT | 15000000.00 | 2 |
| Budi | IT | 12000000.00 | 3 |
| Tono | Keuangan | 13000000.00 | 1 |
| Dewi | Keuangan | 11000000.00 | 2 |
| Sari | Keuangan | 9500000.00 | 3 |
| Nurul | Pemasaran | 12500000.00 | 1 |
| Rizky | Pemasaran | 10500000.00 | 2 |
| Joko | Pemasaran | 9000000.00 | 3 |
+-------+------------+-------------+------------+
9 rows in set (0.00 sec)Tiga hitungan berjalan bersamaan: IT bernomor 1 sampai 3 (Andi, Siti, Budi), Keuangan 1 sampai 3 (Tono, Dewi, Sari), dan Pemasaran 1 sampai 3 (Nurul, Rizky, Joko). Reset ke 1 terjadi otomatis setiap kali MySQL pindah jendela — perilaku yang membuat ROW_NUMBER() cocok untuk kebutuhan seperti “urutan keberapa produk ini di kotanya”. Jumlah total nomor yang terpakai tetap sembilan, karena tiap partisi menomori anggotanya sendiri.
Pola Top-N per Grup #
Inilah pola paling penting chapter ini: mengambil N baris teratas dari setiap kelompok — dua bergaji tertinggi di tiap departemen, produk terlaris di tiap kota, transaksi terbesar tiap bulan. Intuisi pertama biasanya menulis WHERE langsung pada hasil penomoran:
-- @error
SELECT nama, departemen, gaji, ROW_NUMBER() OVER (ORDER BY gaji DESC) AS nomor_urut FROM karyawan WHERE nomor_urut <= 2;Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
ERROR 1054 (42S22): Unknown column 'nomor_urut' in 'where clause'
ByeServer menolak dengan ERROR 1054 karena nomor_urut tidak dikenal di WHERE. Penyebabnya adalah urutan evaluasi query: MySQL memproses FROM, lalu WHERE, lalu SELECT, baru kemudian ORDER BY. Window function dihitung pada tahap SELECT — setelah WHERE selesai — sehingga klausa WHERE tidak pernah melihat kolom hasilnya. Solusinya adalah membungkus penomoran lebih dulu, lalu menyaring hasil bungkusan itu dari luar. Kita pakai CTE yang sudah kita kuasai di section set-operations:
WITH peringkat AS (
SELECT departemen, nama, gaji,
ROW_NUMBER() OVER (PARTITION BY departemen ORDER BY gaji DESC, nama) AS nomor_dept
FROM karyawan
)
SELECT departemen, nama, gaji
FROM peringkat
WHERE nomor_dept <= 2
ORDER BY departemen, nomor_dept;Output yang Dihasilkan:
+------------+-------+-------------+
| departemen | nama | gaji |
+------------+-------+-------------+
| IT | Andi | 15000000.00 |
| IT | Siti | 15000000.00 |
| Keuangan | Tono | 13000000.00 |
| Keuangan | Dewi | 11000000.00 |
| Pemasaran | Nurul | 12500000.00 |
| Pemasaran | Rizky | 10500000.00 |
+------------+-------+-------------+
6 rows in set (0.00 sec)Enam baris keluar — tepat dua per departemen. Mari kita bedah polanya langkah demi langkah, karena pola inilah yang akan dipakai berulang-ulang sepanjang analisis data:
- Baris 1 —
WITH peringkat AS (: mendeklarasikan CTE bernamaperingkatsebagai hasil antara yang bisa kita query seperti tabel. - Baris 2-3 —
ROW_NUMBER() OVER (PARTITION BY departemen ORDER BY gaji DESC, nama) AS nomor_dept: sembilan baris dinomori per departemen, mulai dari gaji tertinggi; kunci keduanamamenjaga determinisme pada pasangan Siti–Andi. - Baris 6-8 —
SELECT ... FROM peringkat WHERE nomor_dept <= 2: query luar berjalan setelah CTE selesai dihitung, sehinggaWHEREkini melihat kolomnomor_deptyang sudah terisi dan bisa menyaringnya.
Sekali bungkus, urutan evaluasi berbalik arah: penomoran dulu, penyaringan kemudian. Subquery di FROM (derived table) bekerja dengan cara yang sama — WITH dipilih karena namanya membuat maksud query lebih terbaca. Ganti angka 2 menjadi 1 dan pola ini berubah menjadi “tertinggi satu per departemen”; ganti kunci partisi dan urutan, pola yang sama berlaku untuk produk terlaris per kota atau transaksi terbesar per bulan.
ROW NUMBER vs AUTO_INCREMENT #
Perbedaan mendasar keduanya: AUTO_INCREMENT adalah identitas baris yang disimpan di tabel, sedangkan ROW_NUMBER() adalah nomor urut yang dihitung ulang setiap query. Kita buktikan dengan menambah satu karyawan bergaji tertinggi:
INSERT INTO karyawan (departemen, nama, gaji)
VALUES ('Pemasaran', 'Zahra', 16000000.00);Output yang Dihasilkan:
Query OK, 1 row affected (0.00 sec)SELECT id, nama, gaji,
ROW_NUMBER() OVER (ORDER BY gaji DESC, nama) AS nomor_urut
FROM karyawan
ORDER BY nomor_urut;Output yang Dihasilkan:
+----+-------+-------------+------------+
| id | nama | gaji | nomor_urut |
+----+-------+-------------+------------+
| 10 | Zahra | 16000000.00 | 1 |
| 2 | Andi | 15000000.00 | 2 |
| 1 | Siti | 15000000.00 | 3 |
| 4 | Tono | 13000000.00 | 4 |
| 7 | Nurul | 12500000.00 | 5 |
| 3 | Budi | 12000000.00 | 6 |
| 5 | Dewi | 11000000.00 | 7 |
| 8 | Rizky | 10500000.00 | 8 |
| 6 | Sari | 9500000.00 | 9 |
| 9 | Joko | 9000000.00 | 10 |
+----+-------+-------------+------------+
10 rows in set (0.00 sec)Zahra masuk sebagai id 10 — angka baru dari AUTO_INCREMENT yang tidak pernah dipakai ulang — namun menduduki nomor urut 1 karena gajinya kini tertinggi. Seluruh nomor urut karyawan lain bergeser turun satu, sementara id mereka tidak berubah sedikit pun. Kesimpulannya: gunakan AUTO_INCREMENT untuk identitas permanen baris (primary key), gunakan ROW_NUMBER() untuk posisi dalam konteks query tertentu. Nomor urut tidak disimpan, tidak bisa di-query kembali tanpa menghitung ulang, dan itulah kekuatannya — selalu segar mengikuti data terkini.
Deduplikasi Baris dengan Nomor 1 #
Tabel hasil impor data sering menyimpan baris ganda. ROW_NUMBER() memberi resep dedup yang rapi: nomori setiap kelompok baris identik, lalu pertahankan hanya nomor 1. Kita buat tabel peserta tanpa constraint unik yang menampung duplikat:
CREATE TABLE peserta (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL
);
INSERT INTO peserta (nama, email) VALUES
('Budi', '[email protected]'),
('Budi', '[email protected]'),
('Siti', '[email protected]'),
('Budi', '[email protected]'),
('Andi', '[email protected]'),
('Siti', '[email protected]'),
('Dewi', '[email protected]'),
('Rina', '[email protected]');Output yang Dihasilkan:
Query OK, 0 rows affected (0.00 sec)
Query OK, 8 rows affected (0.00 sec)
Records: 8 Duplicates: 0 Warnings: 0PARTITION BY nama, email mengelompokkan baris yang isinya sama persis, dan ORDER BY id memilih salinan paling awal sebagai nomor 1. Query luar menyaring nomor 1 sehingga yang tampil adalah satu wakil per kelompok:
SELECT id, nama, email
FROM (
SELECT id, nama, email,
ROW_NUMBER() OVER (PARTITION BY nama, email ORDER BY id) AS nomor
FROM peserta
) bernomor
WHERE nomor = 1
ORDER BY id;Output yang Dihasilkan:
+----+------+------------------+
| id | nama | email |
+----+------+------------------+
| 1 | Budi | [email protected] |
| 3 | Siti | [email protected] |
| 5 | Andi | [email protected] |
| 7 | Dewi | [email protected] |
| 8 | Rina | [email protected] |
+----+------+------------------+
5 rows in set (0.00 sec)Delapan baris menyusut menjadi lima — Budi muncul sekali, Siti sekali, dan tiga peserta lain memang tunggal. Subquery bernomor memainkan peran yang sama dengan CTE pada pola top-N: menyediakan kolom nomor sebelum WHERE query luar bekerja. Bila tabel harus benar-benar bersih, nomor tersebut bisa dipakai untuk menghapus salinan berlebih secara permanen:
DELETE d
FROM peserta d
JOIN (
SELECT id,
ROW_NUMBER() OVER (PARTITION BY nama, email ORDER BY id) AS nomor
FROM peserta
) t ON t.id = d.id AND t.nomor > 1;Output yang Dihasilkan:
Query OK, 3 rows affected (0.00 sec)SELECT id, nama, email
FROM peserta
ORDER BY id;Output yang Dihasilkan:
+----+------+------------------+
| id | nama | email |
+----+------+------------------+
| 1 | Budi | [email protected] |
| 3 | Siti | [email protected] |
| 5 | Andi | [email protected] |
| 7 | Dewi | [email protected] |
| 8 | Rina | [email protected] |
+----+------+------------------+
5 rows in set (0.00 sec)Statement DELETE meng-join tabel peserta dengan subquery bernomor, lalu menghapus baris yang nomornya lebih besar dari 1 — tiga baris ganda hilang, disisakan lima baris bersih. Kunci keberhasilan pola ini adalah adanya kolom pembeda seperti id: join penghapusan membutuhkan pengenal unik untuk membidik baris fisik yang tepat, sesuatu yang tidak dimiliki tabel tanpa primary key.
Membersihkan Database Latihan #
Rangkaian latihan selesai dan database latihan kita jatuhkan kembali:
DROP DATABASE belajar_window;Output yang Dihasilkan:
Query OK, 2 rows affected (0.00 sec)Referensi #
- Window Function Descriptions — MySQL 8.4 Reference Manual
- WITH (Common Table Expressions) — MySQL 8.4 Reference Manual
- Window Function Concepts and Syntax — MySQL 8.4 Reference Manual
ROW_NUMBER() kini menjadi senjata andalan: penomoran global, penomoran per kelompok, pola top-N per grup lewat CTE, hingga deduplikasi. Satu keterbatasan masih terasa — pasangan Siti dan Andi yang bergaji sama dipaksa mendapat nomor berbeda padahal keduanya setara. Chapter berikutnya memperkenalkan RANK() dan DENSE_RANK(), dua fungsi yang memperlakukan nilai seri secara jujur.