Incremental model pada Chapter Incremental Models menjaga tabel target selalu mutakhir: koreksi menimpa nilai lama, dan nilai lama hilang begitu saja. Namun sebagian pertanyaan bisnis justru menuntut nilai lama tersebut — di kota mana Budi tinggal ketika pesanan Januari dibuat, sebelum pindah bulan berikutnya? Chapter Slowly Changing Dimensions di section Data Modeling dan Data Warehouse sudah membangun mekanisme SCD Type 2 secara manual: tutup baris lama, sisipkan versi baru, lalu join fact ke versi yang benar. dbt mengotomatiskan seluruh mekanisme tersebut lewat fitur snapshot: cukup satu berkas deklarasi di proyek, dan setiap kali dbt snapshot dipanggil, pola tutup-versi-lama-lalu-sisipkan-versi-baru berjalan tanpa satu pun UPDATE tulisan tangan.
Deklarasi Snapshot dalam YAML #
Snapshot dideklarasikan pada berkas YAML di direktori snapshots/ — direktori yang sudah terdaftar di snapshot-paths pada dbt_project.yml Chapter dbt. Deklarasi untuk tabel pelanggan TokoKita tampak seperti ini:
version: 2
snapshots:
- name: pelanggan_snapshot
relation: source('aplikasi_toko', 'pelanggan')
description: "Riwayat perubahan atribut pelanggan (SCD Type 2)"
config:
strategy: check
unique_key: id
check_cols: [kota]
hard_deletes: invalidateMari kita telaah baris-baris konfigurasi di atas:
- Baris 5 (
relation): tabel sumber yang dipantau — pelanggan dari deklarasisourcepada Chapter dbt. - Baris 8 (
strategy: check): cara dbt mendeteksi perubahan, dibahas setelah simulasi. - Baris 9 (
unique_key: id): pengenal baris sumber; satu pelanggan menghasilkan rangkaian versi yang tersambung lewatidyang sama. - Baris 10 (
check_cols: [kota]): hanya perubahan pada kolomkotayang menumbuhkan versi baru; perubahan kolom lain diabaikan. - Baris 11 (
hard_deletes: invalidate): bila baris terhapus dari sumber, versi current-nya ditutup dengandbt_valid_to, bukan dibiarkan menggantung.
Kita periksa struktur berkasnya dengan yaml.safe_load, parser yang sama yang dbt pakai membaca deklarasi snapshot:
"""Validasi YAML deklarasi snapshot bab 5 dengan yaml.safe_load."""
import yaml
from pathlib import Path
BASE = Path(__file__).parent
data = yaml.safe_load((BASE / "ch5_snapshots.yml").read_text(encoding="utf-8"))
snap = data["snapshots"][0]
print("=== ch5_snapshots.yml -> valid ===")
print(f"name : {snap['name']}")
print(f"relation : {snap['relation']}")
print(f"strategy : {snap['config']['strategy']}")
print(f"unique_key : {snap['config']['unique_key']}")
print(f"check_cols : {snap['config']['check_cols']}")
print(f"hard_deletes: {snap['config']['hard_deletes']}")Output yang Dihasilkan:
=== ch5_snapshots.yml -> valid ===
name : pelanggan_snapshot
relation : source('aplikasi_toko', 'pelanggan')
strategy : check
unique_key : id
check_cols : ['kota']
hard_deletes: invalidateEmpat Kolom yang dbt Tambahkan #
Tabel hasil snapshot berisi semua kolom sumber plus empat kolom meta. dbt_scd_id adalah hash pengenal versi baris; dbt_updated_at mencatat kapan dbt merekam baris tersebut; dbt_valid_from menandai sejak kapan versi berlaku; dan dbt_valid_to menandai akhir masa berlakunya — nilai NULL berarti versi tersebut masih current. Snapshot bersifat append-only: dbt tidak pernah mengubah isi kolom sumber pada baris lama, sehingga riwayat dapat diaudit. Simulasi di bawah menyederhanakan dbt_scd_id sebagai hash md5(id || '|' || kota) dan meniadakan dbt_updated_at agar logika intinya tetap terbaca.
Simulasi Dua Run Snapshot di DuckDB #
dbt tidak terpasang di lingkungan bab ini, jadi kita jalankan logika strategi check secara langsung di DuckDB. Blok di bawah memotong bagian awal skrip yang membuat tabel sumber pelanggan_sumber serta tabel snapshot kosong:
def jalankan_snapshot(tanggal):
"""Tiruan logika strategi check dbt: bandingkan hash, tutup lama, sisip baru."""
# 1) tutup versi current yang hash-nya tidak lagi cocok dengan sumber
con.execute(f"""
UPDATE snapshot_pelanggan
SET dbt_valid_to = DATE '{tanggal}'
WHERE dbt_valid_to IS NULL
AND dbt_scd_id NOT IN (
SELECT md5(id || '|' || kota) FROM pelanggan_sumber
)
""")
# 2) sisipkan versi baru untuk baris sumber yang hash-nya belum current
con.execute(f"""
INSERT INTO snapshot_pelanggan
SELECT id, email, kota,
md5(id || '|' || kota) AS dbt_scd_id,
DATE '{tanggal}' AS dbt_valid_from,
NULL AS dbt_valid_to
FROM pelanggan_sumber
WHERE md5(id || '|' || kota) NOT IN (
SELECT dbt_scd_id FROM snapshot_pelanggan WHERE dbt_valid_to IS NULL
)
""")
def tampil(judul):
print(judul)
for r in con.execute("""
SELECT id, email, kota,
strftime(dbt_valid_from, '%Y-%m-%d'),
COALESCE(strftime(dbt_valid_to, '%Y-%m-%d'), 'current')
FROM snapshot_pelanggan ORDER BY id, dbt_valid_from
""").fetchall():
print(" ", r)
jalankan_snapshot("2026-01-10")
tampil("=== RUN 1: snapshot 2026-01-10 ===")
print()
print("--- sumber berubah: Budi pindah ke Depok, pelanggan baru Rina ---")
con.execute("""
UPDATE pelanggan_sumber SET kota = 'Depok' WHERE id = 1;
INSERT INTO pelanggan_sumber VALUES (3, '[email protected]', 'Surabaya');
""")
jalankan_snapshot("2026-01-20")
tampil("=== RUN 2: snapshot 2026-01-20 ===")Output yang Dihasilkan:
=== RUN 1: snapshot 2026-01-10 ===
(1, '[email protected]', 'Jakarta', '2026-01-10', 'current')
(2, '[email protected]', 'Bandung', '2026-01-10', 'current')
--- sumber berubah: Budi pindah ke Depok, pelanggan baru Rina ---
=== RUN 2: snapshot 2026-01-20 ===
(1, '[email protected]', 'Jakarta', '2026-01-10', '2026-01-20')
(1, '[email protected]', 'Depok', '2026-01-20', 'current')
(2, '[email protected]', 'Bandung', '2026-01-10', 'current')
(3, '[email protected]', 'Surabaya', '2026-01-20', 'current')Mari kita telaah bagian-bagian penting skrip di atas:
- Langkah 1 di
jalankan_snapshot(UPDATE): menutup setiap versi current yang hash-nya tidak lagi cocok dengan sumber — baris Jakarta milik Budi mendapatdbt_valid_to = 2026-01-20. Inilah deteksi perubahan strategi check: perbandingan hash, bukan perbandingan isi kolom satu per satu. - Langkah 2 (INSERT): menyisipkan versi baru untuk sumber yang hash-nya belum terwakili sebagai current — Budi versi Depok dan Rina yang baru pertama kali direkam.
- Fungsi
tampil:COALESCE(strftime(dbt_valid_to, ...), 'current')menampilkan status versi; dua baris milikid = 1kini membentuk riwayat: Jakarta sampai 20 Januari, Depok sejak 20 Januari.
Membaca Keadaan pada Titik Waktu #
Nilai snapshot baru terasa saat di-query. Pola point-in-time: pilih versi yang dbt_valid_from <= tanggal dan dbt_valid_to > tanggal (atau NULL):
print("=== keadaan pada 2026-01-15 (sebelum perpindahan) ===")
for r in con.execute("""
SELECT email, kota FROM snapshot_pelanggan
WHERE dbt_valid_from <= DATE '2026-01-15'
AND (dbt_valid_to > DATE '2026-01-15' OR dbt_valid_to IS NULL)
ORDER BY id
""").fetchall():
print(" ", r)
print()
print("=== keadaan saat ini (dbt_valid_to IS NULL) ===")
for r in con.execute("""
SELECT email, kota FROM snapshot_pelanggan
WHERE dbt_valid_to IS NULL ORDER BY id
""").fetchall():
print(" ", r)Output yang Dihasilkan:
=== keadaan pada 2026-01-15 (sebelum perpindahan) ===
('[email protected]', 'Jakarta')
('[email protected]', 'Bandung')
=== keadaan saat ini (dbt_valid_to IS NULL) ===
('[email protected]', 'Depok')
('[email protected]', 'Bandung')
('[email protected]', 'Surabaya')Query pertama menjawab pertanyaan pembuka chapter: pada 15 Januari, Budi masih tercatat di Jakarta — fact table pesanan Januari dapat di-join ke versi dimensi yang benar lewat pola yang sama dengan Chapter Slowly Changing Dimensions. Query kedua adalah pintu menuju dimensi current: cukup menyaring dbt_valid_to IS NULL. Model lain membaca snapshot ini lewat ref('pelanggan_snapshot') seperti model biasa.
Strategi check vs timestamp, dan hard_deletes #
Dua strategi deteksi tersedia. Strategi check membandingkan hash unique_key dan check_cols — tidak butuh kolom waktu apa pun, jadi cocok untuk sumber aplikasi yang tidak memelihara updated_at dengan disiplin. Strategi timestamp memakai kolom updated_at milik sumber dan hanya menumbuhkan versi bila kolom tersebut berubah — lebih murah, tetapi percaya sepenuhnya pada disiplin sumber. Untuk baris yang terhapus dari sumber, hard_deletes menentukan perilaku: ignore membiarkan versi tetap current, invalidate menutupnya dengan dbt_valid_to, dan new_record menambah baris penanda lewat kolom meta dbt_is_deleted. Rincian strategi dan konfigurasi ini ada di dokumentasi Snapshots dbt
.
Rangkaian model dbt TokoKita kini lengkap: staging, intermediate, marts, incremental, hingga snapshot. Tabel-tabel tersebut terbentuk rapi — tetapi apakah isinya benar? Satu double-load bisa menggandakan GMV, satu status berkapital bisa lolos dari filter. Chapter berikutnya memasang lapisan pertahanan untuk kejadian seperti itu: Testing Model Data.