DHDataHọc
QuizLesson 8/87 min read+10 XP

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 run là 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:

ModelMaterializationGhi chú
stg_ordersviewĐọc từ raw.orders
stg_customersviewĐọc từ raw.customers
fct_ordersincrementalChưa khai báo unique_key, chiến lược mặc định của dbt-postgres
dim_customerstableselect * from stg_customers

Model fct_orders lọc incremental bằng:

code
{% 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àyViệc đã làmKết quả
9/3Pipeline chạy bình thườngNgày 9/3: 8.400 đơn, doanh thu 840.000.000
10/3dbt 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/3Không ai phát hiện; chạy tiếp—
12/3Nguồn raw.customers bắt đầu lưu lịch sử: 150 khách có 2 dòngdim_customers có 3.150 dòng
13/3150 khách đó phát sinh 420 đơn, tổng 42.000.000Dashboard ngày 12/3 hiện 882.000.000 thay vì 840.000.000
14/3Kiểm tra thấy test unique trên fct_orders.order_id đang để severity: warnCI 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:

code
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

code
{{ 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:

code
{% 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:

code
3.000 khách + 150 dòng thêm = 3.150 dòng

fct_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:

code
840.000.000 + 42.000.000 = 882.000.000

Sai lệch 5% — đủ nhỏ để không ai để ý, đủ lớn để quyết định sai.

Câu 4 — unique trên khoá của dimension

code
models:
  - name: dim_customers
    columns:
      - name: customer_id
        tests:
          - unique
          - not_null

Test 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

code
      - name: order_id
        tests:
          - unique:
              config:
                severity: warn

warn 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

code
dbt build \
  --select state:modified+ \
  --defer \
  --state ./prod-manifest

Ba thay đổi:

  1. build thay cho run — test chạy xen kẽ và chặn được.
  2. state:modified+ — chỉ build model thay đổi và model phụ thuộc, CI nhanh nên không ai có lý do bỏ qua.
  3. --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

ModelMaterializationLý do
stg_ordersviewChỉ đổi tên và ép kiểu; view luôn phản ánh nguồn mới, không tốn chỗ
fct_ordersincrementalBả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_key hoặc chiến lược ghi từ append sang merge.

Với trường hợp này, sau khi thêm unique_key, cần chạy một lần:

code
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

  1. Dừng pipeline để không phát sinh thêm dòng trùng.
  2. Sửa fct_orders: thêm unique_key + merge, sửa điều kiện lọc và lookback.
  3. Sửa dim_customers: dedupe theo customer_id (lấy bản ghi mới nhất), thêm test unique + not_null ở mức error.
  4. Build lại và đối chiếu: --full-refresh cho fct_orders, sau đó so count(*) và tổng doanh thu với nguồn cho ngày 9/3 và 12/3.
  5. Sửa quy trình: CI dùng dbt build, test khoá ở mức error, thêm kiểm tra count(*) = 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 đúngMứcGợi ý
9–10VữngThiết kế lại project mẫu có CI và test đầy đủ
7–8KháXem lại bài 2 và bài 5
5–6Trung bìnhĐọc lại bài 2, 4, 5
Dưới 5Cần ônLàm lại từ bài 1

Bản đồ câu hỏi → bài học

CâuBài
1, 2Bài 5: Incremental model
3, 4Bài 2: Kiểm thử dữ liệu, bài 3: Tầng staging và mart
5, 6Bài 2: Kiểm thử dữ liệu
7Bài 7: Tài liệu, lineage và môi trường
8, 9Bài 4: Cấu trúc project và materialization, bài 5
10Tổng hợp

Bài tập mở rộng

  1. Viết lại fct_orders hoàn chỉnh với unique_key, lookback và test.
  2. Viết model dim_customers dedupe bằng row_number().
  3. Thêm test unique ở mức error và chứng minh CI đỏ khi dữ liệu trùng.
  4. Viết đoạn dbt build trong CI với state:modified+ và --defer.
  5. Tính doanh thu đúng cho ngày 9/3 và 12/3 sau khi sửa.

Tóm tắt

  • incremental không có unique_key là 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 build chạy test xen kẽ; dbt run && dbt test thì 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