Latihan SQL 30 Hari: 30 Soal Bertingkat dengan Pembahasan
Blog/Tutorial SQL/Latihan SQL 30 Hari: 30 Soal Bertingkat dengan Pembahasan

Latihan SQL 30 Hari: 30 Soal Bertingkat dengan Pembahasan

BimaBima
·9 September 2025·13 menit baca

Penulis

Bima

Bima

Founder & Data Professional

Bagikan

TL;DR

Program latihan SQL 30 hari ini membagi 30 soal jadi empat tingkat: minggu pertama filter dan agregasi, minggu kedua JOIN dan tanggal, minggu ketiga subquery dan CTE, minggu keempat window function. Tiap soal dikerjain 20 sampai 30 menit dengan satu skema tabel toko yang sama, jadi kamu nggak buang waktu bolak-balik memahami data baru.

Latihan SQL 30 hari ini isinya satu soal per hari yang naik tingkat, dari SELECT dasar sampai window function. Semua soal pakai satu skema tabel toko yang sama, jadi kamu nggak buang waktu ngerti data baru tiap hari.

Alasan formatnya harian sederhana. SQL itu keterampilan otot, bukan bahan hafalan. Ngerjain 30 soal dalam satu akhir pekan bikin kamu ngerti, tapi nggak bikin kamu inget.

Ambil satu soal, kerjain 20 sampai 30 menit, baru buka pembahasannya.

Skema tabel yang dipakai

Empat tabel, mirip struktur toko online kecil di Indonesia.

pelanggan(id, nama, kota, tanggal_daftar)
produk(id, nama, kategori, harga)
pesanan(id, pelanggan_id, tanggal, total, status)
pesanan_item(pesanan_id, produk_id, qty, harga)

Kolom status isinya selesai, batal, atau pending. Hampir semua soal cuma ngitung yang selesai, dan itu memang sengaja biar kamu terbiasa nanya definisi angka sebelum ngetik.

Semua contoh ditulis dengan sintaks PostgreSQL. Rujukan fungsi lengkapnya ada di dokumentasi resmi PostgreSQL.

Minggu 1: filter dan agregasi (hari 1 sampai 7)

HariSoalYang dilatih
1Tampilkan nama dan kota semua pelanggan dari SurabayaSELECT, WHERE
2Ambil pesanan bulan Maret 2025 yang statusnya selesaiFilter tanggal
3Sepuluh pesanan dengan nilai terbesarORDER BY, LIMIT
4Daftar kota unik tempat pelanggan tinggalDISTINCT
5Jumlah pesanan, total omzet, dan rata-rata nilai pesananCOUNT, SUM, AVG
6Omzet per kategori produkGROUP BY plus JOIN
7Kategori dengan omzet di atas 10 juta sajaHAVING

Pembahasan hari 6:

SELECT pr.kategori,
       SUM(pi.qty * pi.harga) AS omzet,
       COUNT(DISTINCT pe.id) AS jumlah_pesanan
FROM pesanan_item pi
JOIN produk pr ON pr.id = pi.produk_id
JOIN pesanan pe ON pe.id = pi.pesanan_id
WHERE pe.status = 'selesai'
GROUP BY pr.kategori
ORDER BY omzet DESC;

Perhatiin COUNT(DISTINCT pe.id). Tanpa DISTINCT, satu pesanan berisi 4 barang kehitung 4 kali. Kesalahan ini muncul di hampir semua laporan pertama yang aku review.

Beda WHERE dan HAVING di hari 7: WHERE nyaring baris sebelum dikelompokkan, HAVING nyaring hasil setelah dikelompokkan. Rangkuman perilaku fungsi ringkasnya ada di glosarium aggregate function.

Minggu 2: JOIN dan tanggal (hari 8 sampai 14)

HariSoalYang dilatih
8Nama pelanggan beserta tanggal pesanannyaINNER JOIN
9Pelanggan yang belum pernah pesan sama sekaliLEFT JOIN plus IS NULL
10Nama pelanggan, nama produk, dan qty tiap baris pesananJOIN tiga tabel
11Total belanja per pelanggan, urut dari terbesarJOIN plus GROUP BY
12Kelompokkan pesanan jadi kecil, sedang, besarCASE WHEN
13Ganti kota kosong jadi "tidak diisi"COALESCE
14Omzet per bulan sepanjang 2025DATE_TRUNC

Pembahasan hari 9:

SELECT p.id, p.nama
FROM pelanggan p
LEFT JOIN pesanan o ON o.pelanggan_id = p.id
WHERE o.id IS NULL;

Ini soal yang paling sering bikin macet di minggu kedua. Kuncinya: LEFT JOIN nyimpan semua baris pelanggan, dan pelanggan tanpa pasangan dapat NULL di semua kolom pesanan. Baris NULL itu yang kamu saring.

Perbandingan biasa nggak jalan buat NULL. WHERE o.id != 5 bakal ngebuang baris NULL, bukan nyimpan.

Pembahasan hari 14:

