I66๐Top-N & Deduplikasiโ
โ
โMenengahTulis Query
Login Terakhir Tiap User
#row-number#dedup#latest-per-key#window
Pertanyaan
"Kita punya tabel
loginyang mencatat setiap kali user masuk ke aplikasi. Tampilkan login terakhir tiap user, lengkap dengan perangkat yang dipakai saat login itu."
Tabel login:
| login_id | user_id | waktu_login | perangkat |
|---|---|---|---|
| 1 | U01 | 2026-09-01 08:15:00 | android |
| 2 | U02 | 2026-09-01 09:40:00 | web |
| 3 | U01 | 2026-09-03 21:05:00 | web |
| 4 | U03 | 2026-09-02 07:30:00 | ios |
| 5 | U02 | 2026-09-04 12:00:00 | android |
| 6 | U01 | 2026-09-03 21:05:00 | android |
Jawaban Singkat
Beri nomor urut login per user dengan ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY waktu_login DESC), lalu ambil baris yang nomornya 1. MAX(waktu_login) saja tidak cukup karena kolom lain (perangkat) tidak ikut terbawa. Kalau ada dua login di detik yang sama, tambahkan kolom pemecah seri seperti login_id di ORDER BY supaya hasilnya deterministik.
Pembahasan
- Kenapa bukan
GROUP BY+MAX?SELECT user_id, MAX(waktu_login) FROM login GROUP BY user_idmemang memberi waktu login terakhir, tapi begitu kamu butuh kolom lain (perangkat), kolom itu harus di-agregasi juga.MAX(perangkat)akan memberi nilai abjad terbesar, bukan perangkat dari login terakhir. - Pakai window function.
ROW_NUMBER()memberi nomor 1, 2, 3, ... per user (PARTITION BY user_id), diurutkan dari login paling baru (ORDER BY waktu_login DESC). - Pecah seri. U01 punya dua login di
2026-09-03 21:05:00. Tanpa pemecah seri, database bebas memilih salah satu, dan hasilnya bisa berubah tiap dijalankan. Tambahkanlogin_id DESCsebagai kunci kedua. - Filter di luar. Window function tidak bisa dipakai langsung di
WHERE, jadi bungkus dulu dengan CTE, baru filterrn = 1.
Query
WITH urut AS ( SELECT user_id, waktu_login, perangkat, ROW_NUMBER() OVER ( PARTITION BY user_id ORDER BY waktu_login DESC, login_id DESC ) AS rn FROM login ) SELECT user_id, waktu_login, perangkat FROM urut WHERE rn = 1 ORDER BY user_id;
Contoh Output
| user_id | waktu_login | perangkat |
|---|---|---|
| U01 | 2026-09-03 21:05:00 | android |
| U02 | 2026-09-04 12:00:00 | android |
| U03 | 2026-09-02 07:30:00 | ios |
Jebakan Umum
- Pakai
MAX(perangkat)bersamaMAX(waktu_login). Dua agregasi itu independen, jadi perangkat yang keluar belum tentu dari login yang sama. - Lupa pemecah seri. Hasil jadi tidak deterministik saat ada timestamp kembar.
- Pakai
RANK()atauDENSE_RANK(). Keduanya memberi angka 1 untuk semua baris yang seri, jadi U01 muncul dua kali. - Menaruh
ROW_NUMBER() ... = 1langsung diWHERE. Error, karenaWHEREdievaluasi sebelum window function.
Follow-up dari Interviewer
- "Bisa tanpa window function?" Bisa: join balik ke subquery
MAX(waktu_login)per user. Tapi kalau ada waktu kembar, hasilnya tetap dobel, jadi butuh langkah dedup tambahan. - "Kalau mau 3 login terakhir?" Ganti filter jadi
rn <= 3. - "Tabelnya 2 miliar baris, apa yang kamu perhatikan?" Filter rentang tanggal dulu (misalnya 90 hari terakhir) supaya partisi window lebih kecil, dan pastikan tabel dipartisi atau di-cluster berdasarkan tanggal.
- "User yang belum pernah login perlu muncul?" Mulai dari tabel
penggunalaluLEFT JOINke hasil ini.
Yang Sebenarnya Diuji
Pola "baris terbaru per key" adalah salah satu pola yang paling sering muncul di interview. Interviewer ingin melihat kamu tahu batas GROUP BY, paham PARTITION BY, dan sadar soal seri.
Catatan Dialect
- DuckDB, BigQuery, dan Snowflake mendukung
QUALIFY, jadi CTE bisa dipersingkat:SELECT ... FROM login QUALIFY ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY waktu_login DESC, login_id DESC) = 1. - PostgreSQL punya
SELECT DISTINCT ON (user_id) ... ORDER BY user_id, waktu_login DESC. - MySQL butuh versi 8.0 ke atas untuk window function.