TL;DR
Latihan soal SQL pemula paling efektif kalau pakai satu dataset yang sama dari awal sampai akhir, biar kamu hafal strukturnya dan bisa fokus ke logikanya. Halaman ini berisi 50 soal bertingkat, dari SELECT dasar sampai window function, semuanya pakai dataset toko_berkah berisi transaksi UMKM retail Indonesia. Kunci jawaban disediakan lengkap, tapi pola belajar yang paling berhasil adalah nyoba 10 menit dulu sebelum ngintip.
Cara tercepat jago SQL bukan nonton tutorial. Nulis query sendiri, salah, benerin, ulang.
Di bawah ini 50 soal bertingkat yang aku susun dari yang paling gampang sampai yang bikin mikir. Semua pakai satu dataset yang sama, biar kamu nggak buang waktu ngapalin struktur tabel baru tiap soal.
Kunci jawabannya ada semua. Tapi janji dulu: coba 10 menit sendiri sebelum ngintip.
Dataset toko_berkah berisi 18.400 transaksi dari 6 cabang UMKM retail sepanjang 2025. Ada dua tabel.
Tabel transaksi:
| Kolom | Tipe | Keterangan |
|---|---|---|
| id_transaksi | INT | Primary key |
| tanggal | DATE | Tanggal transaksi |
| cabang | TEXT | Jakarta, Bekasi, Depok, Bandung, Surabaya, Semarang |
| id_pelanggan | INT | Foreign key ke tabel pelanggan |
| kategori | TEXT | Sembako, Snack, Minuman, Perawatan |
| jumlah_item | INT | Banyak barang dibeli |
| total_harga | INT | Nilai transaksi dalam rupiah |
Tabel pelanggan:
| Kolom | Tipe | Keterangan |
|---|---|---|
| id_pelanggan | INT | Primary key |
| nama | TEXT | Nama pelanggan |
| kota | TEXT | Kota domisili |
| tanggal_daftar | DATE | Kapan pertama daftar member |
-- 1
SELECT * FROM transaksi;
-- 2
SELECT tanggal, cabang, total_harga FROM transaksi;
-- 3
SELECT * FROM transaksi WHERE cabang = 'Bekasi';
-- 4
SELECT * FROM transaksi WHERE total_harga > 500000;
-- 5
SELECT * FROM transaksi
WHERE kategori = 'Sembako' AND cabang = 'Depok';
-- 6
SELECT * FROM transaksi
WHERE tanggal BETWEEN '2025-10-01' AND '2025-12-31';
-- 7
SELECT * FROM transaksi WHERE jumlah_item >= 5;
-- 8
SELECT * FROM transaksi
WHERE cabang IN ('Jakarta', 'Surabaya');
-- 9
SELECT * FROM transaksi
WHERE total_harga BETWEEN 100000 AND 300000;
-- 10
SELECT DISTINCT kategori FROM transaksi;
Soal 8 bisa juga pakai OR. Tapi IN lebih rapi kalau daftarnya panjang.
-- 11
SELECT * FROM transaksi ORDER BY total_harga DESC LIMIT 10;
-- 12
SELECT * FROM transaksi ORDER BY tanggal ASC LIMIT 5;
-- 13
SELECT * FROM transaksi ORDER BY cabang ASC, total_harga DESC;
-- 14
SELECT * FROM transaksi
WHERE cabang = 'Bandung'
ORDER BY total_harga DESC LIMIT 3;
-- 15
SELECT UPPER(nama) AS nama_kapital FROM pelanggan;
-- 16
SELECT * FROM pelanggan WHERE nama LIKE 'A%';
-- 17
SELECT * FROM pelanggan WHERE nama LIKE '%Sari%';
-- 18
SELECT nama, LENGTH(nama) AS panjang_nama FROM pelanggan;
-- 19
SELECT
id_transaksi,
total_harga,
jumlah_item,
ROUND(total_harga::numeric / NULLIF(jumlah_item, 0), 0)
AS harga_per_item
FROM transaksi;
-- 20
SELECT
id_transaksi,
ROUND(total_harga::numeric / NULLIF(jumlah_item, 0), 0)
AS harga_per_item
FROM transaksi
ORDER BY harga_per_item ASC
LIMIT 10;
NULLIF(jumlah_item, 0) di soal 19 buat ngindarin error pembagian dengan nol. Kebiasaan kecil yang nyelametin banyak query.
-- 21
SELECT COUNT(*) AS jumlah_transaksi FROM transaksi;
-- 22
SELECT SUM(total_harga) AS total_omzet FROM transaksi;
-- 23
SELECT ROUND(AVG(total_harga), 0) AS rata_rata FROM transaksi;
-- 24
SELECT cabang, SUM(total_harga) AS omzet
FROM transaksi
GROUP BY cabang;
-- 25
SELECT kategori, COUNT(*) AS jumlah
FROM transaksi
GROUP BY kategori;
-- 26
SELECT cabang, ROUND(AVG(total_harga), 0) AS rata_rata
FROM transaksi
GROUP BY cabang
ORDER BY rata_rata DESC;
-- 27
SELECT
cabang,
MAX(total_harga) AS tertinggi,
MIN(total_harga) AS terendah
FROM transaksi
GROUP BY cabang;
-- 28
SELECT
EXTRACT(MONTH FROM tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM transaksi
GROUP BY 1
ORDER BY 1;
-- 29
SELECT cabang, kategori, SUM(total_harga) AS omzet
FROM transaksi
GROUP BY cabang, kategori
ORDER BY cabang, omzet DESC;
-- 30
SELECT cabang, SUM(total_harga) AS omzet
FROM transaksi
GROUP BY cabang
HAVING SUM(total_harga) > 500000000
ORDER BY omzet DESC;
-- 31
SELECT kategori, COUNT(*) AS jumlah
FROM transaksi
GROUP BY kategori
HAVING COUNT(*) > 3000;
-- 32
SELECT cabang, COUNT(DISTINCT id_pelanggan) AS pelanggan_unik
FROM transaksi
GROUP BY cabang;
Soal 30 dan 31 nguji hal yang sama: WHERE nyaring sebelum dikelompokin, HAVING nyaring sesudah. Ini yang paling sering ketuker.
-- 33
SELECT t.*, p.nama
FROM transaksi t
INNER JOIN pelanggan p ON t.id_pelanggan = p.id_pelanggan;
-- 34
SELECT t.id_transaksi, t.tanggal, t.total_harga, p.nama, p.kota
FROM transaksi t
INNER JOIN pelanggan p ON t.id_pelanggan = p.id_pelanggan;
-- 35
SELECT p.nama, t.id_transaksi, t.total_harga
FROM pelanggan p
LEFT JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan;
-- 36
SELECT p.id_pelanggan, p.nama
FROM pelanggan p
LEFT JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
WHERE t.id_transaksi IS NULL;
-- 37
SELECT p.nama, SUM(t.total_harga) AS total_belanja
FROM pelanggan p
INNER JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
GROUP BY p.nama;
-- 38
SELECT p.nama, SUM(t.total_harga) AS total_belanja
FROM pelanggan p
INNER JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
GROUP BY p.nama
ORDER BY total_belanja DESC
LIMIT 10;
-- 39
SELECT p.kota, SUM(t.total_harga) AS omzet
FROM pelanggan p
INNER JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
GROUP BY p.kota
ORDER BY omzet DESC;
-- 40
SELECT p.nama, COUNT(DISTINCT t.cabang) AS jumlah_cabang
FROM pelanggan p
INNER JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
GROUP BY p.nama
HAVING COUNT(DISTINCT t.cabang) > 1;
-- 41
SELECT SUM(t.total_harga) AS omzet_pelanggan_baru
FROM pelanggan p
INNER JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
WHERE p.tanggal_daftar >= '2025-01-01';
-- 42
SELECT
CASE
WHEN p.tanggal_daftar < '2025-01-01' THEN 'Lama'
ELSE 'Baru'
END AS tipe_pelanggan,
COUNT(DISTINCT p.id_pelanggan) AS jumlah_orang,
SUM(t.total_harga) AS omzet
FROM pelanggan p
INNER JOIN transaksi t ON p.id_pelanggan = t.id_pelanggan
GROUP BY 1;
Soal 36 itu pola klasik: LEFT JOIN plus IS NULL buat nyari yang nggak punya pasangan. Sering keluar di interview.
-- 43
SELECT * FROM transaksi
WHERE total_harga > (SELECT AVG(total_harga) FROM transaksi);
-- 44
SELECT t.*
FROM transaksi t
WHERE t.total_harga > (
SELECT AVG(total_harga)
FROM transaksi
WHERE cabang = t.cabang
);
-- 45
WITH omzet_cabang AS (
SELECT cabang, SUM(total_harga) AS omzet
FROM transaksi
GROUP BY cabang
)
SELECT *
FROM omzet_cabang
WHERE omzet > (SELECT AVG(omzet) FROM omzet_cabang);
-- 46
SELECT
cabang,
id_transaksi,
total_harga,
RANK() OVER (
PARTITION BY cabang
ORDER BY total_harga DESC
) AS peringkat
FROM transaksi;
-- 47
WITH ranked AS (
SELECT
cabang,
id_transaksi,
total_harga,
ROW_NUMBER() OVER (
PARTITION BY cabang
ORDER BY total_harga DESC
) AS rn
FROM transaksi
)
SELECT cabang, id_transaksi, total_harga
FROM ranked
WHERE rn <= 3
ORDER BY cabang, total_harga DESC;
-- 48
WITH omzet_bulanan AS (
SELECT
EXTRACT(MONTH FROM tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM transaksi
GROUP BY 1
)
SELECT
bulan,
omzet,
SUM(omzet) OVER (ORDER BY bulan) AS omzet_kumulatif
FROM omzet_bulanan
ORDER BY bulan;
-- 49
WITH omzet_bulanan AS (
SELECT
EXTRACT(MONTH FROM tanggal) AS bulan,
SUM(total_harga) AS omzet
FROM transaksi
GROUP BY 1
)
SELECT
bulan,
omzet,
LAG(omzet) OVER (ORDER BY bulan) AS omzet_bulan_lalu,
omzet - LAG(omzet) OVER (ORDER BY bulan) AS selisih
FROM omzet_bulanan
ORDER BY bulan;
-- 50
SELECT
cabang,
SUM(total_harga) AS omzet,
ROUND(
100.0 * SUM(total_harga)
/ SUM(SUM(total_harga)) OVER (),
1
) AS persen_kontribusi
FROM transaksi
GROUP BY cabang
ORDER BY omzet DESC;
Soal 50 kelihatan aneh dengan SUM(SUM(...)) OVER (). Yang di dalam ngitung per cabang, yang di luar ngitung total semuanya. Ini trik yang kepake terus di kerjaan.
Waktu aku jalanin soal 50 di dataset toko_berkah, hasilnya bikin berhenti sebentar.
| Cabang | Omzet | Kontribusi |
|---|---|---|
| Jakarta | Rp1,42 miliar | 28,4% |
| Surabaya | Rp1,08 miliar | 21,6% |
| Bandung | Rp894 juta | 17,9% |
| Semarang | Rp731 juta | 14,6% |
| Depok | Rp512 juta | 10,2% |
| Bekasi | Rp366 juta | 7,3% |
Dua cabang teratas nyumbang separuh omzet. Bekasi cuma 7,3%, padahal jumlah transaksinya nggak jauh beda dari Depok.
Artinya masalah Bekasi ada di nilai per transaksi, bukan di jumlah pembeli. Satu query, satu temuan.
Langsung ngintip kunci jawaban. Otak kamu belajar dari momen macet, bukan dari baca jawaban benar. Kasih 10 menit buat tiap soal.
Ketuker WHERE dan HAVING. WHERE nyaring baris sebelum dikelompokin. HAVING nyaring hasil kelompok. Kalau kamu nulis WHERE SUM(...) > 100, itu bakal error.
Lupa GROUP BY semua kolom non-agregasi. Kalau kamu SELECT cabang dan kategori tapi cuma GROUP BY cabang, hasilnya error di kebanyakan database.
Ngerjain 50 soal dalam sehari. Nggak bakal nempel. Pecah jadi 2 minggu, 5 soal per hari.
Nggak cek hasilnya masuk akal. Query yang jalan bukan berarti query yang benar. Selalu cek jumlah barisnya wajar.
Kalau kamu masih goyang di dasar, balik dulu ke halaman GROUP BY dan JOIN.
Kalau kamu benar-benar pemula, sekitar 10-15 jam total, dan sebaiknya dipecah jadi 2 minggu. Level 1 sampai 3 biasanya lancar dalam 3-4 jam. Level 4 dan 5 yang makan waktu. Ngerjain 5 soal sampai paham betul lebih berguna dari nyontek 50 jawaban dalam sehari.
Mayoritas jalan di dua-duanya. Yang beda cuma beberapa fungsi tanggal dan casting angka. Kalau kamu pakai MySQL lama, ganti EXTRACT(MONTH FROM tanggal) jadi MONTH(tanggal). Referensi lengkapnya ada di dokumentasi resmi PostgreSQL.
Nggak harus. Kamu bisa langsung praktek di editor SQL online yang jalan di browser. Kalau kamu mau setup sendiri, SQLite paling ringan dan cukup buat semua soal di halaman ini.
SELECT dan WHERE dulu sampai lancar, lalu ORDER BY dan LIMIT, baru GROUP BY. Setelah itu JOIN, lalu subquery dan CTE, terakhir window function. Banyak orang lompat ke JOIN terlalu cepat lalu bingung, padahal masalahnya karena WHERE-nya belum kuat.
Cukup buat lolos tes teknisnya, tapi belum cukup buat lolos keseluruhan. Interviewer juga nanya kenapa kamu pilih pendekatan tertentu dan gimana kamu jelasin hasilnya ke orang non-teknis. Latih dua-duanya.
Kerjain 5 soal per hari, bukan 50 sekaligus. Dan cek hasilnya masuk akal, jangan cuma seneng karena querynya nggak error.
Mau langsung ngetik querynya tanpa install apa-apa? Buka NgulikSQL, editornya jalan di browser.
Baca juga: prospek kerja data analyst 2026 kalau kamu latihan SQL buat pindah karir.
Mau praktek langsung? Mulai latihan SQL gratis
Latihan interaktif, langsung di browser.
Analisa per kuartal, hari kerja, atau musim jadi ribet kalau tiap query ngitung ulang atribut tanggal. Tabel kalender nyimpen semua atribut itu sekali, biar tinggal di-JOIN.
Laporan penjualan harian sering bolong di hari tanpa transaksi. Ini cara bikin deret tanggal lengkap di SQL biar tiap hari muncul, walau nilainya nol.
Struktur organisasi tersimpan sebagai kolom id_atasan yang saling nunjuk. Ini cara narik rantai jabatan, hitung total bawahan, dan span of control cuma pakai SQL.