Ilustrasi dihasilkan AI untuk artikel ini; bukan tangkapan layar lingkungan produksi. Di banyak tim data, transformasi masih hidup ...
Di banyak tim data, transformasi masih hidup sebagai kumpulan skrip SQL yang dijalankan manual, atau sebagai query panjang di dalam dashboard yang tidak pernah diuji siapa pun. Ketika sumber berubah nama kolom, tidak ada yang memberi tahu sampai angka di laporan pagi tiba-tiba meleset. dbt Core mencoba menutup celah itu bukan dengan mesin pemrosesan baru, melainkan dengan disiplin rekayasa perangkat lunak: versi terkontrol, dependensi eksplisit, pengujian otomatis, dan dokumentasi yang dihasilkan dari kode itu sendiri.
Artikel ini membahas dbt Core sebagai lapisan transformasi di atas warehouse yang sudah ada. Fokusnya praktis: bagaimana menyusun proyek, memisahkan staging dari marts, menulis test yang benar-benar menangkap kegagalan, menjalankan dbt build dan dbt docs generate, serta kapan model incremental layak dipakai. Semua contoh di bawah adalah kerangka yang belum dijalankan end-to-end pada lingkungan nyata; angka performa sengaja tidak dicantumkan karena sangat bergantung pada warehouse, volume data, dan strategi materialisasi masing-masing tim.
Mengapa dbt Core, dan Apa yang Sebenarnya Ia Selesaikan
dbt Core adalah alat baris perintah yang menerjemahkan file SQL dan konfigurasi YAML menjadi pernyataan CREATE TABLE, CREATE VIEW, atau MERGE yang dieksekusi oleh warehouse. Ia tidak menyimpan data dan tidak memindahkan data; ia mengorkestrasi transformasi di dalam database tujuan. Konsekuensinya penting: biaya komputasi tetap ditanggung warehouse, dan kualitas hasil sangat ditentukan oleh cara model disusun.
Yang dbt tambahkan di atas SQL biasa adalah lapisan semantik. Setiap model adalah file dengan nama, sehingga otomatis menjadi tabel atau view. Referensi antar model memakai ref(), bukan nama tabel literal, sehingga dbt dapat membangun graf dependensi (DAG) dan menjalankan model dalam urutan yang benar. Di atas graf itu, dbt menempelkan test, dokumentasi, deskripsi kolom, serta metadata lineage yang bisa dibaca manusia maupun mesin.
Bagi tim kecil, nilai terbesarnya sering kali bukan fitur canggih, melainkan efek disipliner: tidak ada lagi query yang hanya dipahami satu orang, dan tidak ada lagi perubahan skema yang lolos tanpa peringatan. Dokumentasi resminya di docs.getdbt.com menjadi rujukan utama untuk perilaku yang tidak dijelaskan di sini.
Prasyarat Lingkungan dan Struktur Proyek
Prasyarat yang Sering Terlewat
dbt Core berjalan sebagai paket Python, sehingga prasyaratnya adalah Python 3.9 atau lebih baru, virtual environment yang terisolasi, dan adapter sesuai warehouse. Untuk PostgreSQL, adapter-nya adalah dbt-postgres; untuk warehouse lain tersedia adapter terpisah dengan versi yang harus cocok dengan versi dbt Core. Selain itu dibutuhkan kredensial warehouse yang punya hak membuat skema, karena dbt akan menulis tabel dan view di sana.
Konfigurasi koneksi tidak disimpan di dalam repositori proyek, melainkan di profiles.yml pada direktori pengguna, biasanya ~/.dbt/profiles.yml. Pemisahan ini disengaja: repositori berisi logika transformasi dan aman dibagikan, sedangkan kredensial tetap lokal. Untuk tim, pola yang lazim adalah menyimpan profiles.yml sebagai templat tanpa nilai rahasia, lalu mengisinya lewat variabel lingkungan pada mesin masing-masing atau lewat secret manager di CI.
Struktur Direktori
Struktur berikut adalah konvensi yang cukup umum dan sudah terbukti mudah dirawat. Yang penting bukan nama direktorinya, melainkan pemisahan tanggung jawab: sumber, staging, marts, dan test tidak dicampur dalam satu folder.
analytics_dbt/
├── dbt_project.yml
├── models/
│ ├── staging/
│ │ ├── _sources.yml
│ │ ├── stg_orders.sql
│ │ └── stg_customers.sql
│ └── marts/
│ ├── _marts.yml
│ ├── fct_orders.sql
│ └── dim_customers.sql
├── tests/
│ └── assert_order_total_positive.sql
├── macros/
│ └── cents_to_dollars.sql
└── snapshots/
└── customers_snapshot.sql
Direktori models/staging menyentuh data mentah; models/marts menyusun entitas bisnis yang siap dikonsumsi. Folder tests menampung test singular yang tidak bisa dinyatakan sebagai test generik, dan macros menyimpan logika Jinja yang dipakai berulang. Di dbt_project.yml, materialisasi biasanya diatur per folder: staging sebagai view agar murah dan selalu segar, marts sebagai table atau incremental sesuai volume.
Model Staging: Satu Sumber, Satu Tanggung Jawab
Model staging adalah lapisan pertama setelah data mentah. Tugasnya sempit: memilih kolom yang relevan, mengganti nama menjadi konvensi yang konsisten, memperbaiki tipe data, dan menyeragamkan nilai yang jelas-jelas salah format. Tidak ada agregasi, tidak ada join antar domain, dan tidak ada logika bisnis di sini. Aturan ini terlihat kaku, tetapi justru itulah yang membuat perubahan di hulu mudah dilacak: jika sumber berubah, hanya satu model staging yang perlu diperbaiki.
Deklarasi Source dan Konvensi Penamaan
Sumber data dideklarasikan di _sources.yml agar dbt mengenali tabel mentah sebagai node dalam DAG, lengkap dengan deskripsi dan pengaturan freshness. Model staging kemudian memakai source(), bukan nama tabel langsung. Awalan stg_ pada nama file membuat lapisan ini mudah dikenali, sementara nama kolom dijaga tetap identik dengan hilirnya agar tidak ada penerjemahan ganda.
Setiap model staging idealnya satu sumber dan satu grain. Jika sebuah model staging mulai butuh join ke tiga tabel, biasanya itu tanda bahwa logika tersebut sudah masuk wilayah marts dan sebaiknya dipindahkan.
Model Marts: ref() sebagai Kontrak Dependensi
Lapisan marts adalah tempat logika bisnis hidup. Di sinilah definisi "pesanan yang sah", "pelanggan aktif", atau "pendapatan bersih" dinyatakan sekali, lalu dipakai ulang oleh semua konsumen. Model marts membaca staging melalui ref(), dan itulah kontraknya: jika model yang direferensikan berubah nama atau dihapus, dbt gagal saat kompilasi, bukan setelah dashboard menampilkan angka salah.
Pemisahan fakta dan dimensi membantu, tetapi bukan harga mati. Yang lebih penting adalah konsistensi grain: tabel fakta menyatakan satu baris mewakili apa, dan tabel dimensi menyatakan satu baris mewakili entitas apa. Tanpa kejelasan grain, test duplikasi tidak bisa ditulis dengan benar.
-- models/staging/stg_orders.sql
with source_orders as (
select * from {{ source('raw', 'orders') }}
),
renamed as (
select
id as order_id,
customer_id,
status,
cast(created_at as timestamp) as ordered_at,
amount_cents / 100.0 as amount
from source_orders
where status is not null
)
select * from renamed
-- models/marts/fct_orders.sql
with orders as (
select * from {{ ref('stg_orders') }}
),
customers as (
select * from {{ ref('dim_customers') }}
)
select
o.order_id,
o.customer_id,
c.customer_segment,
o.ordered_at,
date_trunc('day', o.ordered_at) as order_date,
o.amount
from orders o
left join customers c using (customer_id)
where o.status = 'completed'
Perhatikan bahwa fct_orders tidak mengetahui nama tabel fisik apa pun. Ia hanya tahu model stg_orders dan dim_customers harus ada lebih dulu. Ketika materialisasi berubah dari view menjadi table, atau ketika model dipindahkan ke skema lain, isi SQL tidak perlu disentuh.
Test sebagai Gerbang Kualitas, Bukan Formalitas
Test di dbt dijalankan sebagai query yang mengembalikan baris bermasalah; jika ada baris yang kembali, test gagal. Model berpikir ini sederhana tetapi kuat, karena test dapat mengekspresikan hampir semua asumsi tentang data tanpa menulis kode tambahan. Ada dua bentuk: test generik yang dideklarasikan di YAML dan test singular berupa file SQL di folder tests.
Test Generik dan Test Singular
Test generik mencakup pemeriksaan umum seperti unique, not_null, accepted_values, dan relationships. Test singular dipakai untuk aturan yang khas domain, misalnya memastikan total pembayaran tidak pernah melebihi nilai pesanan. Keduanya sebaiknya tidak dipisahkan ke sistem lain: menempatkan test di dekat definisi model membuat perubahan skema dan perubahan test terjadi dalam satu commit.
Tingkat keparahan juga bisa diatur. Untuk pelanggaran yang tidak boleh terjadi sama sekali, gunakan severity: error sehingga pipeline berhenti. Untuk pelanggaran yang perlu diketahui tetapi tidak memblokir, severity: warn lebih realistis, dengan catatan bahwa peringatan yang dibiarkan menumpuk akan kehilangan makna. Rujukan lengkap jenis test dan opsinya ada di dokumentasi test dbt.
# models/marts/_marts.yml
version: 2
models:
- name: fct_orders
description: "Satu baris per pesanan yang berstatus completed."
columns:
- name: order_id
description: "Kunci utama pesanan."
data_tests:
- unique
- not_null
- name: customer_id
data_tests:
- not_null
- relationships:
to: ref('dim_customers')
field: customer_id
- name: order_date
data_tests:
- not_null
- name: dim_customers
description: "Dimensi pelanggan dengan segmen terkini."
columns:
- name: customer_id
data_tests:
- unique
- not_null
Catatan operasional: test yang gagal pada data historis sering bukan bug model, melainkan bug ekspektasi. Sebelum menurunkan severity atau menghapus test, periksa apakah grain model berubah. Mematikan test untuk membuat pipeline hijau adalah cara paling cepat kehilangan kepercayaan pada seluruh lapisan data.
Build, Test, dan Dokumentasi Otomatis
Perintah dbt build menjalankan model, test, snapshot, dan seed dalam satu perjalanan yang mengikuti urutan DAG. Ini berbeda dari dbt run yang hanya membangun model: dengan build, test dijalankan tepat setelah model yang diuji selesai dibuat, sehingga kegagalan terdeteksi lebih awal dan model hilir dapat dihentikan sebelum menyebarkan data yang salah.
Dalam praktik, alur kerjanya bertingkat. Saat mengembangkan, jalankan model tertentu saja dengan --select beserta keturunannya. Setelah stabil, jalankan seluruh proyek. Untuk pemeriksaan sebelum merge, dbt build --select state:modified+ membatasi eksekusi pada model yang berubah dan turunannya, yang jauh lebih murah daripada membangun ulang semuanya.
Dokumentasi yang Tidak Bisa Basi
dbt docs generate menghasilkan katalog berisi struktur kolom, deskripsi dari YAML, dan lineage grafis. Karena dokumentasi dibaca dari kode, ia ikut berubah ketika kode berubah. Dokumentasi yang dihasilkan memang tidak otomatis bagus; kualitasnya bergantung pada disiplin menulis description pada model dan kolom yang sering dipakai. Namun setidaknya tidak ada lagi dokumen terpisah yang tertinggal tiga versi di belakang.
# Sekali di awal: pastikan koneksi warehouse valid
dbt debug
# Bangun model tertentu dan semua turunannya
dbt build --select stg_orders+
# Bangun hanya yang berubah dibanding manifest sebelumnya
dbt build --select state:modified+ --defer --state ./prod-manifest
# Jalankan test saja pada lapisan marts
dbt test --select marts
# Hasilkan katalog dokumentasi lalu sajikan secara lokal
dbt docs generate
dbt docs serve --port 8080
Pada pipeline CI, dbt build biasanya dijalankan terhadap skema sementara agar tidak mengganggu produksi. Skema terpisah per branch juga mencegah dua pekerjaan yang berjalan bersamaan saling menimpa tabel. Yang perlu diingat, dbt tidak mengelola orkestrasi cluster atau penjadwalan; untuk itu ia dipanggil dari cron, Airflow, Dagster, atau sistem CI yang sudah ada.
Model Incremental dan Disiplin Operasional
Ketika tabel fakta tumbuh menjadi puluhan juta baris, membangun ulang seluruh tabel setiap kali jauh lebih mahal daripada menambahkan baris baru. Model incremental memecahkan ini dengan menyimpan hasil sebelumnya dan hanya memproses subset data. Kata kuncinya adalah subset: kualitas model incremental sepenuhnya bergantung pada filter yang benar, dan filter yang salah akan menghasilkan duplikasi atau data hilang yang sulit dilacak.
Blok is_incremental() hanya aktif ketika tabel tujuan sudah ada dan materialisasi disetel ke incremental. Di dalamnya, filter membatasi data ke rentang yang belum diproses, biasanya memakai timestamp dengan jendela mundur beberapa hari untuk menangkap data yang terlambat datang. Kolom unique_key menentukan bagaimana baris yang sudah ada digabungkan, sedangkan on_schema_change mengatur perilaku ketika kolom baru muncul di hulu.
Satu jebakan yang sering muncul: menjalankan model incremental pertama kali pada tabel kosong. Pada eksekusi pertama, blok is_incremental() dilewati sehingga seluruh data dimuat; ini benar, tetapi berarti uji coba pada data kecil tidak membuktikan perilaku inkrementalnya. Perlu satu siklus penuh dengan data duplikat dan data terlambat untuk memastikan strategi penggabungan bekerja seperti yang diharapkan.
-- models/marts/fct_events.sql
{{
config(
materialized='incremental',
unique_key='event_id',
incremental_strategy='merge',
on_schema_change='append_new_columns'
)
}}
with events as (
select * from {{ ref('stg_events') }}
{% if is_incremental() %}
-- jendela 3 hari untuk menangkap data yang terlambat tiba
where event_at >= (
select coalesce(max(event_at), '1970-01-01'::timestamp)
- interval '3 days'
from {{ this }}
)
{% endif %}
)
select
event_id,
user_id,
event_name,
event_at,
properties
from events
Detail perilaku masing-masing strategi materialisasi, termasuk perbedaan merge, delete+insert, dan append, didokumentasikan di halaman model incremental dbt. Tidak semua warehouse mendukung seluruh strategi, sehingga pilihan harus disesuaikan dengan adapter yang dipakai.
Di luar model itu sendiri, ada beberapa kebiasaan yang membedakan proyek dbt yang sehat dari yang rapuh. Pertama, setiap model punya pemilik yang jelas dan deskripsi singkat tentang grain-nya. Kedua, test ditambahkan bersamaan dengan model, bukan menyusul ketika ada insiden. Ketiga, perubahan pada model yang dipakai banyak tim melewati review, karena dampaknya menyebar melalui DAG tanpa terlihat di diff. Keempat, manifest dari lingkungan produksi disimpan sebagai artefak agar seleksi state:modified di CI punya pembanding yang sah.
dbt Core bukan obat untuk data yang buruk, dan ia tidak menggantikan pemahaman tentang isi warehouse. Ia hanya membuat asumsi tim menjadi eksplisit, dapat diuji, dan dapat ditelusuri. Bagi tim yang selama ini bergantung pada query ad-hoc dan ingatan kolektif, langkah pertama yang paling berdampak biasanya sederhana: pindahkan transformasi ke dalam model, beri nama yang konsisten, lalu tulis test untuk satu aturan yang paling sering dilanggar. Sisanya tumbuh dari sana.
COMMENTS