TL;DR
Menguji query SQL sebelum masuk produksi artinya nulis sekumpulan query pengecekan yang balikin nol baris kalau semua benar, lalu jalanin otomatis tiap kali query utamanya diubah. Enam uji yang paling sering nangkap masalah: kunci unik, kolom wajib nggak kosong, rentang nilai masuk akal, relasi antar tabel utuh, total hasil cocok sama sumber, dan jumlah baris nggak anjlok mendadak. Semua uji bisa digabung jadi satu file SQL, dijalanin pakai psql dengan ON_ERROR_STOP, dan dipasang di GitHub Actions supaya pull request yang bikin angka salah langsung ketahan.
Query yang error itu gampang. Dia berhenti, kamu benerin, selesai.
Yang bahaya query yang jalan mulus tapi angkanya salah. Satu JOIN yang bikin baris ganda bisa naikin omzet di dashboard 40% tanpa satu pun pesan error, dan baru ketahuan tiga minggu kemudian waktu finance protes.
CI sederhana nangkap masalah itu sebelum sampai ke dashboard. Di bawah ini 6 uji yang paling sering nangkap kesalahan, cara nyatuin semuanya jadi satu file, dan cara jalanin otomatis tiap kali query kamu berubah.
CI buat query SQL adalah proses otomatis yang jalan tiap kali kamu ngubah file query. Mesin CI bikin database kosong, ngisi data contoh, jalanin query kamu, terus jalanin daftar uji. Kalau ada uji yang gagal, perubahan kamu ketahan sebelum sampai ke produksi.
Prinsipnya satu: uji yang bagus balikin nol baris kalau semuanya benar. Jadi kamu nggak nulis "cek apakah benar", kamu nulis "tunjukin baris yang melanggar". Kalau hasilnya kosong, aman.
Ini bagian dari kerjaan jaga kualitas data yang sering kelewat waktu tim masih kecil.
Karena SQL nggak punya cara tahu apa yang kamu maksud. Dia cuma ngerjain apa yang kamu tulis.
Tiga kesalahan yang paling sering lolos tanpa error:
WHERE status = 'selesai', jadi transaksi batal ikut kehitung.SUM kamu diam-diam ngasih hasil yang beda.Dari 40 perubahan query yang aku catat selama ngerjain pipeline latihan toko_berkah, 7 ketahan di tahap uji sebelum sempat jalan di produksi. Lima dari tujuh itu penyebabnya sama: JOIN yang bikin baris ganda.
Enam uji ini urut dari yang paling sering nangkap masalah. Semua ditulis buat PostgreSQL, tapi polanya jalan di database lain dengan sedikit penyesuaian.
Uji paling penting. Kalau kolom kunci kamu ganda, semua total di atasnya salah.
SELECT order_id, COUNT(*) AS jumlah_baris
FROM ringkasan_harian
GROUP BY order_id
HAVING COUNT(*) > 1;
SELECT order_id, tanggal, nilai
FROM ringkasan_harian
WHERE tanggal IS NULL OR nilai IS NULL OR kota IS NULL;
Batas atasnya kamu tentukan sendiri sesuai bisnis. Buat toko kelontong, transaksi Rp 500 juta dalam satu struk hampir pasti salah input.
SELECT order_id, nilai
FROM ringkasan_harian
WHERE nilai < 0 OR nilai > 500000000;
Nyari transaksi yang nunjuk ke pelanggan yang nggak ada di tabel pelanggan. Pola LEFT JOIN plus IS NULL ini yang paling ringkas.
SELECT t.order_id, t.pelanggan_id
FROM transaksi t
LEFT JOIN pelanggan p ON t.pelanggan_id = p.id
WHERE p.id IS NULL;
Ini yang nangkap JOIN ganda dan filter yang kelewat sekaligus. Bandingkan total di tabel hasil sama total di tabel sumber.
WITH sumber AS (
SELECT SUM(nilai) AS total
FROM transaksi
WHERE status = 'selesai' AND tanggal >= DATE '2025-10-01'
),
hasil AS (
SELECT SUM(omzet) AS total
FROM ringkasan_harian
WHERE tanggal >= DATE '2025-10-01'
)
SELECT sumber.total AS total_sumber, hasil.total AS total_hasil
FROM sumber, hasil
WHERE ABS(COALESCE(sumber.total,0) - COALESCE(hasil.total,0)) > 1;
Toleransi 1 rupiah kepakai buat nyerap efek pembulatan. Kalau kamu pakai tipe numeric dan nggak ada pembulatan sama sekali, ganti jadi nol.
Uji ini nangkap sumber data yang tiba-tiba berhenti kirim. Ambang 50% bisa kamu sesuaikan.
WITH per_hari AS (
SELECT tanggal, COUNT(*) AS baris
FROM ringkasan_harian
GROUP BY tanggal
),
banding AS (
SELECT tanggal, baris,
LAG(baris) OVER (ORDER BY tanggal) AS baris_kemarin
FROM per_hari
)
SELECT tanggal, baris, baris_kemarin
FROM banding
WHERE baris_kemarin IS NOT NULL
AND baris < baris_kemarin * 0.5;
Fungsi LAG di sini ngambil nilai baris sebelumnya. Kalau kamu belum familiar sama cara kerja fungsi agregat dan window function, penjelasan resminya ada di tutorial window function PostgreSQL.
Enam query terpisah susah dijalanin otomatis. Gabungin jadi satu laporan pakai UNION ALL, dengan format nama uji plus jumlah pelanggaran.
WITH uji AS (
SELECT 'kunci_unik' AS nama_uji, COUNT(*) AS pelanggaran
FROM (SELECT order_id FROM ringkasan_harian
GROUP BY order_id HAVING COUNT(*) > 1) d
UNION ALL
SELECT 'kolom_wajib', COUNT(*)
FROM ringkasan_harian
WHERE tanggal IS NULL OR nilai IS NULL OR kota IS NULL
UNION ALL
SELECT 'rentang_nilai', COUNT(*)
FROM ringkasan_harian
WHERE nilai < 0 OR nilai > 500000000
)
SELECT nama_uji, pelanggaran
FROM uji
WHERE pelanggaran > 0
ORDER BY nama_uji;
Hasil kosong artinya lolos. Hasil berisi artinya ada yang perlu kamu benerin, lengkap dengan nama ujinya.
Query di atas cuma nampilin baris. CI perlu sinyal yang lebih tegas, yaitu kode keluar yang bukan nol.
Caranya pakai blok DO yang manggil RAISE EXCEPTION:
DO $$
DECLARE
jumlah_gagal integer;
BEGIN
SELECT COUNT(*) INTO jumlah_gagal
FROM (
SELECT 'kunci_unik' AS nama_uji, COUNT(*) AS pelanggaran
FROM (SELECT order_id FROM ringkasan_harian
GROUP BY order_id HAVING COUNT(*) > 1) d
UNION ALL
SELECT 'kolom_wajib', COUNT(*)
FROM ringkasan_harian
WHERE tanggal IS NULL OR nilai IS NULL
) u
WHERE pelanggaran > 0;
IF jumlah_gagal > 0 THEN
RAISE EXCEPTION 'Uji query gagal: % aturan dilanggar', jumlah_gagal;
END IF;
END $$;
Jalanin filenya begini:
psql -h localhost -U postgres -d toko_berkah -v ON_ERROR_STOP=1 -f sql/uji.sql
Opsi ON_ERROR_STOP=1 yang bikin psql berhenti dan keluar dengan kode error begitu ada exception. Tanpa opsi itu, psql tetap keluar dengan kode nol dan CI kamu bakal ngira semuanya aman. Detail perilakunya ada di dokumentasi psql PostgreSQL.
Struktur foldernya kayak gini:
sql/
skema.sql # bikin tabel kosong
data_contoh.sql # 120 baris data buatan, termasuk yang sengaja kotor
ringkasan_harian.sql # query yang mau diuji
uji.sql # daftar uji + blok DO
.github/workflows/uji-query.yml
Isi file workflow-nya:
name: uji-query
on: [pull_request]
jobs:
uji:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: toko_berkah
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
PGPASSWORD: postgres
steps:
- uses: actions/checkout@v4
- name: Siapkan skema dan data contoh
run: |
psql -h localhost -U postgres -d toko_berkah -v ON_ERROR_STOP=1 -f sql/skema.sql
psql -h localhost -U postgres -d toko_berkah -v ON_ERROR_STOP=1 -f sql/data_contoh.sql
- name: Jalankan query yang diuji
run: psql -h localhost -U postgres -d toko_berkah -v ON_ERROR_STOP=1 -f sql/ringkasan_harian.sql
- name: Jalankan uji
run: psql -h localhost -U postgres -d toko_berkah -v ON_ERROR_STOP=1 -f sql/uji.sql
Mulai sekarang, tiap pull request yang nyentuh file SQL bakal otomatis dites. Kalau ada uji gagal, tombol merge-nya ketahan.
Query ringkasan harian versi awal aku tulis begini:
SELECT t.tanggal, p.kota, SUM(t.nilai) AS omzet
FROM transaksi t
JOIN pelanggan p ON t.pelanggan_id = p.id
WHERE t.status = 'selesai'
GROUP BY t.tanggal, p.kota;
Kelihatan benar. Jalan tanpa error, hasilnya rapi.
Masalahnya, tabel pelanggan punya 23 baris duplikat dari proses impor lama. Pelanggan yang punya dua baris bikin transaksinya kehitung dua kali.
Uji 5 langsung nangkap ini. Total sumber Rp 1.842.600.000, total hasil Rp 1.911.480.000. Selisih Rp 68.880.000, atau 3,7% dari omzet. Cukup kecil buat lolos dari pandangan sekilas, cukup besar buat bikin keputusan diskon jadi salah.
Perbaikannya nambah DISTINCT ON di sisi pelanggan:
WITH pelanggan_unik AS (
SELECT DISTINCT ON (id) id, kota
FROM pelanggan
ORDER BY id, updated_at DESC
)
SELECT t.tanggal, p.kota, SUM(t.nilai) AS omzet
FROM transaksi t
JOIN pelanggan_unik p ON t.pelanggan_id = p.id
WHERE t.status = 'selesai'
GROUP BY t.tanggal, p.kota;
Setelah diperbaiki, uji 5 lolos dan selisihnya nol. Seluruh 6 uji jalan 1,8 detik buat 84.320 baris di runner GitHub Actions gratisan.
nilai itu sebelum atau sesudah diskon. Rapiin definisinya di kamus data.CI atau continuous integration buat query SQL artinya tiap kali kamu ngubah file query lalu ngirim perubahannya, ada mesin yang otomatis bikin database kosong, ngisi data contoh, jalanin query kamu, terus jalanin daftar uji. Kalau ada uji yang gagal, perubahan kamu ketahan dan nggak bisa digabung. Konsepnya sama kayak CI di software, cuma yang diuji hasil query, bukan fungsi program.
Kalau kamu punya di bawah 20 query dan satu database, psql plus satu file uji udah cukup dan bisa jalan hari ini juga. dbt kepakai waktu jumlah model kamu udah puluhan, ada ketergantungan antar tabel yang ribet, dan kamu perlu dokumentasi otomatis. Mulai dari yang sederhana dulu, pindah kalau file uji kamu udah susah dirawat.
Jangan pakai salinan data produksi, apalagi kalau ada data pribadi pelanggan. Bikin file SQL berisi 50 sampai 200 baris buatan yang sengaja mencakup kasus aneh: nilai nol, tanggal kosong, pelanggan yang nggak terdaftar, dan angka negatif. Data kecil yang sengaja kotor lebih cepat nangkap bug dibanding satu juta baris yang semuanya rapi.
Jalanin psql dengan opsi -v ON_ERROR_STOP=1, lalu di file uji pakai blok DO yang manggil RAISE EXCEPTION waktu ada pelanggaran. Waktu exception muncul, psql berhenti dan keluar dengan kode error yang bukan nol. Runner CI kayak GitHub Actions baca kode itu dan langsung nandain job sebagai gagal, jadi pull request kamu ketahan.
Mulai dari tiga: kunci unik, kolom wajib nggak kosong, dan total cocok sama sumber. Tiga itu nangkap sebagian besar kesalahan JOIN dan filter. Tambahin uji baru tiap kali ada bug lolos ke produksi, satu bug satu uji. Cara ini bikin daftar uji kamu tumbuh sesuai masalah nyata, bukan sesuai daftar teori.
Tiga hal yang bikin uji query kepakai: uji balikin baris yang melanggar, semua digabung jadi satu file, dan psql dijalanin dengan ON_ERROR_STOP biar CI-nya beneran berhenti.
Kalau kamu mulai hari ini, cukup tulis satu uji: kunci unik. Itu yang paling sering nyelamatin angka di dashboard.
Mau latihan nulis query dan window function-nya dulu? Coba latihan interaktif di NgulikSQL, langsung ketik di browser tanpa install database. Buat pipeline yang masih di spreadsheet, cara serupa bisa kamu terapin lewat langkah-langkah Power Query M yang bisa diulang tanpa klik manual.
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.