Pemeriksaan freshness, kelengkapan, dan validitas pada chapter-chapter sebelumnya bisa lulus semua — dan tetap saja laporan keuangan menyebut pendapatan Rp1.041.000 sementara dashboard analytics menyebut Rp1.105.000. Setiap tabel sehat sendiri-sendiri, tetapi dua tabel yang menggambarkan fakta yang sama saling bertentangan angkanya. Data consistency adalah dimensi yang menguji keselarasan lintas tabel: rujukan antar tabel harus tersambung, dan agregat atas fakta yang sama harus bernilai sama. Chapter ini memakai DuckDB untuk menjalankan dua kelompok uji — konsistensi referensial dan konsistensi agregat — lengkap dengan cara membaca selisihnya.
Tiga Jenis Uji Konsistensi #
| Jenis Uji | Pertanyaan | Contoh Temuan |
|---|---|---|
| Referensial | Apakah setiap rujukan antar tabel tersambung? | pembayaran untuk pesanan yang tidak ada |
| Agregat | Apakah jumlah yang sama dihitung sama di tabel berbeda? | total pesanan ≠ total pembayaran |
| Lintas sistem | Apakah salinan data di sistem lain bernilai sama? | jumlah pelanggan di OLTP ≠ di warehouse |
Uji lintas sistem adalah generalisasi uji agregat ke arah arsitektur — tabel warehouse yang direplikasi dari database operasional — dan mekanismenya sama: bandingkan agregat kunci pada titik waktu yang sama. Dua jenis pertama kita praktikkan sekarang.
Konsistensi Referensial: Orphaned Foreign Key #
Tabel pesanan merujuk ke pelanggan lewat customer_id, dan tabel pembayaran merujuk ke pesanan lewat order_id. Baris yang rujukannya tidak ditemukan di tabel tujuan disebut orphaned foreign key — pesanan dari pelanggan yang tidak terdaftar, atau pembayaran tanpa pesanan. Query pendeteksinya memakai pola anti-join: LEFT JOIN ke tabel tujuan lalu saring baris yang pasangannya NULL. Pemeriksaan wajib dijalankan dua arah, karena setiap arah menunjuk masalah yang berbeda: pesanan tanpa pelanggan biasanya berarti data master belum tersinkron, sedangkan pembayaran tanpa pesanan bisa berarti pesanan justru hilang di tengah pipeline.
Konsistensi Agregat dengan Toleransi #
Membandingkan SUM(total) dari tabel pesanan dengan SUM(jumlah) dari tabel pembayaran jarang menghasilkan kesamaan sempurna: pembulatan desimal, biaya layanan yang dipotong di satu sisi, dan event yang masih bergerak antar sistem membuat selisih kecil menjadi hal yang wajar. Karena itu perbandingan agregat selalu memakai toleransi — misalnya selisih dianggap aman bila tidak melebihi 1 rupiah atau 0,5% dari nilai pembanding, mana yang lebih besar. Toleransi ganda ini menangani dua skala sekaligus: angka kecil (pembulatan Rp0,01) dan angka besar (0,5% dari jutaan rupiah).
Uji Konsistensi dengan DuckDB #
Tiga tabel kecil TokoKita dimuat ke DuckDB in-memory: pelanggan berisi tiga pelanggan terdaftar, pesanan memuat enam baris termasuk satu rujukan ke pelanggan yang tidak ada, dan pembayaran memuat tujuh baris termasuk satu pembayaran tanpa pesanan serta satu selisih pembulatan Rp0,01. Seluruh query berikut adalah SQL standar yang bisa dijalankan di mesin SQL analitik mana pun; DuckDB kita pakai karena berjalan lokal tanpa server (dokumentasi di duckdb.org/docs
):
"""Uji konsistensi antar tabel dengan DuckDB: foreign key dan agregat lintas tabel."""
import duckdb
import pandas as pd
pd.set_option("display.float_format", "{:,.2f}".format)
con = duckdb.connect()
con.execute("CREATE TABLE pelanggan (customer_id INTEGER, nama VARCHAR)")
con.execute("INSERT INTO pelanggan VALUES (1, 'Budi'), (2, 'Siti'), (3, 'Dewi')")
con.execute("""CREATE TABLE pesanan (
order_id VARCHAR, customer_id INTEGER, total DECIMAL(12,2), tanggal DATE)""")
con.execute("""INSERT INTO pesanan VALUES
('ORD-701', 1, 150000.00, DATE '2026-10-20'),
('ORD-702', 2, 320000.00, DATE '2026-10-20'),
('ORD-703', 3, 99000.00, DATE '2026-10-20'),
('ORD-704', 1, 210000.00, DATE '2026-10-19'),
('ORD-705', 9, 87000.00, DATE '2026-10-19'),
('ORD-706', 2, 175000.00, DATE '2026-10-19')""")
con.execute("""CREATE TABLE pembayaran (
order_id VARCHAR, jumlah DECIMAL(12,2), tanggal DATE)""")
con.execute("""INSERT INTO pembayaran VALUES
('ORD-701', 150000.00, DATE '2026-10-20'),
('ORD-702', 320000.00, DATE '2026-10-20'),
('ORD-703', 98999.99, DATE '2026-10-20'),
('ORD-704', 210000.00, DATE '2026-10-19'),
('ORD-705', 87000.00, DATE '2026-10-19'),
('ORD-706', 175000.00, DATE '2026-10-19'),
('ORD-707', 64000.00, DATE '2026-10-19')""")
# 1. Konsistensi referensial: orphaned foreign key di dua arah.
orphan_pesanan = con.execute("""
SELECT p.order_id, p.customer_id
FROM pesanan p
LEFT JOIN pelanggan c ON p.customer_id = c.customer_id
WHERE c.customer_id IS NULL""").fetchall()
orphan_bayar = con.execute("""
SELECT b.order_id, b.jumlah
FROM pembayaran b
LEFT JOIN pesanan p ON b.order_id = p.order_id
WHERE p.order_id IS NULL""").fetchall()
print("konsistensi referensial:")
print(f" pesanan tanpa pelanggan : {len(orphan_pesanan)} -> {orphan_pesanan}")
print(f" pembayaran tanpa pesanan: {len(orphan_bayar)} -> {orphan_bayar}")
# 2. Konsistensi agregat: total pesanan vs total pembayaran per tanggal, dengan toleransi.
rekap = con.sql("""
WITH agg AS (
SELECT COALESCE(a.tanggal, b.tanggal) AS tanggal,
COALESCE(a.jumlah, 0) AS total_pesanan,
COALESCE(b.jumlah, 0) AS total_pembayaran
FROM (SELECT tanggal, SUM(total) AS jumlah FROM pesanan GROUP BY tanggal) a
FULL JOIN (SELECT tanggal, SUM(jumlah) AS jumlah FROM pembayaran GROUP BY tanggal) b
ON a.tanggal = b.tanggal
)
SELECT tanggal,
CAST(total_pesanan AS DOUBLE) AS total_pesanan,
CAST(total_pembayaran AS DOUBLE) AS total_pembayaran,
CAST(total_pembayaran - total_pesanan AS DOUBLE) AS selisih,
CASE WHEN ABS(total_pembayaran - total_pesanan)
<= GREATEST(1, 0.005 * total_pesanan)
THEN 'OK' ELSE 'PERIKSA' END AS status
FROM agg
ORDER BY tanggal""").to_df()
print("\nkonsistensi agregat per tanggal:")
print(rekap.to_string(index=False))Output yang Dihasilkan:
konsistensi referensial:
pesanan tanpa pelanggan : 1 -> [('ORD-705', 9)]
pembayaran tanpa pesanan: 1 -> [('ORD-707', Decimal('64000.00'))]
konsistensi agregat per tanggal:
tanggal total_pesanan total_pembayaran selisih status
2026-10-19 472,000.00 536,000.00 64,000.00 PERIKSA
2026-10-20 569,000.00 568,999.99 -0.01 OKMari kita telaah baris-baris kode di atas:
- Baris 12-31 (tiga tabel): data uji sengaja menanam tiga anomali —
ORD-705merujuk pelanggan 9 yang tidak ada,ORD-707adalah pembayaran tanpa pesanan, dan pembayaranORD-703lebih kecil Rp0,01 dari pesanannya. - Baris 34-43 (anti-join dua arah):
LEFT JOINmempertahankan semua baris sisi kiri; pasangan yang tidak ditemukan membuat kolom tabel kanan bernilaiNULL, dan filterIS NULLitulah yang mengisolasi baris orphaned. - Baris 50-58 (agregasi per tanggal): subquery menjumlahkan tiap tabel per tanggal, lalu
FULL JOINmenyatukannya —COALESCEmenjaga tanggal yang hanya ada di salah satu sisi tetap tampil. - Baris 63-65 (toleransi ganda):
GREATEST(1, 0.005 * total_pesanan)memilih batas yang lebih longgar antara 1 rupiah absolut dan 0,5% relatif;CASE WHENmengubah perbandingan menjadi status yang bisa langsung difilter. - Baris 69-70 (cetak):
con.sql(...).to_df()mengubah hasil query menjadi DataFrame pandas murni untuk pencetakan rapi.
Hasilnya membedakan dua situasi dengan tegas: tanggal 20 Oktober berstatus OK karena selisih -0,01 jauh di bawah toleransi — pembulatan yang wajar; tanggal 19 Oktober berstatus PERIKSA karena selisih 64.000 justru persis senilai pembayaran ORD-707 yang tidak memiliki pesanan. Angka selisih yang cocok dengan nilai baris orphaned adalah petunjuk investigasi yang sangat kuat: dua temuan berbeda pada uji konsistensi ternyata satu kejadian yang sama.
Ketika Konsistensi Dilanggar #
Selisih agregat yang melebihi toleransi hampir selalu berasal dari salah satu penyebab berikut:
- Penulisan ganda tanpa transaksi: dua sistem ditulis oleh proses terpisah — satu berhasil, satu gagal — sehingga salinan datanya berbeda.
- Perbedaan titik potong: laporan pesanan dipotong pukul 00.00, laporan pembayaran pukul 00.05; lima menit transaksi tersebut hanya di salah satu sisi.
- Baris ganda: duplikasi di satu tabel menggelembungkan agregatnya — cek uniqueness sebelum menuduh proses bisnis.
- Transformasi yang tidak disadari: join atau filter di salah satu pipeline membuang baris yang dianggap penting oleh pihak lain.
Karena uji konsistensi selalu melibatkan lebih dari satu tabel, pertanyaan berikutnya hampir pasti muncul: dari mana data tiap tabel ini berasal, dan tahap pipeline mana yang menyentuhnya terakhir? Menelusuri jejak asal-usul data itulah topik chapter berikutnya: Data Lineage.