Scheduling、Idempotency 與 Incremental Runs
學習目標:設計安全重跑;理解 full refresh 與 incremental processing。
排程在凌晨 2 點跑你的月度 pipeline,中途因為網路問題失敗了一半。值班的人手動重新觸發了一次,結果早上你發現輸出表裡同一個 batch_id 被算了兩次,總額整整多了一倍。這是典型的非 idempotent pipeline:重跑同一批輸入,卻沒有把之前寫入的結果覆寫或替換,而是疊加上去。設計安全的重跑,是任何排程化 pipeline 的基本要求,不是進階選配。
# 錯誤做法:每次執行都用 append,重跑會造成重複列
result.to_sql("monthly_summary", con=engine, if_exists="append")
# 正確做法:以 run 的處理範圍為 key,先刪除同一範圍的舊結果,再寫入新結果(idempotent)
def publish_idempotent(result: pd.DataFrame, period: str, engine) -> None:
with engine.begin() as conn:
conn.execute(
"DELETE FROM monthly_summary WHERE period = :period",
{"period": period}
)
result.to_sql("monthly_summary", con=conn, if_exists="append", index=False)
# incremental run 需要追蹤已處理到哪個時間點(watermark)
def get_watermark(engine) -> str:
row = engine.execute("SELECT MAX(period) AS last_period FROM monthly_summary").first()
return row.last_period if row and row.last_period else "1970-01"| 方式 | 適合情境 | 風險 |
|---|---|---|
| Full refresh | 資料量小、來源會回溯修正歷史資料 | 資料量變大後執行時間與成本上升 |
| Incremental | 資料量大、來源穩定只新增不回溯 | 需要正確追蹤 watermark,並處理 late-arriving data |
除了「先刪除同一期間再寫入」,另一種常見做法是以 (source_system, period, metric) 當作主鍵做 upsert:如果這個主鍵已存在就更新數值,不存在就新增一列。兩種做法都能讓同一批輸入重跑多次後,輸出表的內容保持一致;選哪一種取決於資料庫是否方便支援 upsert 語法,以及一次重算的範圍是否總是以完整期間為單位。
- 確認寫入方式是否 idempotent
同樣的輸入重跑兩次,輸出表的內容應該完全相同,不應該重複累加。 - 選擇 full refresh 或 incremental
依資料量、來源是否會回溯修正,決定要整批重算還是只算新增部分。 - 追蹤 watermark
incremental pipeline 需要記錄已處理到哪個時間點,作為下次執行的起點。 - 處理 late-arriving data
設計機制讓比預期晚到的資料,仍能被正確涵蓋進對應期間的結果。
注意
排程因網路問題失敗,值班人員手動重跑了一次同一個 batch,結果輸出表裡的總額變成兩倍。這反映了 pipeline 缺少什麼設計?