SELECT DATE_TRUNC('month', tanggal) AS bulan,
       SUM(total) AS omzet
FROM pesanan
WHERE status = 'selesai'
  AND tanggal >= DATE '2025-01-01'
  AND tanggal < DATE '2026-01-01'
GROUP BY 1
ORDER BY 1;

Batas kanan pakai tanda kurang dari, bukan BETWEEN. Kalau kolomnya bertipe timestamp, BETWEEN sampai 31 Desember bakal ngebuang transaksi jam 10 malam di hari terakhir.

Minggu 3: subquery, CTE, dan gabungan (hari 15 sampai 21)

HariSoalYang dilatih
15Pesanan yang nilainya di atas rata-rataSubquery di WHERE
16Tiap pelanggan beserta jumlah pesanannyaSubquery di SELECT
17Tulis ulang soal hari 16 pakai CTEWITH
18Pelanggan yang pernah beli kategori SembakoEXISTS
19Gabungkan daftar kota pelanggan dan kota gudangUNION vs UNION ALL
20Pasangan pelanggan yang tinggal di kota samaSelf join
21Produk terlaris di kota dengan omzet terbesarCTE bertingkat

Pembahasan hari 20:

SELECT a.nama AS pelanggan_1,
       b.nama AS pelanggan_2,
       a.kota
FROM pelanggan a
JOIN pelanggan b
  ON a.kota = b.kota
 AND a.id < b.id
ORDER BY a.kota;

Syarat a.id < b.id yang bikin querynya benar. Tanpa itu, tiap pasangan muncul dua kali dan tiap orang berpasangan sama dirinya sendiri.

Buat hari 19, ingat bedanya: UNION ngebuang baris kembar, UNION ALL nyimpan semuanya. UNION ALL lebih cepat karena nggak perlu ngecek duplikat.

Minggu 4: window function (hari 22 sampai 28)

HariSoalYang dilatih
22Nomor urut pesanan tiap pelangganROW_NUMBER
23Peringkat produk per kategori, tangani nilai seriRANK vs DENSE_RANK
24Pertumbuhan omzet dibanding bulan sebelumnyaLAG
25Omzet kumulatif sepanjang tahunSUM OVER
26Bagi pelanggan jadi 4 kelompok belanjaNTILE
27Rata-rata bergerak omzet 3 bulanROWS BETWEEN
28Transaksi terakhir tiap pelangganROW_NUMBER plus filter

Pembahasan hari 24:

WITH omzet_bulanan AS (
    SELECT DATE_TRUNC('month', tanggal) AS bulan,
           SUM(total) AS omzet
    FROM pesanan
    WHERE status = 'selesai'
    GROUP BY 1
)
SELECT bulan,
       omzet,
       LAG(omzet) OVER (ORDER BY bulan) AS omzet_bulan_lalu,
       ROUND(
         (omzet - LAG(omzet) OVER (ORDER BY bulan)) * 100.0
         / NULLIF(LAG(omzet) OVER (ORDER BY bulan), 0), 2
       ) AS pertumbuhan_persen
FROM omzet_bulanan
ORDER BY bulan;

NULLIF dipasang biar bulan dengan omzet nol nggak bikin pembagian error. Baris pertama pasti kosong pertumbuhannya, karena nggak ada bulan sebelumnya.

Pembahasan hari 27:

SELECT bulan,
       omzet,
       ROUND(AVG(omzet) OVER (
           ORDER BY bulan
           ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
       ), 0) AS rata_3_bulan
FROM omzet_bulanan
ORDER BY bulan;

Bagian ROWS BETWEEN 2 PRECEDING AND CURRENT ROW nentuin jendela tiga baris. Tanpa itu, AVG OVER bakal ngerata-rata seluruh baris dan hasilnya sama di tiap bulan.

Angka rata-rata bergerak inilah yang biasanya dipasang di dashboard, soalnya dia meredam lonjakan hari besar. Cara nentuin ukuran yang layak dipantau ada di glosarium metrik.

Hari 29 dan 30: dua studi kasus gabungan

Hari 29, retensi bulanan. Hitung berapa persen pelanggan yang belanja di bulan tertentu masih belanja lagi bulan berikutnya. Gabungan CTE, self join berbasis bulan, dan pembagian dengan NULLIF.

Hari 30, RFM sederhana. Buat tiap pelanggan, hitung tiga angka: jarak hari sejak transaksi terakhir, jumlah transaksi, dan total belanja. Lalu bagi tiap angka jadi 4 kelompok pakai NTILE, dan gabung jadi satu kode segmen.

WITH rfm AS (
    SELECT pelanggan_id,
           CURRENT_DATE - MAX(tanggal)::date AS recency,
           COUNT(*) AS frequency,
           SUM(total) AS monetary
    FROM pesanan
    WHERE status = 'selesai'
    GROUP BY pelanggan_id
)
SELECT pelanggan_id,
       NTILE(4) OVER (ORDER BY recency DESC) AS skor_r,
       NTILE(4) OVER (ORDER BY frequency)    AS skor_f,
       NTILE(4) OVER (ORDER BY monetary)     AS skor_m
