Bài tập: đọc project dbt
Bạn sẽ học được
- Chẩn đoán lỗi incremental nhân bản dữ liệu.
- Tính được doanh thu sai do join với dimension trùng.
- Chọn đúng test để bắt từng loại lỗi.
- Hiểu vì sao CI chạy
dbt runlà chưa đủ. - Sắp xếp thứ tự khắc phục một sự cố dữ liệu.
Tình huống
Công ty Bình Minh có project dbt_shop gồm bốn model:
| Model | Materialization | Ghi chú |
|---|---|---|
stg_orders | view | Đọc từ raw.orders |
stg_customers | view | Đọc từ raw.customers |
fct_orders | incremental | Chưa khai báo unique_key, chiến lược mặc định của dbt-postgres |
dim_customers | table | select * from stg_customers |
Model fct_orders lọc incremental bằng:
{% if is_incremental() %}
where order_date >= (select max(order_date) from {{ this }})
{% endif %}Dashboard doanh thu đọc từ fct_orders.
Nhật ký tuần 10–14/3:
| Ngày | Việc đã làm | Kết quả |
|---|---|---|
| 9/3 | Pipeline chạy bình thường | Ngày 9/3: 8.400 đơn, doanh thu 840.000.000 |
| 10/3 | dbt run lỗi ở bước cuối, kỹ sư chạy lại toàn bộ | Ngày 9/3 có 16.800 dòng, dashboard hiện 1.680.000.000 |
| 11/3 | Không ai phát hiện; chạy tiếp | — |
| 12/3 | Nguồn raw.customers bắt đầu lưu lịch sử: 150 khách có 2 dòng | dim_customers có 3.150 dòng |
| 13/3 | 150 khách đó phát sinh 420 đơn, tổng 42.000.000 | Dashboard ngày 12/3 hiện 882.000.000 thay vì 840.000.000 |
| 14/3 | Kiểm tra thấy test unique trên fct_orders.order_id đang để severity: warn | CI vẫn xanh |
Mỗi ngày đều có đúng 8.400 đơn mới, giá trị trung bình 100.000 mỗi đơn.
Câu hỏi
Câu 1. Vì sao ngày 9/3 có 16.800 dòng?
Câu 2. Sửa cấu hình tối thiểu cho fct_orders là gì?
Câu 3. Vì sao dashboard ngày 12/3 hiện 882.000.000 thay vì 840.000.000?
Câu 4. Test nào trong schema.yml bắt được lỗi của dim_customers?
Câu 5. Vì sao test unique trên fct_orders không chặn được sự cố ngày 10/3?
Câu 6. Vì sao chạy dbt run rồi mới dbt test không ngăn được dữ liệu sai đi tới dashboard?
Câu 7. CI cần thay đổi gì để lỗi tương tự không vào production?
Câu 8. Materialization phù hợp cho stg_orders và fct_orders là gì, vì sao?
Câu 9. Khi nào bắt buộc phải chạy --full-refresh cho fct_orders?
Câu 10. Sắp xếp thứ tự năm bước khắc phục.
Đáp án
Câu 1 — Append cộng dồn hai lần
fct_orders dùng materialization incremental nhưng không có unique_key, nên dbt-postgres dùng chiến lược append: chỉ chèn thêm, không kiểm tra trùng.
Lần chạy thứ nhất đã ghi 8.400 dòng. Lần chạy lại thêm 8.400 dòng nữa. Tổng:
8.400 + 8.400 = 16.800Điều kiện order_date >= max(order_date) cũng góp phần: nó lấy lại đúng ngày lớn nhất đã có, nên lần chạy lại đọc lại toàn bộ ngày 9/3 thay vì bỏ qua.
Câu 2 — Thêm unique_key và chiến lược ghi
{{ config(
materialized='incremental',
unique_key='order_id',
incremental_strategy='merge'
) }}Với unique_key, lần chạy thứ hai cập nhật 8.400 dòng cũ thay vì thêm mới. Số dòng vẫn là 8.400.
Nên sửa thêm điều kiện lọc thành updated_at > ... kèm cửa sổ lookback, để không quét lại toàn bộ ngày:
{% if is_incremental() %}
where updated_at >= (select max(updated_at) from {{ this }}) - interval '3 days'
{% endif %}Nhưng mức tối thiểu để hết trùng là unique_key + merge.
Câu 3 — Dimension trùng làm join nhân đôi
dim_customers được build bằng select * from stg_customers. Khi nguồn bắt đầu lưu lịch sử, 150 khách có 2 dòng:
3.000 khách + 150 dòng thêm = 3.150 dòngfct_orders join với dim_customers theo customer_id. Mỗi đơn của 150 khách đó khớp hai dòng dimension, nên bị đếm hai lần.
150 khách đó có 420 đơn, tổng 42.000.000. Phần bị cộng thêm đúng bằng 42.000.000:
840.000.000 + 42.000.000 = 882.000.000Sai lệch 5% — đủ nhỏ để không ai để ý, đủ lớn để quyết định sai.
Câu 4 — unique trên khoá của dimension
models:
- name: dim_customers
columns:
- name: customer_id
tests:
- unique
- not_nullTest này thất bại ngay khi dim_customers có 3.150 dòng cho 3.000 khách. Nó bắt lỗi tại nguồn của vấn đề, trước khi lỗi lan sang fct_orders.
Cần thêm relationships từ fct_orders.customer_id tới dim_customers.customer_id — test này không bắt được trùng, nhưng bắt được khoá mồ côi.
Câu 5 — severity: warn không chặn
- name: order_id
tests:
- unique:
config:
severity: warnwarn in cảnh báo và cho pipeline chạy tiếp. Với 16.800 dòng nhưng chỉ 8.400 order_id khác nhau, test thực sự thất bại — nhưng ở mức cảnh báo, dbt build vẫn thoát với mã 0 và CI vẫn xanh.
Khoá chính là quy tắc bắt buộc. Mức đúng phải là error (mặc định), không phải warn.
Câu 6 — run xong hết mới tới test
dbt run && dbt test chạy toàn bộ model trước, sau đó mới chạy test. Khi test phát hiện lỗi, fct_orders và mọi mart phụ thuộc đã được ghi xong. Dashboard đã đọc dữ liệu sai.
dbt build chạy xen kẽ theo DAG: stg_orders → test của stg_orders → fct_orders → test của fct_orders. Test thất bại chặn mọi thứ phía sau.
Đây là lý do mặc định nên dùng dbt build, không dùng hai lệnh rời.
Câu 7 — CI dùng dbt build và chọn phần thay đổi
dbt build \
--select state:modified+ \
--defer \
--state ./prod-manifestBa thay đổi:
buildthay chorun— test chạy xen kẽ và chặn được.state:modified+— chỉ build model thay đổi và model phụ thuộc, CI nhanh nên không ai có lý do bỏ qua.--defer— model không đổi đọc bảng production, tránh build lại 300 model.
Ngoài ra cần chặn quyền chạy --target prod từ máy cá nhân.
Câu 8 — View cho staging, incremental cho fact
| Model | Materialization | Lý do |
|---|---|---|
stg_orders | view | Chỉ đổi tên và ép kiểu; view luôn phản ánh nguồn mới, không tốn chỗ |
fct_orders | incremental | Bảng lớn, dữ liệu chủ yếu thêm mới; build lại toàn bộ quá tốn |
Nếu fct_orders để table, mỗi đêm phải quét và ghi lại toàn bộ lịch sử. Nếu stg_orders để table, mỗi lần nguồn có dữ liệu mới phải chờ build mà không có lợi ích gì.
Câu 9 — Khi thay đổi cấu trúc hoặc logic lịch sử
--full-refresh bắt buộc khi:
- Thêm cột mới vào
SELECT. - Đổi kiểu dữ liệu một cột.
- Đổi công thức tính cho toàn bộ lịch sử.
- Đổi
unique_keyhoặc chiến lược ghi từappendsangmerge.
Với trường hợp này, sau khi thêm unique_key, cần chạy một lần:
dbt run --select fct_orders --full-refreshđể xoá 16.800 dòng cũ và build lại sạch từ nguồn.
Câu 10 — Thứ tự khắc phục
- Dừng pipeline để không phát sinh thêm dòng trùng.
- Sửa
fct_orders: thêmunique_key+merge, sửa điều kiện lọc và lookback. - Sửa
dim_customers: dedupe theocustomer_id(lấy bản ghi mới nhất), thêm testunique+not_nullở mứcerror. - Build lại và đối chiếu:
--full-refreshchofct_orders, sau đó socount(*)và tổng doanh thu với nguồn cho ngày 9/3 và 12/3. - Sửa quy trình: CI dùng
dbt build, test khoá ở mứcerror, thêm kiểm tracount(*) = count(distinct order_id)vào dashboard giám sát.
Không làm bước 4 trước bước 2 và 3 — build lại bằng cấu hình cũ sẽ tạo thêm dữ liệu trùng.
Cách chấm điểm
| Số câu đúng | Mức | Gợi ý |
|---|---|---|
| 9–10 | Vững | Thiết kế lại project mẫu có CI và test đầy đủ |
| 7–8 | Khá | Xem lại bài 2 và bài 5 |
| 5–6 | Trung bình | Đọc lại bài 2, 4, 5 |
| Dưới 5 | Cần ôn | Làm lại từ bài 1 |
Bản đồ câu hỏi → bài học
| Câu | Bài |
|---|---|
| 1, 2 | Bài 5: Incremental model |
| 3, 4 | Bài 2: Kiểm thử dữ liệu, bài 3: Tầng staging và mart |
| 5, 6 | Bài 2: Kiểm thử dữ liệu |
| 7 | Bài 7: Tài liệu, lineage và môi trường |
| 8, 9 | Bài 4: Cấu trúc project và materialization, bài 5 |
| 10 | Tổng hợp |
Bài tập mở rộng
- Viết lại
fct_ordershoàn chỉnh vớiunique_key, lookback và test. - Viết model
dim_customersdedupe bằngrow_number(). - Thêm test
uniqueở mứcerrorvà chứng minh CI đỏ khi dữ liệu trùng. - Viết đoạn
dbt buildtrong CI vớistate:modified+và--defer. - Tính doanh thu đúng cho ngày 9/3 và 12/3 sau khi sửa.
Tóm tắt
incrementalkhông cóunique_keylà append — chạy lại sẽ nhân đôi.- Dimension trùng làm join nhân bản dòng và thổi phồng doanh thu.
- Test khoá phải ở mức
error, và phải đặt ở đúng tầng. dbt buildchạy test xen kẽ;dbt run && dbt testthì không.- Sửa cấu hình trước, build lại sau, cải tiến quy trình cuối cùng.
Use ← → to move between lessons