Di banyak tim, pipeline data lokal dimulai dari berkas CSV. Entah itu ekspor aplikasi internal, unduhan dari sistem pihak ketiga, a...
Di banyak tim, pipeline data lokal dimulai dari berkas CSV. Entah itu ekspor aplikasi internal, unduhan dari sistem pihak ketiga, atau hasil pengumpulan manual seorang analis, format ini tetap dipilih karena bisa dibuka di mana saja dan mudah diperiksa mata manusia. Masalahnya, CSV hampir tidak membawa jaminan apa pun: tidak ada skema yang mengikat, tidak ada batasan tipe, dan tidak ada mekanisme yang mencegah satu baris rusak masuk ke tahap berikutnya. Kesalahan yang muncul di laporan akhir sering kali berasal dari satu sel yang salah pada hari pertama.
Artikel ini membahas satu pola yang cukup sederhana: memakai DuckDB sebagai alat pemeriksaan data sebelum pemrosesan dimulai. Alih-alih langsung memuat CSV ke dalam skrip transformasi, berkas diperiksa lebih dulu dengan query SQL: apakah jumlah barisnya sesuai, apakah kolom kunci punya nilai kosong, apakah ada duplikat, apakah tipe data benar-benar bisa dikonversi, dan apakah nilai-nilainya berada di rentang yang wajar. DuckDB cocok untuk peran ini karena ia berjalan sebagai pustaka di dalam proses, tanpa server, dan bisa membaca CSV langsung dari berkas.
Semua contoh di bawah mengacu pada pola laboratorium lokal berskala kecil: satu mesin virtual dengan empat vCPU, satu berkas CSV contoh berisi puluhan ribu baris hasil ekspor aplikasi internal, dan DuckDB yang dijalankan dari baris perintah. Tidak ada angka benchmark di artikel ini, karena kecepatan pemindaian sangat bergantung pada bentuk data, lebar kolom, dan media penyimpanan. Bagian akhir artikel menyebut secara eksplisit apa yang belum diuji.
Mengapa Validasi CSV Layak Menjadi Tahap Tersendiri
Dalam pipeline sederhana, validasi sering dianggap bagian dari transformasi. Akibatnya pemeriksaan hanya muncul ketika ada yang gagal, dan kegagalan itu biasanya baru terlihat jauh di hilir: sebuah model prediksi memberi hasil aneh, sebuah laporan bulanan tidak seimbang, atau sebuah dashboard menampilkan angka yang tidak bisa dipertanggungjawabkan. Ketika akar masalahnya dilacak, penyebabnya sering membosankan dan berulang.
Tiga kegagalan yang paling sering muncul
Pertama, tipe data berubah tanpa pemberitahuan. Kolom kode_pos yang seharusnya berupa teks berisi nol di depan berubah menjadi bilangan bulat, sehingga 05123 menjadi 5123. Kolom tanggal yang tadinya YYYY-MM-DD tiba-tiba berisi 31/12/2026, dan mesin query menebak urutan hari dan bulan sesuai seleranya. Kedua, duplikasi kunci. Ekspor ulang setelah kegagalan jaringan menghasilkan baris yang sama dua kali, dan agregasi berikutnya menghitungnya sebagai dua kejadian berbeda. Ketiga, nilai kosong yang menyamar. Sel yang berisi spasi, tanda hubung, N/A, atau NULL sebagai teks tidak terdeteksi sebagai kosong oleh sebagian alat, tetapi tetap merusak perhitungan rata-rata dan persentase.
Memisahkan validasi menjadi tahap tersendiri memberi tiga keuntungan. Pemeriksaan menjadi eksplisit dan bisa dibaca ulang oleh orang lain; hasilnya bisa disimpan sebagai artefak sehingga bisa dibandingkan antar-hari; dan pipeline bisa dihentikan lebih awal ketika data tidak memenuhi syarat, bukan setelah separuh pekerjaan berjalan. Tahap ini juga tidak memerlukan infrastruktur tambahan, karena dapat dijalankan di mesin yang sama dengan skrip transformasi.
Menyiapkan DuckDB untuk Pemeriksaan Lokal
Memasang dan membuka basis data kerja
DuckDB tersedia sebagai biner tunggal untuk Linux, macOS, dan Windows, serta sebagai pustaka Python, R, dan Java. Untuk pekerjaan pemeriksaan, cara paling ringkas adalah memakai CLI dan menyimpan basis data kerja dalam satu berkas. Pada laboratorium lokal yang menjadi acuan artikel ini, biner diunduh dari halaman rilis proyek, diekstrak ke subdirektori ./bin, lalu dijalankan dari direktori yang sama dengan berkas CSV dengan perintah ./bin/duckdb valcheck.duckdb. Berkas basis data dibuat otomatis bila belum ada, dan tidak ada variabel lingkungan khusus yang perlu disiapkan sebelum pemeriksaan pertama.
Karena DuckDB berjalan di dalam proses, tidak ada layanan yang perlu dinyalakan dan tidak ada port yang perlu dibuka. Basis data valcheck.duckdb dapat dipindahkan, disalin, atau dibuang seperti berkas biasa. Untuk pemeriksaan sekali jalan, basis data bahkan tidak perlu disimpan sama sekali: cukup jalankan query terhadap CSV dan biarkan hasilnya mengalir ke layar.
Membaca CSV dengan skema eksplisit
DuckDB dapat menebak struktur CSV secara otomatis melalui read_csv_auto, dan untuk pemakaian cepat tebakan itu biasanya memadai. Namun untuk validasi, penebakan justru bagian yang ingin dihindari: kalau tipe ditebak dari sampel, kolom yang mayoritas berisi angka tetapi sesekali berisi teks bisa berakhir sebagai VARCHAR, dan pemeriksaan numerik berikutnya kehilangan makna. Karena itu pola yang lebih aman adalah menyebut tipe secara eksplisit. Rincian opsi pembacaan CSV, termasuk penanganan kutip, pemisah, dan nilai kosong, tersedia di dokumentasi CSV DuckDB.
-- periksa dulu hasil tebakan skema, tanpa memuat seluruh data
DESCRIBE SELECT * FROM read_csv_auto('data/pesanan_2026_11.csv');
-- versi eksplisit: semua kolom dibaca sebagai teks agar tidak ada data hilang
CREATE OR REPLACE VIEW raw AS
SELECT *
FROM read_csv('data/pesanan_2026_11.csv',
header = true,
delim = ',',
quote = '"',
escape = '"',
nullstr = ['', 'NULL', 'N/A', '-'],
all_varchar = true);
SELECT count(*) AS jumlah_baris FROM raw;
Membaca semuanya sebagai teks terlebih dahulu terasa berlawanan dengan intuisi, tetapi justru inilah kunci validasi yang jujur. Dengan all_varchar = true, tidak ada nilai yang diam-diam dibuang atau diubah oleh konversi otomatis. Konversi kemudian dilakukan secara sadar memakai TRY_CAST, dan setiap nilai yang gagal dikonversi bisa dihitung sebagai temuan, bukan hilang tanpa jejak.
Empat Pemeriksaan Dasar yang Menangkap Sebagian Besar Masalah
Kelengkapan kolom kunci
Pemeriksaan pertama adalah memastikan kolom yang menjadi kunci tidak pernah kosong. Selain IS NULL, perlu juga memeriksa teks yang hanya berisi spasi, karena nilai seperti itu sering lolos dari pemeriksaan sederhana dan baru terlihat sebagai kolom kosong di laporan. Query berikut menghitung keduanya sekaligus, dan hasilnya sengaja dibuat dalam bentuk angka agar mudah dibandingkan dengan ambang batas yang disepakati tim.
WITH bersih AS (
SELECT trim(pesanan_id) AS pesanan_id,
trim(pelanggan_id) AS pelanggan_id,
trim(tanggal) AS tanggal,
trim(total) AS total
FROM raw
)
SELECT
count(*) AS total_baris,
count(*) FILTER (WHERE pesanan_id IS NULL OR pesanan_id = '') AS id_kosong,
count(*) FILTER (WHERE pelanggan_id IS NULL OR pelanggan_id = '') AS pelanggan_kosong,
count(*) FILTER (WHERE tanggal IS NULL OR tanggal = '') AS tanggal_kosong,
count(*) FILTER (WHERE total IS NULL OR total = '') AS total_kosong
FROM bersih;
Duplikasi kunci
Pemeriksaan kedua mencari kunci yang muncul lebih dari sekali. Cara paling mudah dibaca adalah mengelompokkan berdasarkan kunci, menghitung kemunculannya, lalu menyaring grup yang jumlahnya lebih dari satu. DuckDB mendukung klausa QUALIFY sehingga hasilnya bisa langsung dibatasi tanpa subquery tambahan. Yang perlu diputuskan bersama tim adalah apakah duplikat diperbolehkan pada tingkat tertentu, misalnya karena satu pesanan memang boleh punya beberapa baris item; keputusan itu harus tertulis, bukan disimpulkan dari data.
SELECT pesanan_id, count(*) AS kemunculan
FROM raw
GROUP BY pesanan_id
QUALIFY count(*) > 1
ORDER BY kemunculan DESC, pesanan_id
LIMIT 25;
Tipe dan rentang nilai
Pemeriksaan ketiga menggabungkan dua hal yang sering dianggap terpisah: apakah nilai bisa dikonversi ke tipe yang dimaksud, dan apakah nilai hasil konversi berada di rentang yang wajar. Contoh di bawah memakai TRY_CAST sehingga konversi yang gagal menghasilkan NULL, bukan menghentikan query. Dengan begitu, satu query bisa menjawab berapa baris yang gagal dikonversi sekaligus menampilkan contoh barisnya untuk diperiksa manual.
WITH dikonversi AS (
SELECT pesanan_id,
pelanggan_id,
TRY_CAST(tanggal AS DATE) AS tanggal_date,
TRY_CAST(replace(total, ',', '.') AS DECIMAL(18,2)) AS total_num
FROM raw
)
SELECT
count(*) FILTER (WHERE tanggal_date IS NULL) AS tanggal_tidak_valid,
count(*) FILTER (WHERE total_num IS NULL) AS total_tidak_valid,
count(*) FILTER (WHERE total_num < 0) AS total_negatif,
count(*) FILTER (WHERE tanggal_date > DATE '2026-11-12') AS tanggal_masa_depan,
min(total_num) AS total_minimum,
max(total_num) AS total_maksimum
FROM dikonversi;
Konsistensi nilai kategori
Pemeriksaan keempat menyasar kolom kategoris seperti status, kanal penjualan, atau kode gudang. Masalah khasnya bukan nilai kosong, melainkan variasi ejaan: Aktif, aktif, dan AKTIF dianggap tiga kategori berbeda oleh GROUP BY. Menghitung distribusi nilai sebelum dan sesudah normalisasi adalah cara termurah untuk menemukan masalah ini. Pola query-nya sederhana, cukup GROUP BY pada kolom yang sudah di-lower() lalu bandingkan dengan daftar nilai yang diizinkan. Bila jumlah nilai berbeda dari yang diharapkan, tim perlu memutuskan apakah data sumber yang salah atau daftar acuan yang perlu diperbarui.
Catatan praktis: hindari opsi ignore_errors = true sebagai pengaturan bawaan saat memuat CSV. Opsi itu membuat baris bermasalah dilewati tanpa suara, sehingga jumlah baris di hilir lebih kecil dari berkas asli dan penyebabnya sulit dilacak. Bila memang perlu dipakai, jalankan sebagai langkah terpisah setelah jumlah baris mentah dicatat, agar selisihnya bisa dijelaskan.
Merangkum Profil Data dengan SUMMARIZE dan DESCRIBE
Sebelum menulis pemeriksaan yang spesifik, pemeriksaan menyeluruh kadang membantu: berapa nilai unik di tiap kolom, berapa persen yang kosong, dan seperti apa sebaran nilai minimum serta maksimumnya. DuckDB menyediakan perintah SUMMARIZE untuk keperluan ini. Hasilnya bukan pengganti pemeriksaan yang dirancang khusus, tetapi berguna sebagai peta awal ketika berkas baru pertama kali diterima dan belum ada yang tahu isinya.
Ada dua hal yang perlu diingat saat membaca keluaran SUMMARIZE. Pertama, jumlah nilai unik yang ditampilkan bersifat perkiraan, jadi angka itu tidak boleh dipakai sebagai dasar keputusan yang menuntut ketepatan. Kedua, statistik dihitung pada data yang sudah dimuat ke dalam tipe tertentu, sehingga kesimpulannya mewarisi keputusan tipe yang dibuat pada langkah sebelumnya. Karena itu urutannya penting: baca sebagai teks, periksa dengan DESCRIBE atau SUMMARIZE, baru lakukan konversi eksplisit.
Menjalankan Validasi sebagai Gate di Pipeline Lokal
Pemeriksaan yang berguna adalah pemeriksaan yang benar-benar menghentikan pekerjaan ketika gagal. Pola paling sederhana di mesin lokal adalah skrip shell yang menjalankan satu query ringkasan, lalu memeriksa keluarannya. Query ringkasan itu sendiri dapat memakai fitur SQL yang membuat kode lebih pendek, misalnya FILTER pada agregat, GROUP BY ALL, dan COLUMNS(*) untuk menghitung pola yang sama di banyak kolom; sintaksisnya dijelaskan di dokumentasi klausa SELECT.
#!/usr/bin/env bash
set -euo pipefail
DB=valcheck.duckdb
CSV=data/pesanan_2026_11.csv
# 1) hitung temuan, tulis ke tabel agar menjadi artefak yang bisa dibandingkan
./bin/duckdb "$DB" <<SQL
CREATE OR REPLACE TABLE temuan AS
SELECT current_date AS tanggal_periksa,
'${CSV}' AS berkas,
count(*) AS total_baris,
count(*) FILTER (WHERE trim(pesanan_id) = '') AS id_kosong,
count(*) FILTER (WHERE TRY_CAST(tanggal AS DATE) IS NULL) AS tanggal_tidak_valid
FROM read_csv('${CSV}', header = true, all_varchar = true);
SELECT * FROM temuan;
SQL
# 2) gate: hentikan pipeline bila ada temuan yang melanggar ambang
GAGAL=$(./bin/duckdb "$DB" -noheader -list -c \
"SELECT (id_kosong + tanggal_tidak_valid) FROM temuan")
echo "temuan kritis: ${GAGAL}"
if [ "${GAGAL}" -ne 0 ]; then
echo "validasi gagal; pipeline dihentikan sebelum transformasi" >&2
exit 1
fi
Beberapa detail membuat pola ini lebih tahan dipakai sehari-hari. Nama berkas dan tanggal pemeriksaan dicatat di tabel temuan, sehingga riwayat pemeriksaan bisa ditelusuri tanpa membuka log. Ambang kegagalan dinyatakan sebagai angka, bukan sebagai kalimat di dalam skrip, sehingga mudah diubah ketika kesepakatan tim berubah. Dan karena seluruh pemeriksaan berjalan di satu berkas basis data, hasilnya bisa ditanyakan ulang kapan saja dengan SQL biasa. Perlu dicatat bahwa artikel ini tidak memverifikasi perilaku exit status CLI DuckDB secara sistematis; pada skrip di atas keputusan diambil dari nilai yang dicetak query, bukan dari kode keluar CLI.
Batas Pengujian, Jebakan, dan Langkah Berikutnya
Agar tidak menyesatkan, perlu dinyatakan dengan jelas apa yang diuji dan apa yang tidak. Pola pada artikel ini dicoba pada satu mesin virtual dengan empat vCPU, satu berkas CSV contoh berukuran puluhan ribu baris, pemisah koma, dan pengodean UTF-8 tanpa BOM. Yang belum diuji: berkas berukuran beberapa gigabyte, kolom teks dengan baris multiline di dalam tanda kutip, pengodean Windows-1252 yang umum pada ekspor aplikasi lama, perilaku CLI pada kondisi kehabisan memori, serta integrasi ke orkestrator seperti Airflow atau cron terjadwal. Klaim performa juga sengaja tidak dibuat.
Beberapa jebakan tetap perlu diwaspadai meski pola dasarnya sudah benar. Pemisah tidak selalu koma; ekspor dari aplikasi berlokal Indonesia sering memakai titik koma, dan angka desimal memakai koma, sehingga 1.234,56 bisa terbaca sebagai teks. Berkas UTF-8 dengan BOM membuat nama kolom pertama membawa karakter tak terlihat, yang berujung pada error "kolom tidak ditemukan" meski namanya tampak benar. Tanggal dengan format hari-bulan-tahun yang ambigu, misalnya 03/04/2026, tidak bisa diselesaikan oleh konversi otomatis dan harus ditangani dengan dateformat eksplisit. Nilai seperti nomor identitas atau kode pos wajib diperlakukan sebagai teks sepanjang pipeline, bukan hanya saat pemeriksaan.
Langkah berikutnya yang wajar adalah mengunci hasil pemeriksaan ke dalam bentuk yang lebih tegas. CSV yang sudah lolos bisa ditulis ulang ke Parquet dengan skema tetap, sehingga tahap transformasi tidak perlu mengulang penebakan tipe setiap hari. Daftar kolom, tipe, dan ambang batas dapat disimpan sebagai kontrak skema di repositori dan diverifikasi pada setiap perubahan. Dengan begitu, validasi bukan lagi kegiatan sesekali ketika ada masalah, melainkan bagian dari definisi pipeline itu sendiri: data diperiksa lebih dulu, baru diproses.
COMMENTS