FROM rfm;

Skor recency diurut menurun karena jarak hari makin kecil justru makin bagus.

Cara ngerjain biar nggak berhenti di tengah

  1. Kerjain di jam yang sama tiap hari. Ritme lebih menentukan dari durasi.
  2. Tulis dulu rencananya dalam bahasa biasa sebelum ngetik SQL. Satu kalimat cukup.
  3. Cek jumlah baris hasilnya masuk akal atau nggak. Hasil 40.000 baris dari 2.000 pesanan artinya JOIN kamu meledak.
  4. Kalau macet lebih dari satu jam, berhenti. Baca pembahasannya, tutup, terus tulis ulang dari nol besok pagi.
  5. Simpan semua query di satu file. Akhir bulan kamu punya catatan sendiri yang lebih berguna dari cheatsheet mana pun.

Tiga titik macet yang paling sering aku lihat: hari 9, hari 19, dan hari 27. Ketiganya bukan soal sintaks rumit, melainkan soal ngebayangin hasil antara sebelum baris akhir keluar.

FAQ

Butuh berapa lama per hari?

Patokannya 20 sampai 30 menit per soal di minggu pertama dan kedua, naik ke 40 menit di minggu keempat. Kalau satu soal makan lebih dari satu jam, berhenti dulu dan tulis ulang besok pagi.

Database apa yang dipakai?

Semua contoh ditulis dengan sintaks PostgreSQL. Sebagian besar jalan juga di MySQL 8 dan SQLite modern, kecuali DATE_TRUNC yang perlu diganti DATE_FORMAT di MySQL.

Harus urut dari hari 1 atau boleh loncat?

Urut lebih aman, soalnya soal minggu ketiga pakai hasil dari minggu kedua. Yang jangan diloncat itu minggu keempat, karena window function butuh kebiasaan mikir per kelompok baris.

Soal mana yang paling sering bikin macet?

Hari 9 soal LEFT JOIN dengan IS NULL, hari 19 soal beda UNION dan UNION ALL, dan hari 27 soal frame ROWS BETWEEN.

Latihan ini cukup buat lolos interview?

Cukup buat bagian teknisnya. Yang belum ketutup adalah kemampuan ngobrol: mengklarifikasi soal, menjelaskan logika sambil ngetik, dan menyadari kasus tepi.

Penutup

Tiga puluh soal ini nutup hampir semua yang ditanyakan di tes SQL level pemula sampai menengah. Yang bikin beda bukan jumlah soalnya, tapi kebiasaan ngerjain satu soal tiap hari tanpa bolong.

Mulai hari ini juga dari soal nomor 1, jangan nunggu Senin.

Habis kelar hari ke-30, uji hasilnya lewat transkrip mock interview SQL dan nilai diri kamu pakai rubrik di sana. Kalau kamu butuh data mentah buat latihan sendiri, cara nyiapinnya ada di struk kasir jadi data.

Coba Langsung

Mau praktek langsung? Mulai latihan SQL gratis

Latihan interaktif, langsung di browser.

Buka NgulikSQL →
Bagikan:
Bima
Ditulis oleh

Bima

Founder & Data Professional

Founder Ngulik Data. Passionate about making data analysis accessible for everyone.

Artikel terkait

Cara Membuat Pivot Dinamis di SQL (Panduan 2026)
Tutorial SQL
19 Juli 2026•10 menit baca

Cara Membuat Pivot Dinamis di SQL (Panduan 2026)

Pivot dinamis di SQL ngubah baris jadi kolom tanpa kamu hardcode nama kolomnya. Ini cara bikinnya pakai CASE WHEN dan versi yang kolomnya ngikut data.

BimaBima
Menghitung YTD, QTD, dan MTD di SQL (Panduan 2026)
Tutorial SQL
17 Juli 2026•9 menit baca

Menghitung YTD, QTD, dan MTD di SQL (Panduan 2026)

YTD, QTD, dan MTD ngukur total dari awal tahun, kuartal, atau bulan sampai hari ini. Ini cara ngitungnya di SQL pakai DATE_TRUNC, plus satu query gabungan.

BimaBima
Gap and Island Analysis di SQL
Tutorial SQL
15 Juli 2026•10 menit baca

Gap and Island Analysis di SQL

Gap and island analysis di SQL nyari deret data yang berturut-turut (island) dan celah yang bolong (gap). Cocok buat hitung streak login atau cari tanggal transaksi yang hilang.

BimaBima
Kembali ke Blog
Ngulik Data logoNgulik Data

Platform edukasi data lengkap untuk professionals Indonesia. Belajar SQL, Data Analysis, dan lebih banyak lagi dengan praktek langsung dan feedback real-time.

© 2026 Ngulik Data. Semua hak dilindungi.

TAUTAN
BantuanHargaDatasetBlogAfiliasi
LEGAL
Syarat & KetentuanKebijakan Privasi
Ngulik Data
DatasetLeaderboardBlogStore