数据工程面试:PostgreSQL 18 的 MERGE 为什么会因重复源行失败?
题干与适用场景
每日客户快照表 stagingcustomer 通过 MERGE 同步到 customerdim。某次批次中同一个 customerid 出现两行:一行来自 CRM,一行来自人工修订。目标表没有重复键,但语句仍抛出 cardinality violation。请说明 PostgreSQL 18 如何产生候选变更行、为什么同一目标行不能被多条源行再次修改,以及如何用 RETURNING mergeaction() 产出可审计结果。
面试官考察点
- 能否区分源表重复、目标表重复和
ON条件过宽三类问题。 - 能否说明
MERGE对每个候选变更行只执行第一个满足条件的WHEN分支。 - 能否在去重、拒绝批次和保留最新修订之间做出可解释选择。
- 能否把
RETURNING结果用于审计,而不是把它当成事务提交日志。 - 能否处理并发、重跑、权限和数据质量告警。
回答前需要澄清的问题
customer_id是否是业务唯一键,还是必须与租户、有效日期组成复合键?- 两条源记录是否有可信的版本号、事件时间或修订优先级?答案决定去重规则。
- 失败批次是全量拒绝,还是允许先写入已确定的行?这会改变事务与重放策略。
- 审计需要记录旧值、新值、源字段和执行动作,还是只要行数统计?
- 源数据是否可能跨批次迟到,目标是否允许软删除?
30 秒回答框架
“我先确认 ON 条件能把每个目标键匹配到至多一条源行。PostgreSQL MERGE 会先生成候选变更行,再按 WHEN 顺序对每行执行一个动作;同一目标行被多条源行命中会触发基数错误,事务整体失败。修复时我会用版本或事件时间在源侧确定性去重,无法判定就拒绝批次。RETURNING merge_action() 记录插入、更新和删除的结果,另外保留批次状态、唯一约束和重跑键。”
分步骤深入解答
先证明匹配基数
先用与 ON 条件完全相同的键做质量查询,找出一个目标键对应多条源行的情况。不要只对源表做 DISTINCT,因为不同属性的两行即使键相同也可能代表冲突事实。若业务键是 (tenantid, customerid),就必须在 MERGE 和质量查询中同时使用两列。
让去重规则确定性
用版本号优先;没有版本号时,使用事件时间、可靠来源等级和稳定的 tie-breaker。窗口函数可以选出唯一胜者:
WITH ranked AS (
SELECT s.*, row_number() OVER (
PARTITION BY tenant_id, customer_id
ORDER BY version DESC, event_at DESC, source_priority DESC, ingest_id DESC
) AS rn
FROM staging_customer AS s
)
SELECT * FROM ranked WHERE rn = 1;如果两个来源无法证明谁更新,应把冲突写入 quarantine 表并拒绝该批次,不要依赖数据库任意挑选一行。
设计 WHEN 与审计输出
WHEN 条件按书写顺序判断,首个为真的分支执行。先写保护性条件,例如版本更高才更新;再处理 NOT MATCHED 插入和必要的 NOT MATCHED BY SOURCE 清理。PostgreSQL 18 的 RETURNING 可以返回源列、目标旧值、目标新值和 merge_action(),但它只代表本语句实际改变的行,不替代批次控制表。
处理事务与并发
把单批 MERGE、审计落表和批次状态更新放在同一事务。为输入批次设置幂等键,重跑时先识别已成功批次。并发执行时遵循数据库隔离规则;源查询应保证每个目标行最多一个候选,避免把重复源行留给运行时才发现。
高质量示范回答
“这个错误不是目标表有重复键就能解释的;关键是 ON 条件把同一目标行连到了多条源行。我的第一步是用同样的租户和客户键检查源基数,再按版本、事件时间、来源优先级和摄入 ID 做确定性排序。无法判断的冲突进入隔离表并让批次失败。MERGE 的 WHEN 分支按顺序只执行一个动作,我会让高版本保护条件排在前面。PostgreSQL 18 的 RETURNING merge_action() 输出每行的 insert、update 或 delete,以及旧新值,审计表和批次控制表则放在同一事务里,保证重跑、告警和回放有依据。”
常见错误
- 错误表现: 对源表直接
DISTINCT→ 失败原因: 不同事实可能被错误合并 → 修正方法: 用版本和业务优先级定义唯一胜者,冲突进入隔离区。 - 错误表现: 以为
MERGE会随机选择一条源行 → 失败原因: 同一目标行的多重修改会触发基数错误 → 修正方法: 在源侧证明一对一匹配。 - 错误表现: 用
RETURNING行数当作批次成功标记 → 失败原因: 没有记录零变更、失败和重跑状态 → 修正方法: 使用独立批次控制表与事务状态。 - 错误表现: 只测试单线程 → 失败原因: 并发隔离、迟到数据和重放未被验证 → 修正方法: 做并发、重跑和迟到批次测试。
追问及应对
如果同一键的两条源行一条是删除标记,怎么办?
先把删除与更新纳入同一版本排序,只有最高版本胜出。若删除没有可比较的版本,就隔离冲突,不让一次批次隐式决定客户状态。
RETURNING 能否记录没有匹配到的源行?
它返回实际执行 INSERT、UPDATE 或 DELETE 的变更结果,不能替代源侧的“未匹配”质量报告。另跑一份反连接统计,或把候选与动作写入预审表,再提交 MERGE。
为什么不直接用 INSERT ... ON CONFLICT?
如果需求只有按唯一键插入或更新,ON CONFLICT 可能更简单;需要按来源匹配、删除未出现行或多分支条件时才考虑 MERGE。两者的并发和权限语义不能混用假设。
目标表有触发器时要额外检查什么?
确认触发器不会改变匹配键或让同一目标行在语句内再次成为候选。审计应区分 MERGE 的动作和触发器产生的副作用,并在集成环境验证回滚。
参考资料
- PostgreSQL Documentation 18:MERGE
- PostgreSQL Documentation 18:Merge Support Functions
- Greg Low:SQL Interview: 35 T-SQL Merge Statement Clauses
- Simplyblock:PostgreSQL MERGE tutorial
面试作答要点
先证明 ON 条件的一对一基数,再解释 WHEN 顺序和 cardinality violation,最后给出确定性去重、事务控制、RETURNING merge_action() 审计与重跑策略。
一句话总结
高质量的 MERGE 答案要把源数据基数、分支顺序和审计输出连成一条可重放的数据契约。