dbt 2 — Models & materializations
Model — đơn vị nhỏ nhất, và cũng là tất cả
Ở bài tổng quan chúng ta đã thấy triết lý của dbt: bạn chỉ viết SELECT, dbt lo phần còn lại. Bài này đi vào chi tiết cái "đơn vị công việc" hiện thân cho triết lý đó — model — và câu hỏi kỹ thuật quan trọng nhất đi kèm nó: một model được vật chất hóa (materialize) vào warehouse như thế nào?
Định nghĩa gọn nhất: một model là một file .sql chứa đúng MỘT câu SELECT. Không hơn. Bạn không viết CREATE TABLE, không INSERT, không DROP, không MERGE. Bạn mô tả "dữ liệu kết quả trông như thế nào" bằng một câu truy vấn, và dbt tự sinh ra DDL/DML phù hợp để biến câu SELECT đó thành một quan hệ (relation) thật trong warehouse.
Hai quy ước tưởng nhỏ nhưng chi phối toàn bộ trải nghiệm:
- Tên file = tên quan hệ tạo ra. File
stg_orders.sqlsẽ tạo ra một object tênstg_orderstrong warehouse. Không có bước "đặt tên bảng" tách rời — đổi tên file là đổi tên bảng. - dbt tự lo DDL. Cùng một câu
SELECT, tùy cấu hình, dbt có thể sinh raCREATE VIEW ... AS (SELECT ...), hoặcCREATE TABLE ... AS (SELECT ...), hoặc một chuỗiMERGE/INSERTphức tạp cho incremental. Bạn không cần biết cú pháp DDL đặc thù của từng warehouse (Snowflake, BigQuery, Postgres... viết khác nhau); dbt trừu tượng hóa qua adapter.
Điều này nghe có vẻ chỉ là "tiện lợi", nhưng hệ quả sâu xa hơn nhiều: vì bạn chỉ mô tả kết quả chứ không mô tả cách tạo ra, dbt có toàn quyền quyết định chiến lược vật chất hóa. Đổi một model từ view sang table chỉ là đổi một dòng cấu hình — logic SELECT không phải sửa. Đây chính là điều tách biệt "logic nghiệp vụ" khỏi "chiến lược lưu trữ", và là lý do dbt mở rộng tốt.
ref() và source() — cách dbt biết thứ tự chạy
Nếu model chỉ là câu SELECT độc lập, dbt không hơn gì một thư mục chứa file .sql. Sức mạnh thật sự đến từ cách các model tham chiếu lẫn nhau. Và đây là quy tắc bất di bất dịch:
Không bao giờ hardcode tên bảng. Luôn dùng
ref()để trỏ tới model khác, vàsource()để trỏ tới bảng nguồn thô.
ref() — trỏ tới model khác
Thay vì viết FROM analytics.stg_orders, bạn viết:
-- (minh hoạ)
select *
from {{ ref('stg_orders') }}
{{ ref('stg_orders') }} là một hàm Jinja (xem Jinja & macros). Khi biên dịch, dbt thay nó bằng tên bảng đầy đủ, đúng schema, đúng database — kể cả khi bạn đang chạy ở môi trường dev (schema riêng) hay prod (schema chung). Nhưng ref() làm hai việc quan trọng hơn cả việc điền tên:
- Dựng DAG (Directed Acyclic Graph). Mỗi lần model A dùng
ref('B'), dbt ghi nhận "A phụ thuộc vào B". Toàn bộ các quan hệ phụ thuộc này tạo thành một đồ thị có hướng, không chu trình. dbt dùng DAG để tự sắp xếp thứ tự chạy (topological sort): B luôn được build trước A, không cần bạn khai báo thủ công. Với hàng trăm model, đây là điều không thể làm bằng tay. - Lineage. Chính DAG đó cho bạn biết "con số này từ đâu ra" — truy ngược từ mart về tận nguồn. dbt docs sinh ra sơ đồ lineage tương tác từ dữ liệu này.
source() — trỏ tới bảng nguồn thô
Dữ liệu thô (do EL tool như Fivetran, Airbyte đổ vào) không phải là model dbt. Bạn khai báo chúng trong file .yml rồi tham chiếu bằng source():
# models/staging/jaffle_shop/_sources.yml (minh hoạ)
sources:
- name: jaffle_shop # tên nhóm nguồn
database: raw
schema: jaffle_shop
tables:
- name: orders
- name: customers
-- stg_orders.sql (minh hoạ)
select
id as order_id,
user_id as customer_id,
order_date,
status
from {{ source('jaffle_shop', 'orders') }}
source() cũng đưa bảng nguồn vào DAG như một node "gốc". Ngoài ra, khai báo source còn mở khóa source freshness (kiểm tra dữ liệu nguồn có bị cũ không) — chủ đề của bài Sources & snapshots.
Quy tắc thực hành: staging model là nơi DUY NHẤT được phép dùng source(). Mọi tầng phía sau chỉ dùng ref(). Nhờ đó, khi bảng nguồn đổi tên/đổi schema, bạn chỉ sửa một chỗ (file source .yml).
Bốn kiểu materialization
Đây là phần cốt lõi. Materialization trả lời câu hỏi: dbt biến câu SELECT của bạn thành object gì trong warehouse, và bằng chiến lược nào? Cấu hình qua khóa materialized. dbt 1.x cung cấp bốn kiểu built-in.
view (mặc định)
-- (minh hoạ)
{{ config(materialized='view') }}
select ...
dbt sinh CREATE VIEW ... AS (câu SELECT của bạn). View không lưu dữ liệu — nó chỉ là một câu truy vấn được đặt tên và lưu lại. Mỗi lần có ai query view, warehouse tính toán lại logic bên dưới ngay lúc đó.
- Ưu: không tốn dung lượng lưu trữ; luôn tươi (phản ánh dữ liệu nguồn mới nhất tức thì);
dbt runcực nhanh vì chỉ tạo/thay định nghĩa view. - Nhược: query chậm nếu logic nặng, hoặc nếu view chồng lên view chồng lên view (mỗi lần query phải tính lại toàn bộ ngăn xếp bên dưới).
- Khi nào dùng: mặc định. Đặc biệt hợp với staging (biến đổi nhẹ: đổi tên, ép kiểu) và các model không bị query trực tiếp nhiều lần.
table
-- (minh hoạ)
{{ config(materialized='table') }}
select ...
dbt sinh CREATE TABLE ... AS (câu SELECT). Kết quả được vật chất hóa — tính toán một lần lúc dbt run, ghi ra thành bảng thật.
- Ưu: query nhanh (đọc dữ liệu đã tính sẵn, không phải tính lại).
- Nhược: tốn dung lượng (một bản sao dữ liệu); rebuild lại toàn bộ mỗi lần
dbt run— với biến đổi nặng thì tốn thời gian và tiền tính toán; dữ liệu chỉ "tươi" tới lần run gần nhất. - Khi nào dùng: model bị BI tool query nhiều lần (người dùng cần phản hồi nhanh); model có logic nặng được nhiều model hạ nguồn dùng chung (tính một lần, dùng nhiều lần); nói chung là các mart cuối cùng.
incremental
-- (minh hoạ, chi tiết ở bài incremental)
{{ config(materialized='incremental') }}
select ...
{% if is_incremental() %}
where event_at > (select max(event_at) from {{ this }})
{% endif %}
Thay vì rebuild toàn bảng mỗi lần, incremental chỉ xử lý và chèn/cập nhật dữ liệu MỚI kể từ lần run trước. Lần chạy đầu tiên (hoặc khi --full-refresh) nó build đầy đủ như table; các lần sau chỉ đụng tới phần delta.
- Ưu: giảm mạnh thời gian và chi phí build cho bảng rất lớn (log, event, giao dịch hàng trăm triệu dòng).
- Nhược: phức tạp hơn hẳn — cần cấu hình khóa duy nhất (
unique_key), điều kiện lọc delta, xử lý dữ liệu đến muộn (late-arriving), chiến lượcincremental_strategy(append/merge/delete+insert...). - Khi nào dùng: chỉ khi cần — khi table đã trở nên quá chậm/quá đắt để rebuild. Đừng bắt đầu bằng incremental. Toàn bộ chi tiết (strategy,
is_incremental(),this, xử lý schema drift) được mổ xẻ ở Incremental & hiệu năng.
ephemeral
-- (minh hoạ)
{{ config(materialized='ephemeral') }}
select ...
Ephemeral không tạo ra object nào trong warehouse. Thay vào đó, khi một model khác ref() tới nó, dbt nhúng (inline) logic của nó như một CTE vào câu truy vấn của model dùng nó (dbt đặt tiền tố nội bộ __dbt_cte__).
- Ưu: giữ warehouse sạch (không sinh view/table trung gian thừa); tái sử dụng logic mà không tốn dung lượng.
- Nhược: không thể query trực tiếp (nó không tồn tại độc lập); khó debug hơn (logic bị "tan" vào model khác); không hỗ trợ model contracts; nếu bị nhiều model dùng, logic bị lặp lại ở mỗi nơi (không tái dùng ở tầng SQL).
- Khi nào dùng: biến đổi nhẹ, nằm sớm trong DAG, chỉ được 1–2 model hạ nguồn dùng, và không cần ai query trực tiếp.
Bảng so sánh
| Kiểu | Object tạo ra | Tốc độ query | Lưu trữ | Độ tươi | Chi phí build | Dùng khi |
|---|---|---|---|---|---|---|
| view | View | Chậm (tính lúc query) | Không | Luôn tươi | Rất thấp | Mặc định, staging, biến đổi nhẹ |
| table | Table | Nhanh | Bản sao đầy đủ | Tới lần run gần nhất | Cao (rebuild mỗi run) | Mart, BI, logic nặng dùng chung |
| incremental | Table | Nhanh | Cập nhật một phần | Tùy lịch chạy | Thấp sau lần đầu | Bảng rất lớn (event/log), khi table quá chậm |
| ephemeral | Không có | (nhúng như CTE) | Không | Luôn tươi | Không riêng | Logic trung gian nhẹ, 1–2 model dùng |
Quy tắc ngón tay cái từ chính dbt: bắt đầu bằng view. Chuyển sang table khi query chậm. Chỉ dùng incremental khi table quá tốn kém để rebuild. Đừng tối ưu sớm.
Cấu hình model — ba tầng, ưu tiên tăng dần
Cùng một thuộc tính (materialized, tags, schema, alias, database...) có thể được đặt ở ba nơi. Nơi càng gần model, độ ưu tiên càng cao:
1. Trong dbt_project.yml — áp theo thư mục, ảnh hưởng hàng loạt. Đây là nơi khai báo quy ước chung cho từng tầng:
# dbt_project.yml (minh hoạ)
models:
my_project:
staging:
+materialized: view # toàn bộ staging là view
intermediate:
+materialized: ephemeral
marts:
+materialized: table # toàn bộ mart là table
+schema: analytics # ghi vào schema riêng
Dấu + là cú pháp phân biệt "đây là config" với "đây là tên thư mục con".
2. Trong file property .yml — cấu hình theo từng model cụ thể, tách khỏi code SQL:
# models/marts/_marts.yml (minh hoạ)
models:
- name: fct_revenue
config:
materialized: table
tags: ["finance", "daily"]
3. Trong chính file .sql qua {{ config(...) }} — ưu tiên cao nhất, ghi đè mọi thứ:
-- fct_revenue.sql (minh hoạ)
{{ config(
materialized='table',
tags=['finance'],
alias='revenue_daily'
) }}
select ...
Một vài thuộc tính hay dùng: tags (gắn nhãn để chạy chọn lọc theo nhóm), schema/alias (điều khiển schema và tên object đầu ra), database (ghi sang database khác). Chiến lược thực hành tốt: đặt mặc định theo tầng ở dbt_project.yml, chỉ dùng config() trong file khi một model cá biệt cần khác đi.
Phân tầng — kiến trúc chuẩn của một dự án dbt
dbt khuyến nghị (qua guide "how we structure") tổ chức model thành ba tầng, phản ánh hành trình dữ liệu đi từ source-conformed (định hình bởi hệ nguồn) sang business-conformed (định hình bởi nghiệp vụ):
- Staging (
stg_) — quan hệ 1–1 với bảng nguồn. Chỉ làm việc "vệ sinh": đổi tên cột theo quy ước, ép kiểu, chuẩn hóa nhẹ (ví dụ NULL rỗng, chuẩn hóa boolean). Đây là các "viên gạch nguyên tử". Đây cũng là nơi duy nhất dùngsource(). Thường materialize là view. - Intermediate (
int_) — các bước biến đổi có mục đích rõ ràng, chuẩn bị dữ liệu để ghép nối (join, pivot, tổng hợp trung gian). Nguyên tắc: mỗi biến đổi chỉ xảy ra ở đúng một nơi. Thường là ephemeral hoặc view. Không bị query trực tiếp bởi người dùng cuối. - Marts (
fct_/dim_) — các thực thể nghiệp vụ hoàn chỉnh, "rộng và giàu", phục vụ BI. Theo mô hình chiều (xem Warehouse modeling nếu có):fct_cho bảng fact (sự kiện đo lường được — doanh thu, đơn hàng),dim_cho bảng dimension (thực thể mô tả — khách hàng, sản phẩm). Thường là table (hoặc incremental nếu quá lớn).
Quy ước tiền tố (stg_, int_, fct_, dim_) không chỉ là thẩm mỹ — nó là tín hiệu thị giác cho biết dữ liệu đang ở giai đoạn nào của luồng biến đổi, giúp cả đội đọc DAG nhanh.
Use case thực tế — dựng lớp mart doanh thu
Giả sử ta cần một bảng fct_revenue cho dashboard tài chính, từ hai nguồn thô orders và payments.
Bước 1 — staging (view): làm sạch từng nguồn, 1–1.
-- models/staging/jaffle_shop/stg_orders.sql (minh hoạ)
{{ config(materialized='view') }}
select
id as order_id,
user_id as customer_id,
order_date,
status
from {{ source('jaffle_shop', 'orders') }}
-- models/staging/stripe/stg_payments.sql (minh hoạ)
{{ config(materialized='view') }}
select
orderid as order_id,
amount / 100.0 as amount_usd, -- cent -> dollar
payment_method
from {{ source('stripe', 'payments') }}
where status = 'success'
Bước 2 — mart (table), ghép qua ref():
-- models/marts/finance/fct_revenue.sql (minh hoạ)
{{ config(materialized='table', tags=['finance']) }}
with orders as (
select * from {{ ref('stg_orders') }}
),
payments as (
select * from {{ ref('stg_payments') }}
)
select
o.order_date,
o.customer_id,
count(distinct o.order_id) as num_orders,
sum(p.amount_usd) as total_revenue
from orders o
join payments p on o.order_id = p.order_id
where o.status = 'completed'
group by 1, 2
Chú ý: fct_revenue không hề biết tên vật lý thật của bảng nguồn — nó chỉ nói "tôi cần stg_orders và stg_payments". dbt đọc hai ref() này, suy ra staging phải chạy trước, rồi tự chạy đúng thứ tự. Đổi materialization của staging từ view sang table? fct_revenue không cần sửa một chữ.
Chạy chọn lọc — không phải lúc nào cũng chạy tất cả
dbt run chạy toàn bộ project. Trong thực tế bạn thường chỉ muốn chạy một nhánh. Dùng --select (viết tắt -s) cùng graph operators:
# (minh hoạ)
dbt run --select fct_revenue # chỉ 1 model
dbt run --select +fct_revenue # fct_revenue + TẤT CẢ ancestor (thượng nguồn)
dbt run --select stg_orders+ # stg_orders + TẤT CẢ descendant (hạ nguồn)
dbt run --select +fct_revenue+ # cả thượng lẫn hạ nguồn
dbt run --select 1+fct_revenue # chỉ ancestor bậc 1
dbt run --select tag:finance # mọi model gắn tag 'finance'
dbt run --select staging.jaffle_shop # mọi model trong thư mục đó
Ý nghĩa các toán tử:
+model= model và mọi thứ nó phụ thuộc vào (chạy để có đủ nguyên liệu build nó).model+= model và mọi thứ phụ thuộc vào nó (chạy để cập nhật mọi thứ hạ nguồn khi model này đổi).n+model/model+n= giới hạn số bậc lan tỏa.@model= nhưmodel+nhưng thêm cả ancestor của các descendant — hữu ích trong CI khi bảng hạ nguồn chưa tồn tại.tag:, tên thư mục,path:... là các method chọn model theo tiêu chí khác.
Và --full-refresh — cờ đặc biệt buộc incremental model rebuild từ đầu (bỏ qua logic delta), đồng thời drop-and-recreate table nếu schema đổi:
dbt run --full-refresh --select fct_events
Ghi nhớ
- Model = một file
.sql= một câuSELECT. Tên file = tên object. dbt lo toàn bộ DDL. - Không bao giờ hardcode tên bảng.
ref()cho model,source()cho nguồn thô. Chúng vừa điền tên đúng theo môi trường, vừa dựng DAG (thứ tự chạy + lineage). Staging là nơi duy nhất dùngsource(). - Bốn materialization: view (mặc định, nhẹ, luôn tươi, chậm khi query) → table (nhanh, rebuild mỗi run, tốn lưu trữ) → incremental (chỉ xử lý delta, cho bảng rất lớn, phức tạp) → ephemeral (nhúng như CTE, không tạo object).
- Chiến lược chọn: bắt đầu bằng view; lên table khi query chậm; chỉ incremental khi table quá đắt để rebuild. Đừng tối ưu sớm.
- Cấu hình ba tầng (ưu tiên tăng dần):
dbt_project.ymltheo thư mục → property.yml→{{ config() }}trong file. Đặt mặc định theo tầng, override cá biệt trong file. - Phân tầng chuẩn: staging (
stg_, view, 1–1 nguồn) → intermediate (int_, ephemeral/view) → marts (fct_/dim_, table). Tiền tố là tín hiệu giai đoạn biến đổi. - Chạy chọn lọc:
--selectvới+/n+/@/tag:;--full-refreshđể rebuild incremental từ đầu.
Bài tiếp theo: Sources & snapshots — khai báo nguồn, kiểm tra freshness, và bắt giữ thay đổi lịch sử (SCD) bằng snapshot.
Bài viết liên quan
CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.
Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.
Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.
Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.