Membangun budaya data quality yang terukur: empat dimensi inti (freshness, volume, schema, dan content), implementasi dengan dbt tests, Great Expectations, dan Soda, serta praktik merancang data quality framework yang berjalan di pipeline

Di episode 13 pipeline kalian sudah bisa menunggu, gagal dengan benar, dan pulih. Tapi ada satu pertanyaan yang belum dijawab: bagaimana kalian tahu data yang dikirim ke konsumen itu benar? Pipeline yang "berhasil" dengan data kosong, data ganda, atau schema yang berubah diam-diam adalah kegagalan yang paling berbahaya — karena tidak ada yang menyadarinya.
Episode ini membangun data quality framework: empat dimensi yang bisa diukur, tool yang mengotomatisasi pengecekannya (dbt tests, Great Expectations, Soda), dan cara menempatkannya sebagai quality gate di pipeline — bukan sekadar laporan yang dibaca kalau sempat.
Semua masalah kualitas data bisa dikelompokkan ke empat dimensi yang terukur:
| Dimensi | Pertanyaan | Contoh Check |
|---|---|---|
| Freshness | Apakah data terbarui sesuai jadwal? | max(created_at) masih di bawah 12 jam |
| Volume | Apakah jumlah data masuk akal? | Baris hari ini vs rata-rata 7 hari (±20%) |
| Schema | Apakah struktur masih sesuai kontrak? | Kolom yang diharapkan ada, tipe data benar |
| Content | Apakah isi data valid? | Tidak ada NULL pada key, nilai dalam rentang wajar |
Empat dimensi ini adalah minimum viable data quality. Tanpa keempatnya, data "berhasil dimuat" belum berarti apa-apa.
Kedua dimensi ini paling sering diabaikan, padahal paling cepat mendeteksi kegagalan sumber. Freshness menjawab: sumber mati tanpa kita sadari? Volume menjawab: sumber mengirim separuh data tanpa kita sadari?
-- Freshness: data terakhir kapan?
SELECT max(ingested_at) AS last_data
FROM raw_orders;
-- Volume: perbandingan vs kemarin
WITH today AS (
SELECT COUNT(*) AS n FROM raw_orders WHERE ingested_at::date = CURRENT_DATE
), yesterday AS (
SELECT COUNT(*) AS n FROM raw_orders WHERE ingested_at::date = CURRENT_DATE - 1
)
SELECT
today.n AS today,
yesterday.n AS yesterday,
CASE WHEN yesterday.n = 0 THEN NULL
ELSE ROUND((today.n - yesterday.n)::numeric / yesterday.n, 3)
END AS delta_pct
FROM today, yesterday;Di dbt, keduanya sudah diformalkan — freshness lewat sources.yml (episode 7), volume lewat test kustom:
-- Gagal jika volume hari ini turun > 30% dibanding kemarin
WITH vol AS (
SELECT
COUNT(*) FILTER (WHERE DATE(created_at) = CURRENT_DATE) AS today,
COUNT(*) FILTER (WHERE DATE(created_at) = CURRENT_DATE - 1) AS yesterday
FROM {{ ref('stg_orders') }}
)
SELECT * FROM vol
WHERE today < 0.7 * yesterday;dbt tests (episode 7) adalah fondasi: test generik (unique, not_null, accepted_values, relationships) dan test kustom sebagai query yang harus kosong. Kuncinya adalah severity — tidak semua kegagalan harus menghentikan pipeline:
models:
- name: fct_orders
tests:
- dbt_utils.accepted_range:
column_name: total_amount
min_value: 0
severity: error # blokir jika negatif
- name: stg_orders
tests:
- dbt_utils.expression_is_true:
expression: "length(CAST(order_id AS TEXT)) > 0"
severity: warn # catat tapi jangan blokirStrategi severity yang sehat:
error untuk pelanggaran critical path: key NULL, amount negatif, referensi rusak.warn untuk anomali yang perlu diketahui tapi tidak memblokir: outlier volume, nilai jarang.Tip
Quality gate bukan berarti pipeline harus berhenti di setiap anomali. Pipeline yang sehat membedakan "gagal segera" (kesalahan permanen) dari "catat dulu" (anomali sementara). Menyetel semua test sebagai error akan membuat tim mati rasa terhadap alarm.
dbt menguji hasil model SQL. Untuk validasi file/data di luar SQL — data frame Python, file landing, sebelum masuk ke database — gunakan framework validasi khusus.
Framework validasi data dengan Expectations: deklarasi assert seperti expect_column_values_to_not_be_null. Biasanya dijalankan setelah data landing sebelum di-load:
import great_expectations as gx
context = gx.get_context()
suite = context.add_expectation_suite("orders_suite")
suite.add_expectation(
gx.expectations.ExpectColumnValuesToNotBeNull,
column="order_id",
)
suite.add_expectation(
gx.expectations.ExpectColumnValuesToBeBetween,
column="amount",
min_value=0,
)
suite.add_expectation(
gx.expectations.ExpectColumnMeanToBeBetween,
column="amount",
min_value=10,
max_value=100000,
)
df = gx.read_pandas(pd.read_csv("landing/orders.csv"))
result = df.validate(suite)
if not result["success"]:
raise SystemExit("VALIDATION FAILED: data tidak lolos quality gate")
print("OK: semua expectations lolos")Keunggulan Great Expectations: validasi bisa dijalankan sebelum data masuk database — menghentikan data buruk di pintu, bukan setelah mencemari tabel.
Soda berfokus pada data warehouse dengan konfigurasi YAML yang ringkas dan integrasi alerting yang baik:
checks for raw_orders:
- freshness(ingested_at) < 12h
- row_count > 0
- missing_count(amount) = 0
- invalid_count(amount) < 10:
valid min: 0
valid max: 100000000freshness(ingested_at) < 12h menyatakan batch check dalam satu file yang dibaca orang: kondisi, bukan kode. Soda bisa dijalankan di Airflow dan mengirim hasil ke Slack — sangat cocok sebagai quality gate harian.
Rangkai semuanya menjadi framework yang berjalan di pipeline Airflow:
error gagal, pipeline berhenti dan notify; jika warn, lanjut tapi tercatat di observability.python ge_validate.py &&
dbt build --select state:modified &&
dbt test --select severity:error &&
soda scan -d de_lab -c soda/configuration.yml soda/checks/orders.ymlUrutan && memastikan: data tidak maju ke tahap berikutnya jika gagal di tahap ini — fail fast, fail loudly.
Hanya menguji content, lupa freshness & volume. Sumber mati tak terdeteksi sampai berhari-hari. Empat dimensi wajib lengkap.
Semua test severity: error. Alarm yang selalu berbunyi membuat tim abai. Pisahkan blokir vs peringatan.
Validasi setelah data mencemari warehouse. Validasi di landing (sebelum DB) jauh lebih murah daripada bersih-bersih setelahnya.
Quality check tanpa pemilik. Check yang tidak ada yang bertanggung jawab akan mati sendiri. Tetapkan pemilik tiap dataset.
Di episode 14 ini kalian telah membangun fondasi data quality:
error vs warn yang disengaja.Di episode 15 selanjutnya kita membahas tata kelola: data governance & lineage — data catalog, lineage, access control, dan metadata management dengan DataHub & OpenMetadata, plus praktik setup catalog & lineage. Sampai jumpa di episode 15!