Proyek dbt TokoKita dari Chapter dbt baru berisi tiga model, tetapi TokoKita yang sesungguhnya membaca banyak sumber: pesanan dari aplikasi, pelanggan dari CRM, produk dari master katalog, retur dari sistem gudang. Tanpa pengelompokan, folder models/ berubah menjadi tumpukan berkas yang tidak jelas mana yang boleh dibaca siapa. Komunitas dbt menjawabnya dengan konvensi layer: staging, intermediate, dan marts — pembagian yang juga mengingatkan kita pada pemisahan raw dan curated di section Data Modeling dan Data Warehouse. Chapter ini membangun ketiga layer tersebut dengan DuckDB sebagai simulasi warehouse.
Tiga Layer Model #
| Layer | Awalan | Isi | Pembaca |
|---|---|---|---|
| staging | stg_ | salinan bersih 1:1 dari satu source: rename kolom, menyeragamkan nilai, cast tipe | model lain |
| intermediate | int_ | join antar staging plus business logic | model lain |
| marts | fct_, dim_ | tabel siap konsumen: star schema, wide table, metrik final | dashboard BI, analis |
Pembagian ini menjawab dua pertanyaan sekaligus. Pertanyaan pertama: di mana pembersihan dilakukan? Di staging, tepat setelah sumber — satu tempat untuk seluruh proyek. Pertanyaan kedua: tabel mana yang boleh dibaca pengguna akhir? Hanya marts; dua layer di bawahnya adalah dapur dalam yang boleh berubah tanpa mengganggu laporan. Konfigurasi +materialized pada dbt_project.yml di Chapter dbt mengikuti pembagian yang sama: staging diwujudkan sebagai view yang murah dibangun ulang, marts sebagai table yang cepat dibaca alat BI. Konsep model dan materialisasi selengkapnya terdokumentasi di dokumentasi model dbt
.
Alur Antar Layer #
Bentuk akhir yang kita kejar untuk TokoKita tampak seperti ini:
Setiap panah hanya boleh bergerak maju ke layer berikutnya, tidak pernah mundur. Tabel mentah app_pesanan dan crm_pelanggan disentuh tepat satu kali oleh masing-masing model staging; selebihnya model saling memanggil lewat ref(). Dibanding DAG dua model pada Chapter dbt, pekerjaan join pelanggan yang tadinya melekat di fct_pesanan kini dipindahkan ke int_pesanan — mart tinggal membaca hasil akhirnya. Perhatikan juga dim_pelanggan: dimensi yang hanya butuh pembersihan ringan boleh melompat dari staging ke mart tanpa mampir ke intermediate — layer dilewati, bukan dilanggar.
Pada berkas proyek, ketiga layer hidup sebagai subfolder models/staging/, models/intermediate/, dan models/marts/ — persis pola yang dbt_project.yml pada Chapter dbt konfigurasikan lewat staging: +materialized: view dan marts: +materialized: table. Model intermediate yang belum diberi konfigurasi sendiri mengikuti materialisasi bawaan dbt, yaitu view, sehingga dbt tidak pernah menulis tabel fisik tanpa kita minta.
Implementasi: Membersihkan, Merangkai, Menyajikan #
Sekarang kita jalankan ketiga layer dalam satu skrip DuckDB. Sumber aplikasi menyimpan kolom dengan nama asing (ord_id, cust_email), status yang tidak seragam (paid, PAID, Pending), dan timestamp yang tersimpan sebagai string — persis kondisi mentah yang staging harus tangani. Keuntungan strukturnya terasa saat sumber berubah: bila tim aplikasi kelak mengganti ord_id menjadi order_uuid, hanya stg_pesanan yang perlu disunting, sementara int_pesanan, fct_pesanan, dan seluruh laporan di hilirnya tetap membaca kolom id_pesanan yang sudah distandarkan. Blok di bawah memotong bagian awal skrip yang membuat dua tabel mentah tersebut beserta salinan bersih stg_pelanggan:
print("=== layer 1: stg_pesanan (1:1 dari sumber, rename + clean + cast) ===")
con.execute("""
CREATE TABLE stg_pesanan AS
SELECT
ord_id AS id_pesanan,
lower(trim(cust_email)) AS email,
upper(ord_status) AS status, -- 'paid'/'PAID' -> 'PAID'
CAST(ord_ts AS TIMESTAMP) AS dibuat_pada, -- string -> timestamp
qty,
unit_price AS harga_satuan
FROM raw_app_pesanan
""")
for r in con.execute(
"""SELECT id_pesanan, email, status,
strftime(dibuat_pada, '%Y-%m-%d %H:%M'), harga_satuan
FROM stg_pesanan ORDER BY id_pesanan"""
).fetchall():
print(" ", r)
print()
print("=== layer 2: int_pesanan (join sumber lain + business logic) ===")
con.execute("""
CREATE TABLE int_pesanan AS
SELECT
s.id_pesanan,
s.dibuat_pada,
s.status,
c.kota,
c.segmen,
s.qty * s.harga_satuan AS total,
s.qty * s.harga_satuan >= 300000 AS is_pesanan_besar
FROM stg_pesanan s
LEFT JOIN stg_pelanggan c ON c.email = s.email
""")
for r in con.execute(
"SELECT id_pesanan, kota, total, is_pesanan_besar FROM int_pesanan ORDER BY id_pesanan"
).fetchall():
print(" ", r)Output yang Dihasilkan:
=== layer 1: stg_pesanan (1:1 dari sumber, rename + clean + cast) ===
('ORD-1', '[email protected]', 'PAID', '2026-01-10 09:15', 250000)
('ORD-2', '[email protected]', 'PAID', '2026-01-10 14:30', 180000)
('ORD-3', '[email protected]', 'PENDING', '2026-01-11 08:05', 120000)
=== layer 2: int_pesanan (join sumber lain + business logic) ===
('ORD-1', 'Jakarta', 250000, False)
('ORD-2', 'Bandung', 360000, True)
('ORD-3', 'Depok', 120000, False)Layer berikutnya menyaring dan menyajikan hasilnya untuk konsumen:
print()
print("=== layer 3: fct_pesanan di mart (siap konsumen BI) ===")
con.execute("""
CREATE TABLE fct_pesanan AS
SELECT
p.id_pesanan, p.dibuat_pada, p.status, p.kota, p.segmen, p.total
FROM int_pesanan p
WHERE p.status = 'PAID'
""")
for r in con.execute(
"SELECT id_pesanan, kota, segmen, total FROM fct_pesanan ORDER BY total DESC"
).fetchall():
print(" ", r)
print()
print("=== GMV per kota langsung dari mart ===")
for kota, gmv in con.execute(
"SELECT kota, SUM(total) AS gmv FROM fct_pesanan GROUP BY kota ORDER BY gmv DESC"
).fetchall():
print(f" {kota:8s} Rp{gmv}")Output yang Dihasilkan:
=== layer 3: fct_pesanan di mart (siap konsumen BI) ===
('ORD-2', 'Bandung', 'premium', 360000)
('ORD-1', 'Jakarta', 'reguler', 250000)
=== GMV per kota langsung dari mart ===
Bandung Rp360000
Jakarta Rp250000Mari kita telaah bagian-bagian penting skrip di atas:
- Blok
CREATE TABLE stg_pesanan: renameord_idmenjadiid_pesanan, membersihkan email denganlower(trim()), menyeragamkan status lewatupper(), dan meng-cast stringord_tsmenjadiTIMESTAMP. Tiga nilai status yang tadinya tidak seragam kini tampil sebagaiPAIDdanPENDING. - Blok
CREATE TABLE int_pesanan: menggabungkanstg_pesanandenganstg_pelangganlewat email, lalu menambahkan business logic — kolomtotaldan penandais_pesanan_besaruntuk ambang Rp300.000. - Blok
CREATE TABLE fct_pesanan: menyaring hanya pesanan berstatusPAID, sehingga pesananORD-3yang masihPENDINGtidak ikut menghitung GMV. Inilah satu-satunya definisi GMV di seluruh proyek: yang dibaca BI, yang diuji, yang dipercaya laporan.
Aturan Main Tiap Layer #
Konvensi hanya berguna bila ditegakkan. Empat aturan intinya:
- Staging membaca source, tidak pernah join ke tabel lain: satu model membersihkan satu sumber.
- Intermediate membaca staging atau intermediate lain, tidak membaca source langsung.
- Marts membaca intermediate atau staging, dan menjadi satu-satunya pintu bagi alat BI.
- Penamaan mengikuti layer:
stg_,int_,fct_,dim_— nama berkas langsung menunjukkan posisinya di DAG.
Satu hal penting belum kita bahas: fct_pesanan diwujudkan sebagai table yang ditulis ulang penuh setiap dbt run. Untuk ribuan baris hal tersebut selesai dalam hitungan detik, tetapi untuk jutaan baris riwayat pesanan, menulis ulang semuanya setiap hari adalah pemborosan compute. dbt menyediakan materialization khusus untuk masalah ini, materi Chapter Incremental Models.