Gaji Tertinggi Kedua
Pertanyaan
"Cari gaji tertinggi kedua di perusahaan. Kalau ada beberapa orang dengan gaji tertinggi yang sama, gaji itu tetap dihitung satu. Kalau gaji tertinggi kedua tidak ada, hasilnya harus satu baris berisi NULL, bukan tabel kosong."
Tabel karyawan:
| karyawan_id | nama | gaji |
|---|---|---|
| 1 | Andi | 12000000 |
| 2 | Budi | 15000000 |
| 3 | Citra | 15000000 |
| 4 | Dewi | 9500000 |
| 5 | Eko | 12000000 |
| 6 | Fajar | NULL |
Jawaban Singkat
Beri peringkat gaji dengan DENSE_RANK() OVER (ORDER BY gaji DESC) supaya gaji yang sama dapat peringkat yang sama, lalu ambil peringkat 2. Bungkus dengan MAX(...) supaya kalau peringkat 2 tidak ada, hasilnya tetap satu baris berisi NULL. Gaji NULL dibuang dulu supaya tidak ikut diberi peringkat.
Pembahasan
- Kenapa
DENSE_RANK, bukanROW_NUMBERatauRANK? Budi dan Citra sama-sama 15 juta.ROW_NUMBERakan memberi Citra nomor 2, jadi "tertinggi kedua" keluar 15 juta (salah).RANKmemberi keduanya 1 lalu melompat ke 3, jadi peringkat 2 tidak ada sama sekali. HanyaDENSE_RANKyang memberi 15 juta = 1 dan 12 juta = 2. - Kenapa dibungkus
MAX?WHERE dr = 2saja akan mengembalikan nol baris kalau semua karyawan bergajinya sama. Fungsi agregat tanpaGROUP BYselalu mengembalikan tepat satu baris, danMAXdari himpunan kosong adalah NULL. Bonusnya, kalau ada dua orang di peringkat 2 (Andi dan Eko),MAXmeringkasnya jadi satu nilai. - Kenapa buang NULL? Di PostgreSQL, NULL dianggap nilai paling besar, jadi dengan
ORDER BY gaji DESCgaji NULL milik Fajar dapat peringkat 1 dan 15 juta turun ke peringkat 2. Hasilnya salah tanpa ada error.WHERE gaji IS NOT NULLmembuat query aman di semua engine.
Uji kasus "tidak ada nilai kedua": kalau tabel hanya berisi dua karyawan yang sama-sama bergaji 15 juta, query di bawah mengembalikan:
| gaji_tertinggi_kedua |
|---|
| NULL |
Sementara versi tanpa MAX (SELECT gaji FROM peringkat WHERE dr = 2) mengembalikan tabel kosong.
Query
WITH peringkat AS ( SELECT gaji, DENSE_RANK() OVER (ORDER BY gaji DESC) AS dr FROM karyawan WHERE gaji IS NOT NULL ) SELECT MAX(gaji) AS gaji_tertinggi_kedua FROM peringkat WHERE dr = 2;
Contoh Output
| gaji_tertinggi_kedua |
|---|
| 12000000 |
Jebakan Umum
ORDER BY gaji DESC LIMIT 1 OFFSET 1tanpaDISTINCT. Di data ini hasilnya 15 juta, karena baris kedua masih milik Citra yang gajinya sama dengan Budi.LIMIT 1 OFFSET 1denganDISTINCTsudah benar untuk seri, tapi mengembalikan tabel kosong saat nilai kedua tidak ada. Bungkus sebagai scalar subquery (SELECT (SELECT ... ) AS gaji_tertinggi_kedua) kalau mau tetap pakai cara ini.- Pakai
ROW_NUMBERatauRANK. Lihat pembahasan nomor 1. - Tidak memikirkan NULL. Di engine yang menaruh NULL paling atas saat
DESC, NULL bisa "mencuri" peringkat 1.
Follow-up dari Interviewer
- "Bisa tanpa window function?" Bisa:
SELECT MAX(gaji) FROM karyawan WHERE gaji < (SELECT MAX(gaji) FROM karyawan). Otomatis menangani seri dan NULL, dan otomatis mengembalikan NULL kalau tidak ada nilai kedua. Tapi cara ini susah diperluas ke ke-N. - "Kalau mau ke-N yang bisa diganti-ganti?" Pakai versi
DENSE_RANKdan gantidr = 2jadidr = N. Di PostgreSQL bisa dibungkus jadi function dengan parameter. - "Mau sekalian nama karyawannya?" Buang
MAX, pilihnama, gajidari CTE denganWHERE dr = 2. Hasilnya bisa lebih dari satu baris (Andi dan Eko), dan itu benar.
Yang Sebenarnya Diuji
Soal klasik ini menguji tiga hal sekaligus: paham beda fungsi ranking saat seri, tahu perilaku agregat pada himpunan kosong, dan cukup teliti memikirkan NULL. Banyak kandidat lolos poin pertama tapi lupa dua poin lainnya.
Catatan Dialect
- Query jalan tanpa perubahan di DuckDB, PostgreSQL, BigQuery, dan MySQL 8.0 ke atas.
- Di MySQL 5.7 (tanpa window function), pakai versi
MAX(gaji) WHERE gaji < (SELECT MAX(gaji) ...). - DuckDB dan BigQuery bisa memakai
QUALIFY DENSE_RANK() OVER (ORDER BY gaji DESC) = 2untuk versi yang menampilkan nama, tapiMAXtetap perlu kalau hasil kosong harus jadi NULL.