PostgreSQL 15 的 MERGE 是原子性原生命令,要求 ON 条件字段必须有 UNIQUE/PRIMARY KEY 约束,UPDATE 中旧值用目标表别名、新值用 EXCLUDED,不支持 RETURNING、视图或外部表,WHEN 子句按顺序短路匹配。

PostgreSQL 15 的 MERGE 是真正可用的原生命令,不是语法糖或宏展开——它能在一个原子事务里完成匹配更新、不匹配插入,甚至条件删除,但必须严格满足约束和引用规则,否则直接报错。
ON 条件只能基于唯一约束,不能用普通索引
PostgreSQL 要求 ON 子句中的匹配字段必须落在目标表的 UNIQUE 或 PRIMARY KEY 约束上。比如你写 ON t.username = s.un,那 username 字段必须有 UNIQUE 约束,光建普通 B-tree 索引没用。
- 错误示范:
CREATE INDEX idx_user ON users(username);→ 后续MERGE仍会报ERROR: MERGE is not supported with non-unique join condition - 正确做法:显式加约束,如
ALTER TABLE users ADD CONSTRAINT users_username_uniq UNIQUE (username); - 更稳妥的方式是用约束名写
ON CONFLICT ON CONSTRAINT users_username_uniq风格的写法,避免索引重命名后失效
DO UPDATE 中必须用 EXCLUDED 和目标表别名区分新旧值
MERGE 的 WHEN MATCHED THEN UPDATE 里不能直接写 t.name 访问当前行旧值,也不能写 s.name 当新值——源别名(s)在 UPDATE 分支中不可见。
- 旧值要通过目标表别名访问,例如
t.last_modified - 新值统一走
EXCLUDED伪表,例如EXCLUDED.name、EXCLUDED.score - 如果漏写目标表别名(如只写
last_modified),PostgreSQL 会报column "last_modified" does not exist - 示例片段:
WHEN MATCHED THEN UPDATE SET score = EXCLUDED.score + t.score, updated_at = CURRENT_TIMESTAMP
不支持 RETURNING,也不支持可更新视图或外部表作为目标
这是硬性限制,不是版本问题。哪怕你只是想拿到刚插入或更新的 id,MERGE 语句末尾加 RETURNING * 会直接被拒绝。
- 替代方案:拆成两步,先
MERGE,再用SELECT查刚影响的行(需靠业务逻辑或时间戳/序列辅助定位) - 目标表不能是
VIEW(即使定义为WITH CHECK OPTION),也不能是postgres_fdw或file_fdw创建的外部表 - 触发器可以工作,但
transition table(如REFERENCING NEW TABLE AS new_rows)仅对INSERT/UPDATE/DELETE生效,MERGE不提供过渡表支持
WHEN 子句按顺序匹配,第一个为真就执行,不会继续判断
MERGE 的 WHEN 列表不是“全部评估”,而是类似 if-else if-else 的短路逻辑。这个行为容易被忽略,尤其在写多个 WHEN MATCHED AND ... 时。
- 比如你写:
WHEN MATCHED AND t.status = 'active' THEN UPDATE ...,再写WHEN MATCHED THEN DELETE ...,那所有status != 'active'的匹配行都会被删掉,而不是跳过 - 若想保留未命中条件的匹配行,最后一个
WHEN MATCHED应该是DO NOTHING - 没有
ELSE关键字,靠顺序和显式条件控制流向
最常踩的坑其实是把 MERGE 当成万能 UPSERT 替代品,却忘了它对约束的强依赖和字段引用的特殊语法;一旦 ON 条件不满足唯一性,或者 EXCLUDED 写成 s. 前缀,语句就根本跑不起来——它不像 INSERT ... ON CONFLICT 那样容错高。

















