具代表性的面試主題

資料工程面試:如何設計可靠的 dbt 增量模型?

資料困難
Offer.cc 編輯團隊發佈 更新

題幹

在資料持續成長、每天不能全量重算時,你會如何設計可靠的 dbt 增量模型?

題幹與適用場景

面試官給你一張持續寫入的事件表,要求建立按天彙總的事實表。第一次執行可以掃描歷史資料,但後續每次只能處理最近變動的資料;事件可能延遲、更新或重複抵達,模型欄位也可能變動。請說明篩選邊界、唯一鍵、增量策略、回補方式,以及失敗後如何復原。

本文假設倉庫支援 SQL,目標模型按 date_day 聚合,事件有 event_atupdated_at 與穩定的 event_id。這些假設應在面試開頭說清楚;若沒有穩定唯一鍵,更新語意和去重方案都會改變。

面試官考察點

面試官要看的是你能否同時做到「少掃資料」與「結果仍正確」。普通回答只會說使用 is_incremental() 和時間戳篩選;高品質回答會解釋為什麼篩選視窗需要涵蓋延遲資料、為什麼彙總目標必須宣告符合粒度的 unique_key,以及什麼時候一定要 --full-refresh

也會觀察你是否區分三種風險:漏掉延遲抵達的紀錄、同一業務粒度重複寫入、歷史邏輯改變後新舊結果不一致。面試準備資料通常把增量模型、快照和依賴圖放在資料工程或分析工程的核心範圍;這題適合考察 SQL、資料建模與執行治理的組合能力。

回答前需要釐清的問題

業務粒度是什麼?

如果一列代表一天,date_day 可以作為唯一鍵;如果一列代表使用者和日期,鍵就應是 (user_id, date_day)。粒度不同會改變合併條件、重複檢查和回補成本。

延遲資料最多晚到多久?

若事件通常只晚到兩天,可以每次重算最近三天;若沒有可接受的上限,就不能只用固定視窗,需要水位線、分區重算或定期全量校準。視窗不是越小越好,必須覆蓋業務可接受的延遲範圍。

上游更新由哪個欄位表示?

event_at 表示業務發生時間,updated_at 表示紀錄最後修改時間。只按照 event_at 篩選會漏掉「舊事件後來被修改」的情況;應優先使用可靠的 updated_at,並確認來源系統不會把它回撥。

模型邏輯或欄位變更如何發布?

新增欄位、刪除欄位與計算邏輯變更的處理方式不同。要先確認是否允許 on_schema_change 自動同步、歷史欄位是否需要回填,以及發布時能否安排全量刷新。

30 秒回答框架

「我先確認目標粒度和延遲上限。第一次執行做全量建置,後續用 is_incremental()updated_at 篩選,並向前擴大一個延遲視窗。目標模型宣告與粒度一致的 unique_key,讓同一天的新資料更新原列而不是追加重複列。視窗內按鍵去重後再聚合,用 merge 或倉庫等價策略寫入。視窗、唯一鍵和結構變更都用測試驗證;如果邏輯改變讓歷史結果不再一致,就執行受控的 --full-refresh,並重跑下游模型。最後監控處理筆數、最大事件時間、重複鍵和新舊結果差異。」

分步驟深入解答

1. 先用全量方案定義正確性基線

先寫出全量查詢:讀取所有事件,再按目標粒度聚合。這是正確性基線,之後的增量結果必須能和相同時間範圍的全量結果對帳。沒有基線就無法判斷「少掃資料」是否造成漏算。

2. 選擇增量篩選邊界

增量模型只在目標表已存在、沒有傳入 --full-refresh,且模型設定為 incremental 時進入增量分支。可以用目標表的最大更新時間減去延遲視窗:

sql
{{
  config(
    materialized = 'incremental',
    unique_key = ['date_day'],
    incremental_strategy = 'merge'
  )
}}

with source_events as (
  select *
  from {{ ref('app_events') }}
  {% if is_incremental() %}
    where updated_at >= (
      select coalesce(max(updated_at), '1900-01-01') from {{ this }}
    ) - interval '3 day'
  {% endif %}
)
select
  cast(event_at as date) as date_day,
  count(distinct event_id) as events,
  max(updated_at) as max_updated_at
from source_events
group by 1

範例中的三天只是面試假設,不是通用常數。視窗應由延遲分布、SLA 和重算成本決定;實際日期函式也要依 adapter 調整。

3. 讓唯一鍵符合模型粒度

如果目標表按天儲存,date_day 是唯一鍵;如果按使用者和日期儲存,就使用 ['user_id', 'date_day']。唯一鍵欄位不能含空值,否則 merge 可能無法匹配並產生重複列。沒有唯一鍵時,許多 adapter 只能 append,重算視窗會把同一粒度寫出多列。

4. 在視窗內先去重再聚合

同一事件可能因重放或 CDC 更新產生多筆紀錄。按 event_id 和更新時間排序,保留每個事件的最新版本,再做每日彙總:

sql
with ranked_events as (
  select
    *,
    row_number() over (
      partition by event_id
      order by updated_at desc, ingest_seq desc
    ) as rn
  from source_events
),
deduped_events as (
  select * from ranked_events where rn = 1
)
select
  cast(event_at as date) as date_day,
  count(*) as events,
  max(updated_at) as max_updated_at
