Data bersih yang tersimpan di data/staging/ masih berbentuk berkas datar; dashboard butuh tabel dengan tipe yang benar dan grain yang jelas. Pada bagian ini kita merancang model analitik gaya dbt pada folder models/: tabel sumber dimuat apa adanya, model staging mengkoersi dan menurunkan ulang kolom, lalu fact table menyimpan grain terhalus yang dibutuhkan seluruh metrik. Struktur berlapis ini persis pola staging-marts yang dibahas pada chapter Model Staging, Intermediate, dan Marts dan dbt; panduan resmi penulisan model SQL dbt ada di https://docs.getdbt.com/docs/build/sql-models
. Bedanya dengan proyek data warehouse sebelumnya: kita tidak membangun star schema lengkap, melainkan mart analitik yang lebih ramping — pilihannya kita bahas di akhir bagian dan merujuk chapter Data Marts.
Peta Model Berlapis #
Alur model dari sumber sampai marts digambarkan sebagai berikut; tiga lapisan pertama dibangun pada bagian ini, marts menyusul pada Bagian 6:
Setiap lapisan punya satu tanggung jawab:
| Lapisan | Tabel | Tanggung jawab |
|---|---|---|
| sumber | sumber_pesanan, sumber_events | memuat hasil pembersihan, seluruh kolom VARCHAR |
| staging | stg_pesanan, stg_events | koersi tipe dan penurunan kolom baru |
| fact | fct_pesanan | grain satu baris item pesanan + penanda status |
| marts | mkt_* | agregat siap dashboard (Bagian 6) |
Model Staging Pesanan #
models/stg_pesanan.sql mengkoersi seluruh kolom dan menurunkan ulang total dari komponen:
-- Model staging pesanan: koersi tipe dan penurunan ulang total.
-- Sumber: tabel sumber_pesanan (hasil pembersihan, seluruh kolom VARCHAR).
-- Total diturunkan dari komponen (kuantitas * harga_satuan - diskon), bukan
-- disalin dari kolom total, sehingga satu sumber kebenaran untuk nilai jual.
WITH bertipe AS (
SELECT
pesanan_id,
CAST(tanggal AS DATE) AS tanggal,
pelanggan_id,
kanal,
produk_id,
nama_produk,
kategori,
CAST(kuantitas AS INTEGER) AS kuantitas,
CAST(harga_satuan AS BIGINT) AS harga_satuan,
CAST(diskon AS BIGINT) AS diskon,
CAST(total AS BIGINT) AS total_sumber,
status
FROM sumber_pesanan
)
SELECT
pesanan_id,
tanggal,
pelanggan_id,
kanal,
produk_id,
nama_produk,
kategori,
kuantitas,
harga_satuan,
diskon,
kuantitas * harga_satuan - diskon AS total,
total_sumber,
status
FROM bertipeMari kita telaah bagian-bagian model di atas:
- CTE
bertipe: seluruhCASTterkumpul di satu tempat; selektor luar tinggal memakai kolom yang sudah bertipe, sehingga ekspresikuantitas * harga_satuan - diskontidak perlu mengulangCAST. - Kolom
total_sumber: total asli dari hasil pembersihan tetap disimpan di sampingtotalhasil hitung; selisih keduanya sudah nol karena Bagian 3 memperbaiki total, tetapi kolom ini menjaga jejak audit. kuantitas * harga_satuan - diskon AS total: nilai jual diturunkan dari komponen di dalam SQL, prinsip yang sama dengan kolom kalkulasi dbt.
Model Fact #
models/fct_pesanan.sql menetapkan grain dan menambahkan penanda yang dipakai seluruh marts:
-- Model fact: satu baris per item pesanan (pesanan_id, produk_id).
-- Atribut produk sengaja dibiarkan terdenormalisasi (nama_produk, kategori)
-- karena model ini adalah mart analitik untuk dashboard, bukan star schema
-- gudang data; lihat penjelasan pada Bagian 4 halaman proyek.
SELECT
pesanan_id,
tanggal,
strftime(tanggal, '%Y-%m') AS bulan,
pelanggan_id,
kanal,
produk_id,
nama_produk,
kategori,
kuantitas,
harga_satuan,
diskon,
total,
status,
status = 'selesai' AS is_selesai
FROM stg_pesananTiga keputusan penting pada model tersebut:
- Grain satu baris item pesanan: kunci alami adalah pasangan
pesanan_id+produk_id; pesanan dua produk menempati dua baris, sama sepertifact_pesananpada proyek gudang data. strftime(tanggal, '%Y-%m') AS bulan: kolom bulan diturunkan sekali di sini agar marts tidak mengulang logika format tanggal.status = 'selesai' AS is_selesai: penanda boolean ini mengunci definisi GMV dari Bagian 1 — setiap mart yang memfilteris_selesaiotomatis konsisten denganfilter_statuspadametric_definitions.yaml.
Eksekusi Nyata Model #
Model bukan sekadar berkas SQL; src/models_runner.py menjalankan ketiganya melalui DuckDB dengan pola CREATE OR REPLACE TABLE <nama_model> AS <select>. Skrip berikut mengeksekusi seluruh model pada database di memori, lalu memeriksa struktur hasilnya:
from pathlib import Path
import duckdb
from src import models_runner
con = duckdb.connect(":memory:")
jumlah = models_runner.jalankan_semua(
con,
Path("models"),
Path("data/staging/pesanan_bersih.csv"),
Path("data/staging/events_bersih.jsonl"),
)
print("eksekusi model (database di memori):")
for nama, baris in jumlah.items():
print(f" {nama:16s} {baris:4d} baris")
print("struktur fct_pesanan:")
kolom = con.execute("DESCRIBE fct_pesanan").fetchall()
for nama, tipe in [(k[0], k[1]) for k in kolom]:
print(f" {nama:14s} {tipe}")Output yang Dihasilkan:
eksekusi model (database di memori):
sumber_pesanan 33 baris
sumber_events 112 baris
stg_pesanan 33 baris
stg_events 112 baris
fct_pesanan 33 baris
struktur fct_pesanan:
pesanan_id VARCHAR
tanggal DATE
bulan VARCHAR
pelanggan_id VARCHAR
kanal VARCHAR
produk_id VARCHAR
nama_produk VARCHAR
kategori VARCHAR
kuantitas INTEGER
harga_satuan BIGINT
diskon BIGINT
total BIGINT
status VARCHAR
is_selesai BOOLEANBaris tidak bertambah maupun berkurang dari staging ke fact — 33 masuk, 33 keluar — karena model ini murni transformasi kolom, bukan agregasi. Tipe kolom hasil DESCRIBE membuktikan koersi bekerja: tanggal menjadi DATE, total menjadi BIGINT, dan is_selesai menjadi BOOLEAN.
Mart Ramping, Bukan Star Schema #
Keputusan desain terakhir: mengapa fct_pesanan menyimpan nama_produk dan kategori secara terdenormalisasi, padahal proyek gudang data memisahkannya ke dim_produk? Karena tujuan model ini berbeda. Gudang data melayani analisis lintas domain dalam jangka panjang sehingga normalisasi ke dimensi terpisah terbayar; mart analitik melayani satu dashboard dengan angka tetap, sehingga denormalisasi membuat query mart lebih pendek dan pipeline lebih sederhana. Ketika kebutuhan bertambah — misalnya riwayat perubahan harga produk — saat itulah atribut naik ke tabel dimensi tersendiri, sebagaimana dibahas pada chapter Data Marts.
Tiga model inti sudah berdiri dan terbukti tereksekusi. Pada Bagian 5 kita merangkai seluruh tahap menjadi satu pipeline yang bisa dijalankan dengan satu perintah.