DHDataHọc
TheoryLesson 5/84 min read+10 XP

Incremental model

Bạn sẽ học được

  • Khi nào một model nên là incremental thay vì table.
  • Viết điều kiện is_incremental() đúng để không trùng dữ liệu.
  • Chọn unique_key và chiến lược ghi phù hợp.
  • Xử lý dữ liệu đến muộn bằng cửa sổ lookback.
  • Biết khi nào bắt buộc phải --full-refresh.

Khái niệm cốt lõi

Một bảng fact có 40 triệu dòng và mỗi ngày thêm 180.000 dòng. Nếu materialization là table, mỗi đêm dbt phải quét và ghi lại toàn bộ 40 triệu dòng: đo được 28 phút. Phần lớn thời gian đó lặp lại công việc của hôm qua.

incremental chỉ xử lý dữ liệu mới. Cùng bảng đó, mỗi đêm chỉ đọc 180.000 dòng mới, chạy trong 40 giây.

Cơ chế gồm ba phần:

  1. Lần chạy đầu tiên: dbt tạo bảng như bình thường (giống table).
  2. Các lần sau: dbt thêm điều kiện lọc vào câu SELECT của bạn qua is_incremental().
  3. dbt ghi phần kết quả vào bảng có sẵn bằng chiến lược đã cấu hình.
code
{{ config(
    materialized='incremental',
    unique_key='order_id',
    incremental_strategy='merge'
) }}

select
    order_id,
    customer_id,
    order_date,
    amount,
    updated_at
from {{ ref('stg_orders') }}

{% if is_incremental() %}
  where updated_at > (select coalesce(max(updated_at), '1900-01-01') from {{ this }})
{% endif %}

{{ this }} trỏ tới chính bảng đang được build. Đây là cách duy nhất để model biết trạng thái hiện tại của nó.

Không có is_incremental() thì model là incremental trên giấy nhưng mỗi lần chạy lại đọc toàn bộ nguồn — chậm mà vẫn có thể trùng.

Chiến lược ghi

Chiến lượcCách làmDùng khi
appendChèn thêm, không kiểm traBảng chỉ thêm, không bao giờ sửa
mergeKhớp theo unique_key, cập nhật hoặc chènDữ liệu có thể sửa (trạng thái đơn)
delete+insertXoá các dòng cùng khoá rồi chèn lạiAdapter không hỗ trợ merge

Với dbt-postgres, mặc định là append. Nếu bảng có cập nhật trạng thái — đơn từ pending sang paid — append tạo hai dòng cho cùng một order_id và mọi phép sum đều sai. Phải đặt unique_key và merge (hoặc delete+insert).

Dữ liệu đến muộn

Điều kiện updated_at > max(updated_at) bỏ sót bản ghi đến muộn: một đơn tạo ngày 9/3 nhưng được cập nhật vào kho lúc 10/3 lại mang updated_at cũ. Nó không vượt qua bộ lọc và vĩnh viễn không được nạp.

Cách sửa là mở rộng cửa sổ về quá khứ:

code
{% if is_incremental() %}
  where updated_at >= (select max(updated_at) from {{ this }}) - interval '3 days'
{% endif %}

Ba ngày là cửa sổ lookback. Mỗi lần chạy đọc lại 3 ngày gần nhất — với 180.000 dòng/ngày là 540.000 dòng, vẫn nhanh hơn nhiều so với 40 triệu.

Vì cửa sổ đọc lại các dòng cũ, bắt buộc phải có unique_key và chiến lược merge/delete+insert. Nếu để append, mỗi lần chạy lại 3 ngày sẽ nhân bản đúng phần đó.

--full-refresh

Một số thay đổi không thể áp dụng tăng dần:

  • Thêm cột mới vào SELECT.
  • Đổi kiểu dữ liệu của cột.
  • Đổi logic tính toán cho toàn bộ lịch sử.
  • Đổi unique_key.

Khi đó chạy:

code
dbt run --select fct_orders --full-refresh

dbt drop bảng và build lại từ đầu. Mất 28 phút nhưng cho kết quả đúng. Trong production nên thông báo trước vì bảng tạm thời không đọc được trong lúc build.

Đổi cột mà quên --full-refresh là lỗi phổ biến: dbt cố chèn cột mới vào bảng cũ và báo lỗi kiểu column does not exist.

Kiểm thử model incremental

Incremental sai thường không báo lỗi. Nó chỉ âm thầm nhân đôi dữ liệu. Luôn có:

code
models:
  - name: fct_orders
    columns:
      - name: order_id
        tests:
          - unique
          - not_null

Test này chỉ có tác dụng khi build lại sau lần chạy incremental thứ hai. Quy trình kiểm tra:

  1. Chạy dbt build --select fct_orders hai lần liên tiếp.
  2. So count(*) trước và sau — phải bằng nhau nếu không có dữ liệu mới.
  3. Nếu tăng, kiểm tra unique_key và chiến lược ghi.
code
select count(*) as tong, count(distinct order_id) as khac_nhau
from fct_orders;

Hai số phải bằng nhau.

Thực hành

  1. Đổi một model lớn sang incremental với unique_key.
  2. Chạy hai lần, xác nhận số dòng không đổi.
  3. Thêm cửa sổ lookback 3 ngày và giải thích chi phí.
  4. Chèn một bản ghi có updated_at cũ, xác nhận nó được nạp.
  5. Thêm cột mới, chạy không --full-refresh, đọc lỗi, rồi chạy lại với cờ đó.

Lỗi thường gặp

Triệu chứngNguyên nhânCách sửa
Số dòng tăng sau mỗi lần chạyappend không unique_keyĐặt unique_key + merge
Báo cáo doanh thu gấp đôiTrùng ở biên cửa sổ lookbackDùng merge và unique_key
Đơn đến muộn bị mấtĐiều kiện > chỉ lấy dòng mớiMở lookback về vài ngày
column does not exist sau khi thêm cộtBảng cũ chưa có cột--full-refresh một lần
Lần chạy đầu rất chậmLần đầu build toàn bộ, bình thườngChấp nhận, hoặc chạy ngoài giờ
Model incremental chậm như tableThiếu is_incremental()Thêm khối {% if %}

Tóm tắt

  • incremental chỉ hữu ích khi có điều kiện is_incremental().
  • unique_key + merge là cấu hình an toàn cho dữ liệu có cập nhật.
  • Dữ liệu đến muộn cần cửa sổ lookback, kèm chiến lược ghi không nhân bản.
  • Thêm cột hoặc đổi logic lịch sử phải --full-refresh.
  • Chạy hai lần và kiểm tra count(*) là cách phát hiện lỗi sớm nhất.

Use ← → to move between lessons