from deduped_events
group by 1

只有當 ingest_seq 能穩定打破相同更新時間時才使用它;否則要把並列規則說成待確認的來源契約。去重必須在聚合前完成,否則同一事件的兩個版本會同時計數。

5. 選擇 merge、分割區覆寫或 append

merge 適合按唯一鍵更新與插入;按分割區重算的場景可以使用 insert_overwrite,它依賴分割區而非逐列唯一鍵;純追加事件且上游永不更新時,append 更簡單。選擇依據是更新語意、倉庫掃描成本和 adapter 能力,不要把一種策略當成所有倉庫的預設答案。

6. 處理結構與邏輯變更

新增欄位不一定會回填舊列;刪除欄位或型別變更也可能只在執行時暴露。on_schema_change 可設定為 ignorefailappend_new_columnssync_all_columns,但它只追蹤頂層欄位,不能取代歷史資料回補。計算邏輯改變後,新舊歷史可能使用不同規則,此時要執行 --full-refresh,並依賴關係重建下游增量模型。

7. 設計回補和失敗復原

把延遲視窗、目標最大更新時間、來源水位寫入執行記錄。某次視窗執行失敗時,下一次仍從已提交的目標水位重新計算,不要把記憶體中的「已處理到」當成事實。大範圍歷史修復可按日期分片並限制並行;完成後用抽樣全量查詢對帳,避免一次刷新壓垮倉庫。

8. 建立驗證閉環

至少驗證四組訊號:視窗內每個 event_id 最多一列;目標唯一鍵沒有重複;最近視窗和全量重算結果的差異在允許範圍內;每次執行處理筆數和最大 updated_at 沒有異常跳變。測試要涵蓋空輸入、重複事件、舊事件更新、延遲事件、視窗邊界相等值,以及全量刷新後再增量執行。

高品質示範回答

「我會先確認模型粒度、延遲上限和來源的更新欄位。假設目標是一列一天,事件有穩定的 event_idupdated_at。第一次執行全量建置;後續用 is_incremental() 從目標表最大更新時間向前回看三天。這個視窗依延遲分布決定,三天不是固定答案。

視窗內先按 event_id 和更新時間去重,再按天聚合,目標模型把 date_day 設成 unique_key,用 merge 更新最近幾天,避免重複列。如果目標是使用者日粒度,就改成複合鍵。只按事件發生時間會漏掉舊事件後續更新,所以我會優先使用可靠的更新時間欄位。

我會把視窗大小、最大水位、處理筆數、重複鍵和視窗對帳差異當成執行指標。新增欄位可依結構變更策略處理,但不會自動填滿歷史值;如果計算邏輯改變或需要歷史回補,就安排分片的 full refresh,並重跑受影響的下游模型。最後用全量查詢做抽樣對帳,驗證空輸入、延遲、重複、邊界時間和失敗重試,確保增量最佳化沒有犧牲正確性。」

常見錯誤

只按 event_at 篩選 → 漏掉舊事件更新 → 使用 updated_at 或明確的 CDC 水位

事件發生時間不會隨後續修正而改變。若業務允許更新,就必須按更新時間或變更序列篩選,並確認欄位可靠。

沒有唯一鍵就使用 merge → 無法穩定匹配 → 先定義模型粒度和非空鍵

唯一鍵不是隨便選一欄;它必須唯一標識目標的一列。若粒度是使用者與日期,單獨使用日期會把不同使用者合併。

以為增量模型會自動回填新欄位 → 歷史值保持空白 → 設計回補或 full refresh

結構同步和歷史資料回填是兩件事。新增欄位只改變結構時可以輕量同步;需要舊紀錄有值時必須額外更新或重建。

固定使用一小時視窗 → 延遲分布超過視窗時漏算 → 用分位數和對帳資料校準

視窗大小應由延遲分布、SLA 和成本共同決定。監控視窗外抵達量,發現異常時擴大視窗或執行分片回補。

追問及應對

如果每天有 5% 的事件在兩天後抵達,你會如何選視窗?

先確認業務允許的正確性延遲。如果日報允許隔天修正,可以覆蓋兩到三天並把延遲事件納入對帳;如果必須在首日穩定,就需要水位線加回補佇列,不能只靠更大的 SQL 視窗。選擇應由延遲分布和成本曲線驗證,而不是直接套用比例。

如果 unique_key 在來源資料中重複,會發生什麼?

同一次 merge 的新資料或目標資料含重複鍵時,adapter 可能報錯,也可能產生不確定結果。先在增量輸入和目標表分別做唯一性檢查,找出重複來源;再按事件版本去重,或重新定義能表達真實粒度的複合鍵。不要用隨機 ID 掩蓋業務鍵不穩定。

模型 SQL 改了,但只想重算最近七天,可以繼續增量執行嗎?

只有在歷史列的計算結果不受新邏輯影響時才安全。若邏輯改變會影響全部歷史,最近七天增量會留下新舊規則混合的表,應執行受控 full refresh,或按受影響分割區分片重算,再同步重跑下游模型。

上游表被截斷後,增量模型如何復原?

先停止繼續推進水位,確認來源表重建完成,再從可靠的來源快照或 CDC 起點回補。若無法證明來源表涵蓋目標所需歷史,直接增量執行會把目標當成完整基線,必須恢復快照或執行全量重建。

公開來源

同類題目