Analytics Engineering dengan dbt: Panduan Praktis
dbt (data build tool) telah menjadi bagian standar dari modern data stack karena membawa disiplin software engineering ke transformasi data berbasis SQL. Tutorial ini membahas apa itu dbt, cara menyiapkannya, dan cara membangun proyek transformasi yang mudah dirawat, lengkap dengan contoh SQL dan YAML realistis yang bisa Anda sesuaikan dengan warehouse Anda sendiri.
Apa Itu dbt dan Alur Kerja Analytics Engineering
dbt adalah alat transformasi yang memungkinkan Anda mengekspresikan transformasi data sebagai pernyataan SELECT SQL. dbt tidak melakukan ekstraksi atau pemuatan data; ia berada di bagian T dari ELT, berjalan di dalam data warehouse Anda (Snowflake, BigQuery, Redshift, Postgres, Databricks, dan lainnya). Anda menulis model, lalu dbt mengompilasinya menjadi SQL dan menjalankannya sesuai urutan dependensi.
Peran yang muncul seiring alur kerja ini adalah analytics engineer: seseorang yang menerapkan praktik engineering, version control, pengujian, code review, dokumentasi, dan desain modular, pada kode analitik yang dahulu ditulis sebagai query ad-hoc.
Alur kerja dbt yang umum terlihat seperti ini:
dbt build secara terjadwal, di CI, atau melalui dbt Cloud.Manfaat utamanya adalah keterulangan (repeatability), pelacakan lineage, dan kemampuan menguji transformasi layaknya kode aplikasi.
Instalasi
dbt didistribusikan sebagai dbt-core ditambah paket adapter untuk warehouse spesifik Anda. Pasang hanya adapter yang Anda butuhkan.
# Buat environment terisolasi terlebih dahulu
python -m venv .venv
source .venv/bin/activate
Postgres
pip install dbt-postgres
Atau BigQuery
pip install dbt-bigquery
dbt-core terpasang otomatis sebagai dependensi
dbt --version
dbt membaca pengaturan koneksi dari file profiles.yml. Secara default file ini berada di ~/.dbt/profiles.yml, terpisah dari proyek Anda agar kredensial tidak masuk ke version control.
# ~/.dbt/profiles.yml
jaffleshop:
target: dev
outputs:
dev:
type: postgres
host: localhost
port: 5432
user: analytics
password: "{{ envvar('DBTPASSWORD') }}"
dbname: analytics
schema: dbtdev
threads: 4
Menggunakan envvar() menjaga rahasia tetap di luar file. Uji koneksi dengan:
dbt debug
Struktur Proyek
Buat proyek baru dengan dbt init, lalu telusuri struktur yang dihasilkan.
dbt init jaffleshop
Proyek dbt berpusat pada dbtproject.yml dan sekumpulan direktori konvensional:
jaffleshop/
dbtproject.yml # konfigurasi proyek
models/ # pernyataan SELECT (transformasi Anda)
staging/
marts/
seeds/ # file CSV statis yang dimuat sebagai tabel
snapshots/ # pelacakan SCD type 2
macros/ # Jinja yang dapat dipakai ulang
tests/ # singular test (kustom)
analyses/ # query ad-hoc yang dikompilasi namun tidak dijalankan
File dbtproject.yml mengikat semuanya dan menetapkan nilai default:
name: 'jaffleshop'
version: '1.0.0'
profile: 'jaffle
shop'
model-paths: ["models"]
seed-paths: ["seeds"]
snapshot-paths: ["snapshots"]
macro-paths: ["macros"]
models:
jaffleshop:
staging:
+materialized: view
marts:
+materialized: table
Kunci +materialized menetapkan default per folder. Awalan + menandai sebuah konfigurasi, bukan jalur model bersarang.
Menulis Model sebagai Pernyataan SELECT
Sebuah model adalah satu file .sql yang berisi satu pernyataan SELECT. Nama file menjadi nama relasi di warehouse. Tidak ada boilerplate CREATE TABLE, dbt membungkus query Anda berdasarkan materialisasi.
-- models/staging/stgorders.sql
select
id as orderid,
userid as customerid,
orderdate,
status,
amount / 100.0 as amountusd
from {{ source('jaffle', 'raworders') }}
-- models/marts/orderspercustomer.sql
select
customerid,
count() as ordercount,
sum(amountusd) as lifetimevalue
from {{ ref('stgorders') }}
group by 1
Materialisasi
Materialisasi mengontrol bagaimana dbt menyimpan hasil sebuah model. Anda mengaturnya secara inline dengan blok config() atau di dbtproject.yml.
- view (default): membuat database view. Tanpa biaya penyimpanan, selalu segar, dihitung ulang saat dibaca.
- table: membangun ulang tabel penuh pada setiap run. Cepat untuk di-query, mudah dipahami.
- incremental: menyisipkan atau memperbarui hanya baris baru/berubah. Dipakai untuk tabel besar yang banyak penambahan.
- ephemeral: tidak dibangun di warehouse sama sekali; disisipkan sebagai CTE ke model downstream.
-- konfigurasi inline
{{ config(materialized='table') }}
select from {{ ref('stgorders') }}
Pilih view untuk staging yang ringan, table untuk mart yang sering di-query, incremental ketika rebuild penuh menjadi terlalu lambat atau mahal, dan ephemeral untuk transformasi pembantu kecil yang tidak perlu di-query langsung.
ref() dan source()
Kedua fungsi Jinja ini adalah inti dari dbt. Jangan pernah menulis nama tabel secara hard-code; gunakan ref() dan source() agar dbt dapat membangun graf dependensi dan mengelola schema antar environment.
ref('modelname') mereferensikan model lain. dbt menggunakan pemanggilan ini untuk menentukan urutan run dan menghasilkan lineage.
from {{ ref('stgorders') }}
source('sourcename', 'tablename') mereferensikan data mentah yang didefinisikan di file YAML. Ini memisahkan model Anda dari lokasi fisik tabel.
from {{ source('jaffle', 'raworders') }}
Karena dbt menyelesaikan ini saat kompilasi, beralih dari schema dev ke schema prod tidak memerlukan perubahan kode, cukup target yang berbeda.
Sources dan Freshness
Deklarasikan sources di file YAML agar dapat direferensikan, diuji, dan dipantau.
# models/staging/sources.yml
version: 2
sources:
- name: jaffle
database: raw
schema: public
tables:
- name: raw
orders
loadedatfield: loadedat
freshness:
warnafter: {count: 12, period: hour}
errorafter: {count: 24, period: hour}
- name: rawcustomers
Blok freshness memungkinkan dbt memeriksa apakah data mentah datang tepat waktu. Jalankan pemeriksaan dengan:
dbt source freshness
Jika nilai loadedat terbaru lebih lama dari ambang batas, dbt mengeluarkan peringatan atau error, yang berguna sebagai sinyal awal bahwa pipeline upstream macet.
Tests
Tests memastikan data Anda memenuhi ekspektasi. dbt memiliki dua jenis: tes generic yang diterapkan via YAML, dan tes singular yang ditulis sebagai SQL.
Empat tes bawaan mencakup sebagian besar kebutuhan:
# models/staging/models.yml
version: 2
models:
- name: stgorders
columns:
- name: orderid
tests:
- unique
- notnull
- name: status
tests:
- acceptedvalues:
values: ['placed', 'shipped', 'completed', 'returned']
- name: customerid
tests:
- relationships:
to: ref('stgcustomers')
field: customerid
unique: tidak ada nilai duplikat.notnull: tidak ada nilai yang hilang.acceptedvalues: hanya nilai dari himpunan yang dikenal.relationships: setiap nilai ada di model yang direferensikan (integritas referensial).
Sebuah tes singular adalah query SQL apa pun yang seharusnya mengembalikan nol baris. Jika mengembalikan baris, tes gagal.
-- tests/assertamountispositive.sql
select
orderid,
amountusd
from {{ ref('stgorders') }}
where amountusd < 0
Jalankan tes dengan:
dbt test
dbt test --select stgorders
Dokumentasi dan dbt docs generate
dbt menghasilkan situs dokumentasi yang dapat ditelusuri dari deskripsi YAML dan lineage model Anda. Tambahkan deskripsi di samping tes Anda.
version: 2
models:
- name: stgorders
description: "Satu baris per order, dibersihkan dari raworders."
columns:
- name: order
id
description: "Primary key untuk order."
tests:
- unique
- notnull
Anda juga dapat menulis deskripsi yang lebih panjang di file .md dan mereferensikannya dengan fungsi doc():
description: "{{ doc('stgordersdoc') }}"
Bangun dan sajikan dokumentasi:
dbt docs generate
dbt docs serve
Situs yang dihasilkan mencakup katalog model yang dapat dicari dan graf lineage interaktif (DAG), yang memudahkan menelusuri bagaimana sebuah kolom mengalir dari sumber mentah ke mart akhir.
Jinja dan Macros
dbt membuat template SQL dengan Jinja, yang menambahkan variabel, perulangan, kondisional, dan fungsi yang dapat dipakai ulang yang disebut macros. Ini menjaga SQL tetap DRY tanpa harus copy-paste.
-- perulangan untuk mem-pivot status menjadi kolom
select
customerid,
{% for status in ['placed', 'shipped', 'completed'] %}
sum(case when status = '{{ status }}' then 1 else 0 end) as {{ status }}count
{%- if not loop.last %},{% endif %}
{% endfor %}
from {{ ref('stgorders') }}
group by 1
Sebuah macro adalah blok bernama yang dapat dipakai ulang, didefinisikan di macros/:
-- macros/centstodollars.sql
{% macro centstodollars(columnname, precision=2) %}
round({{ columnname }} / 100.0, {{ precision }})
{% endmacro %}
Panggil dari model mana pun:
select
orderid,
{{ centstodollars('amount') }} as amountusd
from {{ source('jaffle', 'raworders') }}
Macros adalah cara paket seperti dbtutils mendistribusikan logika yang dapat dipakai ulang. Pasang paket dengan mencantumkannya di packages.yml lalu menjalankan dbt deps.
Seeds
Seeds adalah file CSV kecil di direktori seeds/ yang dimuat dbt sebagai tabel. Cocok untuk data referensi statis seperti kode negara, pemetaan status, atau daftar akun uji yang perlu dikecualikan.
seeds/
countrycodes.csv
dbt seed
Setelah dimuat, referensikan sebuah seed dengan ref() persis seperti model:
select
o.orderid,
c.countryname
from {{ ref('stgorders') }} o
left join {{ ref('countrycodes') }} c
on o.countrycode = c.code
Seeds berada di bawah version control, sehingga data referensi hidup berdampingan dengan logika transformasi Anda. Hindari menggunakan seeds untuk dataset besar atau yang sering berubah, seeds ditujukan untuk lookup kecil yang stabil.
Snapshots (SCD Type 2)
Sistem sumber sering menimpa record di tempat, sehingga keadaan historis hilang. Snapshots menangkap perubahan dari waktu ke waktu menggunakan logika slowly changing dimension (SCD) type 2, mencatat kapan setiap versi baris valid.
-- snapshots/orderssnapshot.sql
{% snapshot orders
snapshot %}
{{
config(
targetschema='snapshots',
uniquekey='orderid',
strategy='timestamp',
updatedat='updatedat'
)
}}
select * from {{ source('jaffle', 'raworders') }}
{% endsnapshot %}
Dengan strategi timestamp, dbt membandingkan kolom updatedat pada setiap run. Strategi check membandingkan kolom yang ditentukan, berguna ketika sumber tidak memiliki timestamp yang andal.
dbt snapshot
dbt menambahkan kolom dbtvalidfrom dan dbtvalidto. Nilai dbtvalidto yang null menandai versi baris saat ini, yang membuat analisis point-in-time menjadi mudah.
Incremental Model dengan isincremental()
Incremental model memproses hanya record baru atau berubah alih-alih membangun ulang seluruh tabel pada setiap run. Ini adalah jawaban ketika materialisasi table menjadi terlalu lambat atau mahal pada tabel fakta yang besar.
-- models/marts/fctevents.sql
{{
config(
materialized='incremental',
unique
key='eventid'
)
}}
select
event
id,
userid,
eventtype,
eventat
from {{ source('app', 'rawevents') }}
{% if isincremental() %}
-- hanya pindai baris yang lebih baru dari yang sudah dimuat
where eventat > (select max(eventat) from {{ this }})
{% endif %}
isincremental() mengembalikan true hanya ketika tabel sudah ada dan Anda tidak sedang menjalankan full refresh. {{ this }} merujuk pada relasi model saat ini. Dengan uniquekey diset, dbt melakukan merge pada baris yang cocok alih-alih menggandakannya.
Paksa rebuild penuh ketika logika berubah:
dbt run --select fctevents --full-refresh
Menjalankan dbt: run, test, dan build
Perintah dbt beroperasi pada seleksi resource. Gunakan --select untuk menargetkan subset.
# bangun semua model
dbt run
bangun satu model dan semua downstream-nya
dbt run --select stgorders+
jalankan tes saja
dbt test
build: menjalankan model, tes, snapshot, dan seed bersama-sama sesuai urutan DAG
dbt build
dbt build adalah perintah yang direkomendasikan untuk produksi karena menyisipkan model dan tesnya sesuai urutan dependensi, mencegah model downstream dibangun di atas data yang gagal tesnya. Operator graf patut dipelajari: model+ memilih turunan, +model memilih leluhur, dan tag:nightly memilih berdasarkan tag.
Catatan Deployment: dbt Cloud vs CI
Anda memiliki dua jalur umum menuju produksi.
dbt Cloud adalah layanan terkelola yang menyediakan scheduler, IDE berbasis browser, dokumentasi terhosting, dan CI bawaan yang berjalan terhadap pull request. Ini adalah opsi dengan perawatan lebih rendah dan menangani orkestrasi untuk Anda. CI yang dikelola sendiri menjalankandbt-core di pipeline Anda sendiri (GitHub Actions, GitLab CI, Airflow). Contoh job GitHub Actions:
# .github/workflows/dbt.yml
name: dbt build
on: [pullrequest]
jobs:
dbt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install dbt-postgres
- run: dbt deps
- run: dbt build --target ci
env:
DBTPASSWORD: ${{ secrets.DBTPASSWORD }}
Untuk CI yang efisien, pelajari Slim CI menggunakan state:modified+ dengan deferral, yang hanya membangun model yang berubah dalam sebuah pull request alih-alih seluruh proyek. Apa pun caranya, tujuannya sama: setiap perubahan diuji sebelum mencapai data produksi.
Praktik Terbaik
- Susun model berlapis. Gunakan
staging(satu model per tabel sumber, pembersihan ringan),intermediate(join yang dapat dipakai ulang), danmarts(model menghadap bisnis). Ini menjaga logika tetap modular dan mudah di-review. - Selalu gunakan
ref()dansource(). Jangan pernah menulis nama schema atau tabel secara hard-code; biarkan dbt mengelola lineage dan environment. - Satu staging model per tabel sumber, dinamai
stg, hanya melakukan penamaan ulang, type casting, dan pembersihan ringan.
- Uji asumsi Anda. Minimal, tambahkan
uniquedannotnullpada setiap primary key. Tambahkan tesrelationshipspada foreign key.- Jaga kredensial di luar repo. Gunakan
env_var()diprofiles.yml.- Utamakan
viewuntuk staging dantableuntuk marts. Gunakanincrementalhanya ketika waktu run atau biaya menuntutnya.- Dokumentasikan sambil berjalan. Model dengan deskripsi dan kolom yang teruji jauh lebih mudah dipercaya oleh analis berikutnya.
- Jalankan
dbt builddi CI agar perubahan yang belum teruji tidak pernah mencapai produksi.Kesimpulan dan Poin Penting
dbt membawa praktik software engineering, version control, modularitas, pengujian, dan dokumentasi, ke lapisan transformasi data stack Anda. Dengan menulis model sebagai pernyataan
SELECTbiasa dan menghubungkannya denganref()dansource(), Anda memperoleh graf dependensi yang dikelola otomatis, portabilitas environment, dan lineage yang jelas.Poin penting untuk diingat:
- dbt menangani hanya transformasi (huruf T pada ELT); ia menjalankan SQL di dalam warehouse Anda.
- Materialisasi (
view,table,incremental,ephemeral) memungkinkan Anda menyetel trade-off biaya/kesegaran per model. ref()dansource()wajib untuk lineage dan pengelolaan environment.- Tests (generic dan singular) serta
dbt buildmencegah data buruk menyebar ke downstream. - Snapshots menangkap riwayat (SCD type 2); incremental model menskalakan tabel besar.
- Dokumentasi dan DAG membuat proyek dapat dipahami oleh seluruh tim.
Mulailah dari yang kecil dengan beberapa staging model dan sedikit tes, lalu kembangkan proyek lapis demi lapis. Disiplin ini terbayar seiring transformasi dan tim Anda bertumbuh.
Artikel Terkait
Airbyte: Panduan Lengkap Data Integration Open-Source dari Nol sampai Custom Connector
Airbyte: Panduan Lengkap Data Integration Open-Source dari Nol sampai Custom Connector Halo temen-temen, di tutorial kal...
SQLMesh: Framework Transformasi Data Modern Alternatif dbt
SQLMesh: Framework Transformasi Data Modern yang Bikin Aku Ninggalin dbt Halo temen-temen! Kali ini aku mau ngajak kalia...
Tutorial Ibis: API DataFrame Portabel untuk Banyak Backend
Ibis: API Dataframe Python yang Portabel di Banyak Backend Ibis adalah library dataframe Python yang memungkinkan Anda m...
Tutorial dlt: Pipeline Ingestion Data Berbasis Python
Membangun Pipeline EL Berbasis Python dengan dlt (data load tool) Sebagian besar tim data menghabiskan waktu yang tidak ...
- Uji asumsi Anda. Minimal, tambahkan