Chapter Benchmarking menutup section Optimasi dan Performa dengan prinsip bahwa klaim performa harus berdiri di atas angka yang diukur dengan benar. Section Arsitektur Data ini membuka pertanyaan yang lebih besar: setelah setiap pipeline terbukti cepat, bagaimana seluruh pipeline tersebut disusun agar data perusahaan tetap teratur dan bisa dipercaya? Jawaban paling tua sekaligus paling tahan lama adalah layered data architecture — memisahkan data mentah dari data bersih dan agregat ke dalam layer yang masing-masing punya kontrak jelas. Pola ini menjadi fondasi bagi Medallion, Lambda, hingga Kappa yang kita bahas sepanjang section ini.
Masalah Tabel Tunggal #
Banyak tim memulai perjalanan data dengan satu tabel besar yang dibaca semua orang. Pendekatan tersebut terasa praktis pada awalnya, tetapi tiga masalah muncul segera setelah jumlah konsumen dan sumber bertambah:
- Baris kotor — duplikat, pesanan batal, nilai minus — langsung terlihat di dashboard karena tidak ada area pembersihan.
- Logika transformasi tidak berani diubah karena tidak ada salinan data asli untuk mengulang proses dari awal.
- Dua laporan bisa menunjukkan angka berbeda karena setiap konsumen menafsirkan tabel yang sama dengan aturan yang berbeda.
Layered architecture menjawab dengan aturan satu arah: data mengalir dari layer mentah ke layer turunan, dan tidak pernah sebaliknya. Chapter Zona Data Raw dan Curated pada section Data Lake dan Lakehouse telah memperkenalkan zona serupa di level penyimpanan; bab ini menyusun zona tersebut menjadi pipeline yang bisa dijalankan ulang.
Tiga Layer: Raw, Staging, Mart #
Pola tiga layer membagi perjalanan data menjadi tiga tanggung jawab yang terpisah. Layer raw menyimpan data apa adanya sebagai arsip, layer staging membersihkan dan menegakkan tipe, dan layer mart menyajikan agregat siap konsumsi. Setiap layer hanya boleh membaca layer sebelumnya, sehingga setiap tabel turunan selalu bisa dibangun ulang dari raw.
Ringkasan kontrak tiap layer:
| Layer | Isi | Kontrak | Konsumen utama |
|---|---|---|---|
| Raw | data apa adanya, termasuk baris kotor | append-only, tidak pernah diubah | pipeline turunan, audit |
| Staging | hasil pembersihan dan pengetatan tipe | dedup, validasi, tipe eksplisit | tim data, model transformasi |
| Mart | agregat siap baca | hanya status final, schema stabil | dashboard, laporan bisnis |
Kontrak Layer dalam Praktik #
Kita praktikkan dengan DuckDB memakai data pesanan TokoKita. Dua belas baris berikut sengaja memuat masalah nyata: satu duplikat (9003), satu status batal, satu status pending, dan satu baris dengan total minus. Layer raw memuat semuanya tanpa pembersihan:
import duckdb
con = duckdb.connect()
# --- Layer raw: muat apa adanya, tanpa pembersihan ---
con.execute("""
CREATE TABLE raw_pesanan AS
SELECT * FROM (VALUES
(9001, '2026-01-15 09:12', 'Budi', 'kopi', 2, 45000, 90000, 'lunas'),
(9002, '2026-01-15 10:05', 'Siti', 'teh', 1, 16000, 16000, 'lunas'),
(9003, '2026-01-15 11:40', 'Dewi', 'kopi', 3, 12000, 36000, 'lunas'),
(9003, '2026-01-15 11:40', 'Dewi', 'kopi', 3, 12000, 36000, 'lunas'),
(9004, '2026-01-15 13:02', 'Budi', 'alat', 1, 55000, 55000, 'batal'),
(9005, '2026-01-16 08:20', 'Siti', 'kopi', 2, 52000, 104000, 'lunas'),
(9006, '2026-01-16 09:15', 'Budi', 'teh', 1, 18000, 18000, 'pending'),
(9007, '2026-01-16 10:30', 'Dewi', 'kopi', 1, 60000, 60000, 'lunas'),
(9008, '2026-01-16 12:00', 'Rina', 'alat', 1, 35000, -35000, 'lunas'),
(9009, '2026-01-16 14:45', 'Budi', 'teh', 2, 12000, 24000, 'lunas'),
(9010, '2026-01-16 18:22', 'Siti', 'kopi', 1, 60000, 60000, 'lunas'),
(9011, '2026-01-16 20:40', 'Dewi', 'alat', 1, 48000, 48000, 'lunas')
) AS t(id_pesanan, waktu, pelanggan, kategori, jumlah, harga, total, status)
""")Kontrak layer staging dan mart kemudian ditulis sebagai SQL murni yang bisa dijalankan ulang kapan pun. Fungsi bangun_turunan membangun kedua tabel turunan dari nol — inilah mesin reprocessability:
# --- Kontrak tiap layer sebagai SQL yang bisa dijalankan ulang ---
SQL_STAGING = """
CREATE TABLE stg_pesanan AS
SELECT id_pesanan, waktu, pelanggan, kategori,
jumlah::INTEGER AS jumlah, harga::BIGINT AS harga,
total::BIGINT AS total, status
FROM (
SELECT *, row_number() OVER (
PARTITION BY id_pesanan ORDER BY waktu) AS n
FROM raw_pesanan
WHERE total > 0
) WHERE n = 1
"""
SQL_MART = """
CREATE TABLE mart_penjualan AS
SELECT substr(waktu, 1, 10) AS tanggal, kategori,
count(*) AS jumlah_pesanan, sum(total) AS pendapatan
FROM stg_pesanan
WHERE status = 'lunas'
GROUP BY 1, 2
"""
def bangun_turunan():
"""Jalankan ulang seluruh transform dari raw; kembalikan isi mart."""
con.execute("DROP TABLE IF EXISTS mart_penjualan")
con.execute("DROP TABLE IF EXISTS stg_pesanan")
con.execute(SQL_STAGING)
con.execute(SQL_MART)
return con.execute(
"SELECT * FROM mart_penjualan ORDER BY tanggal, kategori").fetchall()Baris pencetakan laporan per layer dipangkas dari tampilan agar ringkas; skrip aslinya juga menjalankan bangun_turunan dua kali untuk uji rebuild. Hasil eksekusi lengkapnya:
Output yang Dihasilkan:
[raw] 12 baris dimuat apa adanya
isi kotor tetap tersimpan: 1 duplikat (9003), 1 status batal, 1 status pending, 1 total minus
[staging] 10 baris setelah dedup dan filter total > 0
[mart] 5 baris agregat (tanggal x kategori, hanya status lunas):
2026-01-15 kopi 2 pesanan Rp126,000
2026-01-15 teh 1 pesanan Rp16,000
2026-01-16 alat 1 pesanan Rp48,000
2026-01-16 kopi 3 pesanan Rp224,000
2026-01-16 teh 1 pesanan Rp24,000
rebuild staging + mart dari raw menghasilkan mart identik: True
raw tidak pernah berubah: TrueMari kita telaah baris-baris kode di atas:
- Blok pertama, baris 6-22 (
CREATE TABLE raw_pesanan): dua belas baris pesanan dimuat apa adanya, termasuk duplikat9003dan satu baris ber-totalminus; tidak ada satu pun pembersihan di layer ini. - Blok kedua, baris 2-13 (
SQL_STAGING):row_number() OVER (PARTITION BY id_pesanan ORDER BY waktu)memberi nomor urut per pesanan, laluWHERE n = 1memilih kemunculan pertama sehingga duplikat hilang;WHERE total > 0membuang nilai minus; cast::INTEGERdan::BIGINTmenegakkan tipe kolom. - Blok kedua, baris 15-22 (
SQL_MART):substr(waktu, 1, 10)memotong tanggal,WHERE status = 'lunas'menyaring status final, dan agregasicount/summenghasilkan tabel siap dashboard. - Blok kedua, baris 25-32 (
bangun_turunan): kedua tabel turunan di-DROPlalu dibangun ulang murni dariraw_pesanan— uji reprocessability dijalankan dua kali oleh skrip.
Jaminan Reprocessability #
Dua baris terakhir output adalah inti bab ini: membangun ulang staging dan mart dari raw menghasilkan mart yang identik, dan raw tidak berubah setelahnya. Kombinasi keduanya memberi jaminan kuat. Ketika logika transformasi ternyata keliru, kita cukup memperbaiki kontrak lalu menjalankan ulang bangun_turunan tanpa kehilangan data asli.
Seluruh contoh bab ini berjalan di DuckDB yang dipakai proyek ini; rujukan SQL lengkapnya ada di dokumentasi DuckDB .
Tiga layer adalah kerangka minimum yang bekerja untuk sebagian besar kasus. Ketika platform berpindah ke lakehouse dengan table format seperti Delta Lake atau Apache Iceberg — yang sudah kita bahas di section Data Lake dan Lakehouse — pola yang sama tumbuh menjadi tiga lapis bernama Bronze, Silver, Gold dengan gerbang kualitas eksplisit. Chapter berikutnya, Medallion Architecture, membedah pola tersebut satu per satu.