代表性面试主题

数据工程面试:PostgreSQL 18 的 MERGE 为什么会因重复源行失败?

数据困难
Offer.cc 编辑团队发布 更新

题干

每日客户快照通过 MERGE 同步到维表,但同一个 customer_id 在源数据出现两行。如何解释失败、修复源数据、设计 RETURNING 审计并保证重跑安全?

题干与适用场景

每日客户快照表 staging_customer 通过 MERGE 同步到 customer_dim。某次批次中同一个 customer_id 出现两行:一行来自 CRM,一行来自人工修订。目标表没有重复键,但语句仍抛出 cardinality violation。请说明 PostgreSQL 18 如何产生候选变更行、为什么同一目标行不能被多条源行再次修改,以及如何用 RETURNING merge_action() 产出可审计结果。

面试官考察点

  • 能否区分源表重复、目标表重复和 ON 条件过宽三类问题。
  • 能否说明 MERGE 对每个候选变更行只执行第一个满足条件的 WHEN 分支。
  • 能否在去重、拒绝批次和保留最新修订之间做出可解释选择。
  • 能否把 RETURNING 结果用于审计,而不是把它当成事务提交日志。
  • 能否处理并发、重跑、权限和数据质量告警。

回答前需要澄清的问题

  • customer_id 是否是业务唯一键,还是必须与租户、有效日期组成复合键?
  • 两条源记录是否有可信的版本号、事件时间或修订优先级?答案决定去重规则。
  • 失败批次是全量拒绝,还是允许先写入已确定的行?这会改变事务与重放策略。
  • 审计需要记录旧值、新值、源字段和执行动作,还是只要行数统计?
  • 源数据是否可能跨批次迟到,目标是否允许软删除?

30 秒回答框架

“我先确认 ON 条件能把每个目标键匹配到至多一条源行。PostgreSQL MERGE 会先生成候选变更行,再按 WHEN 顺序对每行执行一个动作;同一目标行被多条源行命中会触发基数错误,事务整体失败。修复时我会用版本或事件时间在源侧确定性去重,无法判定就拒绝批次。RETURNING merge_action() 记录插入、更新和删除的结果,另外保留批次状态、唯一约束和重跑键。”

分步骤深入解答

先证明匹配基数

先用与 ON 条件完全相同的键做质量查询,找出一个目标键对应多条源行的情况。不要只对源表做 DISTINCT,因为不同属性的两行即使键相同也可能代表冲突事实。若业务键是 (tenant_id, customer_id),就必须在 MERGE 和质量查询中同时使用两列。

让去重规则确定性

用版本号优先;没有版本号时,使用事件时间、可靠来源等级和稳定的 tie-breaker。窗口函数可以选出唯一胜者:

sql
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 做确定性排序。无法判断的冲突进入隔离表并让批次失败。MERGEWHEN 分支按顺序只执行一个动作,我会让高版本保护条件排在前面。PostgreSQL 18 的 RETURNING merge_action() 输出每行的 insert、update 或 delete,以及旧新值,审计表和批次控制表则放在同一事务里,保证重跑、告警和回放有依据。”

常见错误

  • 错误表现: 对源表直接 DISTINCT失败原因: 不同事实可能被错误合并 → 修正方法: 用版本和业务优先级定义唯一胜者,冲突进入隔离区。
  • 错误表现: 以为 MERGE 会随机选择一条源行 → 失败原因: 同一目标行的多重修改会触发基数错误 → 修正方法: 在源侧证明一对一匹配。
  • 错误表现:RETURNING 行数当作批次成功标记 → 失败原因: 没有记录零变更、失败和重跑状态 → 修正方法: 使用独立批次控制表与事务状态。
  • 错误表现: 只测试单线程 → 失败原因: 并发隔离、迟到数据和重放未被验证 → 修正方法: 做并发、重跑和迟到批次测试。

追问及应对

如果同一键的两条源行一条是删除标记,怎么办?

先把删除与更新纳入同一版本排序,只有最高版本胜出。若删除没有可比较的版本,就隔离冲突,不让一次批次隐式决定客户状态。

RETURNING 能否记录没有匹配到的源行?

它返回实际执行 INSERTUPDATEDELETE 的变更结果,不能替代源侧的“未匹配”质量报告。另跑一份反连接统计,或把候选与动作写入预审表,再提交 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 答案要把源数据基数、分支顺序和审计输出连成一条可重放的数据契约。

公开来源

同类题目