TL;DR
Soal interview SQL bertema produk nguji dua hal sekaligus: kemampuan nulis query dan pemahaman metrik produk kayak retensi, funnel, dan adopsi fitur. Pewawancara biasanya lebih peduli sama definisi metrik yang kamu pilih daripada kerapian sintaksnya. Delapan soal di artikel ini mencakup pola yang paling sering muncul di wawancara data analyst startup.
Soal interview SQL bertema produk nguji dua hal sekaligus: kemampuan nulis query dan pemahaman metrik produk.
Yang bikin kandidat gagal biasanya bukan sintaks. Mereka nulis query yang jalan, tapi ngukur hal yang salah.
Delapan soal di bawah pakai skema yang sama, jadi kamu bisa latihan berurutan. Tiap soal ada query jawaban, jebakannya, dan satu kalimat penutup yang bikin nilaimu naik.
Semua soal pakai database aplikasi pesan antar makanan Warungku. Empat tabel, cukup buat hampir semua pertanyaan produk.
users (user_id, signup_date, kota, sumber_akuisisi)
events (event_id, user_id, event_name, event_time)
orders (order_id, user_id, order_time, gmv, status)
eksperimen (user_id, grup, assigned_at)
Nilai event_name yang tersedia: buka_app, lihat_menu, masuk_keranjang, checkout, bayar_sukses, pakai_dompet.
Semua query di bawah pakai sintaks PostgreSQL.
Stickiness adalah rata-rata DAU dibagi MAU. Angka ini ngasih tau berapa hari dalam sebulan seorang pengguna rata-rata buka aplikasi.
WITH dau AS (
SELECT DATE_TRUNC('day', event_time)::date AS tanggal,
COUNT(DISTINCT user_id) AS jml
FROM events
WHERE event_time >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY 1
),
mau AS (
SELECT COUNT(DISTINCT user_id) AS jml
FROM events
WHERE event_time >= CURRENT_DATE - INTERVAL '30 days'
)
SELECT ROUND(AVG(dau.jml) / MAX(mau.jml), 3) AS stickiness
FROM dau CROSS JOIN mau;
Jebakannya: ngitung MAU dengan njumlahin DAU harian. Pengguna yang buka aplikasi 10 hari bakal kehitung 10 kali. MAU harus pakai COUNT DISTINCT di seluruh periode.
Kalimat penutup: "Stickiness 0,27 artinya rata-rata pengguna aktif buka aplikasi sekitar 8 hari dalam sebulan. Buat aplikasi makanan itu wajar, buat aplikasi chat itu sinyal bahaya."
Ini soal paling sering keluar. Pewawancara mau lihat kamu ngerti konsep kohor.
WITH kohor AS (
SELECT user_id, signup_date::date AS tgl_daftar
FROM users
WHERE signup_date >= DATE '2025-08-01'
AND signup_date < DATE '2025-09-01'
),
aktif_d7 AS (
SELECT DISTINCT k.user_id
FROM kohor k
JOIN events e ON e.user_id = k.user_id
WHERE e.event_time::date = k.tgl_daftar + 7
)
SELECT
COUNT(DISTINCT k.user_id) AS pengguna_baru,
COUNT(DISTINCT a.user_id) AS aktif_hari_ke7,
ROUND(100.0 * COUNT(DISTINCT a.user_id)
/ COUNT(DISTINCT k.user_id), 1) AS retensi_d7_persen
FROM kohor k
LEFT JOIN aktif_d7 a ON a.user_id = k.user_id;
Jebakannya: pakai tanggal absolut. Kalau kamu tulis event_time = DATE '2025-08-08', kamu cuma ngukur orang yang daftar tanggal 1. Hari ke-7 dihitung dari tanggal daftar masing-masing pengguna.
Yang bikin nilai naik: sebut bedanya retensi harian persis dan retensi rentang. Query di atas ngukur aktif tepat di hari ke-7. Banyak perusahaan pakai definisi "aktif di hari 7 sampai 13" karena angkanya lebih stabil. Tanya dulu definisi mana yang mereka pakai.
Kalau kamu belum akrab sama cara baca tabel kohor, mulai dari cohort analysis.
SELECT
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'buka_app') AS buka_app,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'lihat_menu') AS lihat_menu,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'masuk_keranjang') AS keranjang,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'checkout') AS checkout,
COUNT(DISTINCT user_id) FILTER (WHERE event_name = 'bayar_sukses') AS bayar_sukses
FROM events
WHERE event_time >= DATE '2025-09-01'
AND event_time < DATE '2025-10-01';
Klausa FILTER di PostgreSQL bikin query funnel jauh lebih pendek dari tumpukan CASE WHEN. Detailnya ada di dokumentasi fungsi agregat PostgreSQL.
Jebakannya: query ini nggak mastiin urutan. Pengguna yang checkout tanpa pernah lihat menu tetap kehitung. Buat funnel ketat, kamu perlu bandingin timestamp antar tahap per pengguna.
Kalimat penutup: "Angka ini funnel longgar. Kalau tim produk butuh urutan ketat, aku bakal pakai window function buat mastiin tiap event terjadi setelah event sebelumnya."
WITH aktif AS (
SELECT DISTINCT user_id FROM events
WHERE event_time >= DATE '2025-09-01'
AND event_time < DATE '2025-09-08'
),
pemakai AS (
SELECT DISTINCT user_id FROM events
WHERE event_name = 'pakai_dompet'
AND event_time >= DATE '2025-09-01'
AND event_time < DATE '2025-09-08'
)
SELECT
(SELECT COUNT(*) FROM aktif) AS pengguna_aktif,
(SELECT COUNT(*) FROM pemakai) AS pemakai_fitur,
ROUND(100.0 * (SELECT COUNT(*) FROM pemakai)
/ (SELECT COUNT(*) FROM aktif), 1) AS adopsi_persen;
Jebakannya: milih penyebut yang salah. Kalau kamu bagi sama total pengguna terdaftar, angkanya kecil banget dan nggak berarti. Adopsi fitur diukur dari pengguna yang beneran buka aplikasi di periode itu.
Yang bikin nilai naik: tanyain balik apakah fitur dompet ada di semua versi aplikasi. Kalau cuma tersedia di Android, penyebutnya harus dibatasi ke pengguna Android.
WITH pertama AS (
SELECT u.user_id,
u.signup_date,
MIN(o.order_time) AS order_pertama
FROM users u
JOIN orders o ON o.user_id = u.user_id
WHERE o.status = 'sukses'
GROUP BY u.user_id, u.signup_date
)
SELECT
ROUND(PERCENTILE_CONT(0.5) WITHIN GROUP (
ORDER BY EXTRACT(EPOCH FROM (order_pertama - signup_date)) / 3600
)::numeric, 1) AS median_jam
FROM pertama;
Jebakannya: pakai AVG. Beberapa pengguna baru order setelah 8 bulan, dan mereka narik rata-rata jauh ke atas. Median lebih jujur buat data waktu yang miring, dan alasannya sama kayak kenapa kamu hati-hati sama outlier.
Kalimat penutup: "Angka ini cuma buat pengguna yang akhirnya order. Kalau tim mau tau kesehatan onboarding, aku juga bakal laporin berapa persen pengguna yang nggak pernah order sama sekali."
SELECT
u.kota,
COUNT(DISTINCT u.user_id) AS jml_pengguna,
SUM(o.gmv) AS total_gmv,
ROUND(SUM(o.gmv) / COUNT(DISTINCT u.user_id)) AS gmv_per_pengguna
FROM users u
JOIN orders o ON o.user_id = u.user_id
WHERE o.status = 'sukses'
AND o.order_time >= DATE '2025-09-01'
GROUP BY u.kota
HAVING COUNT(DISTINCT u.user_id) >= 50
ORDER BY gmv_per_pengguna DESC
LIMIT 5;
Jebakannya: lupa HAVING. Kota dengan 3 pengguna yang kebetulan satu di antaranya pesan katering kantor bakal nangkring di puncak. Ambang minimal jumlah pengguna wajib ada.
Yang bikin nilai naik: sebutin bahwa GMV per pengguna bukan KPI yang bisa berdiri sendiri. Kota dengan GMV per pengguna tinggi tapi jumlah pengguna kecil punya arti bisnis yang beda dari kota besar dengan angka sedang.
SELECT COUNT(*) AS pengguna_churn
FROM (
SELECT user_id, MAX(order_time) AS order_terakhir
FROM orders
WHERE status = 'sukses'
GROUP BY user_id
) t
WHERE order_terakhir < CURRENT_DATE - INTERVAL '30 days';
Jebakannya: nulis query sebelum nanya definisi churn. Di aplikasi makanan, 30 hari tanpa order itu wajar buat sebagian orang. Di aplikasi langganan, 30 hari itu udah pasti hilang.
Kalimat penutup: "Aku pakai 30 hari sebagai batas. Kalau tim punya definisi lain, angkanya tinggal diganti di satu tempat. Yang lebih berguna sebenernya laporan churn per kohor, biar kelihatan pengguna angkatan mana yang paling cepat pergi."
SELECT
x.grup,
COUNT(DISTINCT x.user_id) AS jml_pengguna,
COUNT(DISTINCT o.user_id) AS jml_konversi,
ROUND(100.0 * COUNT(DISTINCT o.user_id)
/ COUNT(DISTINCT x.user_id), 2) AS konversi_persen
FROM eksperimen x
LEFT JOIN orders o
ON o.user_id = x.user_id
AND o.status = 'sukses'
AND o.order_time >= x.assigned_at
GROUP BY x.grup;
Jebakannya: pakai INNER JOIN. Pengguna yang nggak pernah order bakal hilang dari penyebut, dan konversi kedua grup jadi 100%.
Jebakan kedua: nggak ngasih syarat order_time >= assigned_at. Order yang terjadi sebelum pengguna masuk eksperimen nggak boleh dihitung.
Yang bikin nilai naik: bilang bahwa selisih persentase doang belum cukup buat nyimpulin pemenang. Kamu butuh uji signifikansi dan ukuran sampel yang memadai, dan itu bagian standar dari A/B testing.
| Pola | Muncul di soal | Fungsi kunci |
|---|---|---|
| Hitung pengguna unik | 1, 3, 4 | COUNT DISTINCT |
| Kohor berbasis tanggal daftar | 2 | Tanggal relatif per pengguna |
| Agregat bersyarat | 3 | FILTER atau CASE WHEN |
| Kejadian pertama per pengguna | 5 | MIN atau ROW_NUMBER |
| Rasio dengan ambang minimal | 6 | HAVING |
| Pengguna tanpa aktivitas | 7, 8 | LEFT JOIN |
Kalau kamu kuasai enam pola itu, sebagian besar soal wawancara SQL produk bisa kamu kerjain. Sisanya cuma variasi konteks.
status = 'sukses' hampir selalu perlu.Boleh, dan justru itu yang diharapkan. Tanya nama kolom kunci, tipe datanya, dan apakah ada baris duplikat. Pewawancara sering sengaja ngasih soal setengah jelas buat lihat apakah kamu asal nulis query atau mastiin asumsinya dulu. Dua sampai tiga pertanyaan di awal biasanya cukup.
Bilang aja apa yang mau kamu capai, lalu tulis pendekatannya. Contohnya, "aku mau ambil median, di PostgreSQL biasanya pakai PERCENTILE_CONT, kalau salah sintaks tolong dikoreksi". Pewawancara nilai logikanya, bukan hafalanmu.
Pakai CTE. Blok WITH bikin query gampang dibaca dan gampang dijelasin langkah demi langkah, dan itu penting waktu kamu ngomong sambil nulis. Subquery bertumpuk susah diikuti pewawancara di layar kecil.
Soal SQL biasa nguji sintaks: JOIN, GROUP BY, window function. Soal SQL produk nambahin lapisan definisi metrik: apa itu pengguna aktif, kapan seseorang dianggap churn, tanggal mana yang dipakai buat kohor. Query-nya sering lebih pendek, tapi asumsinya lebih banyak.
Minimal kenal ROW_NUMBER, LAG, dan SUM OVER. Tiga itu nutup sebagian besar kebutuhan: ambil kejadian pertama per pengguna, bandingin nilai hari ini sama kemarin, dan hitung total berjalan. Fungsi lain jarang jadi penentu di level junior sampai mid.
Query yang jalan itu syarat minimum. Yang bikin kamu lolos adalah definisi metrik yang kamu pilih dan alasan di baliknya.
Latihan paling efektif: ambil satu soal di atas, tulis query-nya sambil ngomong keras, lalu tambahin satu kalimat soal keterbatasan angkamu.
Lanjut ke tahap berikutnya lewat transkrip mock interview studi kasus bisnis, atau langsung latihan nulis query di NgulikSQL tanpa perlu install database.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Kolom waktu isinya angka gede kayak 1704067200 dan bikin bingung? Itu Unix timestamp. Ini cara ngubahnya jadi tanggal beneran di SQL, plus balik lagi.
Data transaksi tersimpan UTC, tapi laporan harus jam WIB. Kalau salah konversi, angka penjualan tengah malam bisa kecatat di tanggal yang salah. Ini cara handle timezone di SQL dengan benar.
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.