外观
幂等回刷与重跑怎么设计
适用场景
- 源数据迟到 / 修数,需要对历史
dt补数或回刷 - 任务失败重试、手动重跑,必须保证跑几次结果一样
- 下游已依赖分区表,回刷后要能级联重跑且对账可解释
不适用:
- 无分区的全量覆盖小维表(可整表替换,但仍需版本或备份策略)
- 实时 Exactly-Once 语义(本篇按离线分区批处理)
- 「只追加不更新」且业务允许重复行的流水归档(需另定去重层)
真实场景(一条)
投放渠道文件常迟到 T+2。某周一要把 dt=2025-01-08~01-10 三天 ODS 重拉,并级联 DWD/DWS/对账 ADS。
若写入不是幂等的,会出现:
- 重跑三次 ODS,分区内行数 ×3,消耗翻倍
- 只重跑了 DWD,DWS 仍是旧数,日报与对账打架
- 动态分区一次写出多余空目录,元数据膨胀
目标契约:
- 同一
dt覆盖写(或先删后写),重复跑结果稳定 - 回刷列出分区范围,上游完成再下游
- 回刷前后有行数 / 金额 / 主键唯一验收
怎么做(步骤清单)
1. 先写死「幂等」定义(团队共识)
对本域日批表,幂等至少包含:
| 条款 | 含义 |
|---|---|
| 同分区可重入 | 任意次重跑 dt=D,结果集与成功跑一次一致 |
| 不叠数 | 禁止在原有文件上无控制追加导致重复 |
| 可级联 | 上游 dt=D 变更 ⇒ 下游同 dt(或约定窗口)必须重跑 |
| 可审计 | 留下回刷人、时间、分区列表、前后校验 |
TIP
幂等是写入契约,不是调度点一下重跑就自动具备。表格式、插入语句、依赖边都要配合。
2. 选定写入模式(按层推荐)
| 层 | 推荐写法 | 说明 |
|---|---|---|
| ODS / DWD / DWS 日增量 | INSERT OVERWRITE TABLE ... PARTITION (dt=...) | 同分区覆盖;最常见 |
| 需要保留旧版本 | 写入新分区或版本目录,再切视图 | 回滚友好,成本更高 |
| 引擎支持事务/ACID | 按官方语义做动态分区覆盖 | 先确认隔离级别与小文件 |
| ADS 全量快照 | 按快照日覆盖,或整表替换+备份 | 避免与明细分区语义混用 |
sql
-- 推荐:静态分区覆盖(幂等)
INSERT OVERWRITE TABLE dwd_ad_spend_detail_di
PARTITION (dt = '${biz_date}')
SELECT
bill_id,
channel_id,
play_id,
creative_id,
cost_amount,
event_ts,
account_tz,
src_system,
is_valid,
filter_reason,
current_timestamp() AS etl_ts
FROM tmp_dwd_ad_spend_${biz_date_nodash};
-- 危险:对已有分区无条件 INSERT INTO 追加(重跑即叠数)
-- INSERT INTO TABLE dwd_ad_spend_detail_di PARTITION (dt = '${biz_date}') SELECT ...3. 回刷前清单(不满足就先别跑)
- [ ] 明确分区列表:
dt IN (...)或起止日期 - [ ] 源数据已就绪(文件到达、水位、对账票)
- [ ] 写入方式是 OVERWRITE / 先删分区再写,而不是盲追加
- [ ] 下游依赖列表已导出(DWD→DWS→ADS)
- [ ] 回刷窗口与业务沟通:看板会短暂旧数或雪花
- [ ] 大范围回刷评估:队列、小文件、元数据压力
4. 级联顺序与调度落点
text
对每个 dt in 回刷列表(升序或按依赖允许的并行度):
1) ODS 覆盖写 dt
2) DQ P0 出门禁(主键/空值/空分区)
3) DWD 覆盖写 dt(可关联当时维表口径,见下)
4) DQ P0
5) DWS 覆盖写 dt
6) ADS 覆盖写 dt / 对账结果
7) 分区级验收 SQL维表注意:
- 回刷历史事实时,维表应用业务当日的 SCD 区间(见 拉链表),不要误用「今天的维」解释昨天的事实
- 维表自身若也修数,先回刷维,再回刷事实链路
5. 「先删后写」与路径级清理(可选加固)
当表是外部表或曾用 INSERT INTO 脏写过:
bash
# 示意:删除目标分区路径后再写(需与 Metastore 分区一致)
# 1) 备份(可选)
hdfs dfs -cp /warehouse/dwd/dwd_ad_spend_detail_di/dt=${biz_date} \
/backup/dwd_ad_spend_detail_di/dt=${biz_date}_${ts}
# 2) 丢分区 + 删数据(按你们规范二选一或组合)
hive -e "ALTER TABLE dwd_ad_spend_detail_di DROP IF EXISTS PARTITION (dt='${biz_date}');"
hdfs dfs -rm -r -skipTrash /warehouse/dwd/dwd_ad_spend_detail_di/dt=${biz_date}
# 3) INSERT OVERWRITE 或 LOAD 后 MSCK/ADD PARTITIONWARNING
只删 HDFS 不更新 Metastore,或只丢分区不删文件,都会留下「幽灵数据 / 幽灵分区」。删改必须成对。
6. 任务代码层:去掉隐藏的非幂等
检查这些反模式:
| 反模式 | 后果 | 改法 |
|---|---|---|
INSERT INTO 分区 | 叠数 | 改 OVERWRITE |
用 etl_ts=now() 当主键一部分 | 每次重跑键变 | 主键用业务键 |
| 先读目标表再 merge 却漏删 | 旧行残留 | 按分区全量替换或明确 delete+insert |
| 依赖「第一次全量、以后增量」无状态表 | 重跑丢上下文 | 状态落到表,或分区纯函数重算 |
| 回刷跳过 DQ | 脏数灌下游 | 回刷同样走 P0 出门禁 |
sql
-- 幂等去重示意:ODS 可能重复推送时,DWD 按业务主键取最新
INSERT OVERWRITE TABLE dwd_ad_spend_detail_di
PARTITION (dt = '${biz_date}')
SELECT
bill_id, channel_id, play_id, creative_id,
cost_amount, event_ts, account_tz, src_system,
is_valid, filter_reason, etl_ts
FROM (
SELECT
*,
ROW_NUMBER() OVER (PARTITION BY bill_id ORDER BY event_ts DESC, etl_ts DESC) AS rn
FROM ods_ad_spend_di
WHERE dt = '${biz_date}'
) t
WHERE rn = 1;7. 回刷执行记录(最小字段)
sql
CREATE TABLE IF NOT EXISTS ops_backfill_log (
backfill_id STRING,
table_name STRING,
dt_from STRING,
dt_to STRING,
operator STRING,
start_ts TIMESTAMP,
end_ts TIMESTAMP,
status STRING COMMENT 'RUNNING/SUCCESS/FAILED',
row_cnt_before BIGINT,
row_cnt_after BIGINT,
cost_before DECIMAL(18,4),
cost_after DECIMAL(18,4),
note STRING
)
COMMENT '分区回刷审计(示意)';常见坑
INSERT INTO当重跑
第二次起必然叠数。日批分区默认OVERWRITE。只重跑中间层
ODS 变了只跑 DWD,DWS/ADS 仍旧,对账「修好了又坏」。级联清单要全。大范围动态分区一次回刷
写出海量空分区/小文件。按 dt 串行或小批量,并合并输出。用当前维表解释历史事实
渠道改名/改结算主体后,历史消耗口径被改写。回刷要对齐事件日维表。外部表漏维护分区元数据
数据删了分区还在,或反之。DROP PARTITION与路径删除成对,写后校验SHOW PARTITIONS。回刷跳过质量出门禁
「赶时间先出数」把重复主键灌进 DWS。回刷与日常共用 P0 规则(见 质量闭环)。
如何验收(可量化)
- [ ] 目标表写入语句为分区
OVERWRITE(或等价先删后写),代码评审可核对 - [ ] 故意连续重跑两次同
dt:行数、金额、主键集合一致 - [ ] 回刷级联列表完整;下游同分区更新时间 ≥ 上游
- [ ] P0:主键唯一、核心度量非空、非空分区
- [ ] 对账 ADS 与源差异可解释;
ops_backfill_log有成功记录
sql
-- 验收 1:同分区连跑两次的快照对比(将 _run1 / _run2 换成两次导出)
SELECT
'row_cnt' AS metric,
r1.cnt AS run1,
r2.cnt AS run2,
r1.cnt - r2.cnt AS diff
FROM (SELECT COUNT(*) AS cnt FROM dwd_ad_spend_detail_di WHERE dt = '${biz_date}') r1
CROSS JOIN (SELECT COUNT(*) AS cnt FROM dwd_ad_spend_detail_di_run2 WHERE dt = '${biz_date}') r2
UNION ALL
SELECT
'cost_sum',
r1.s, r2.s, r1.s - r2.s
FROM (
SELECT SUM(cost_amount) AS s FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}' AND is_valid = true
) r1
CROSS JOIN (
SELECT SUM(cost_amount) AS s FROM dwd_ad_spend_detail_di_run2
WHERE dt = '${biz_date}' AND is_valid = true
) r2;
-- 期望:diff = 0
-- 验收 2:主键唯一
SELECT bill_id, COUNT(*) AS cnt
FROM dwd_ad_spend_detail_di
WHERE dt = '${biz_date}'
GROUP BY bill_id
HAVING COUNT(*) > 1
LIMIT 20;
-- 期望:0 行bash
# 验收 3:连续提交两次同一分区任务(示意)
# airbox/dolphinscheduler/crontab:对 dt=2025-01-10 触发两次 dwd 任务
# 比对两次完成后的 count / sum;并确认下游 dws 也被触发且完成时间更晚相关面试题
占位:数仓任务如何保证幂等;补数/回刷怎么做;INSERT OVERWRITE 与 INTO 的区别。
改写说明:写入与回刷契约参考 Wood 原创数仓/面试实践,已改写成可执行清单;未包含未公开公司数据与内部调度系统名。