on conflict do update 的本质是基于唯一约束触发的原子冲突处理机制,不执行先查后插/更的模拟逻辑,仅允许引用 excluded 伪表和目标表当前行,禁止子查询与聚合函数。

什么是 ON CONFLICT DO UPDATE 的本质行为
PostgreSQL 的 UPSERT 并不是原子级“先查再插或更新”的模拟逻辑,而是基于唯一约束(UNIQUE 或 PRIMARY KEY)触发冲突检测后,直接执行 DO UPDATE 分支。它不运行 WHERE 子句中的子查询,也不支持对非冲突键字段做条件判断 —— 所有更新逻辑必须写在 SET 后面,且只能引用 EXCLUDED 伪表和目标表当前行。
如何正确指定冲突目标:用 index_predicate 还是列名列表?
冲突目标必须明确指向一个**已存在的唯一索引或主键**。常见错误是直接写 (col_a, col_b) 却没确认该组合是否被索引覆盖 —— 这会报错 there is no unique or exclusion constraint matching the ON CONFLICT specification。
- 如果表有主键
id,直接写ON CONFLICT (id) - 如果靠唯一索引
CREATE UNIQUE INDEX idx_user_email ON users(email)冲突,就写ON CONFLICT (email) - 若索引带条件(如
WHERE status = 'active'),则不能用于ON CONFLICT—— PostgreSQL 不支持 partial index 作为冲突目标 - 多列唯一索引必须**按索引定义顺序**列出列,例如索引为
ON things(a, b),就不能写成ON CONFLICT (b, a)
如何只更新部分字段,且避免覆盖已有值?
DO UPDATE SET 默认会无条件覆盖,但可通过 EXCLUDED 表和 CASE 或 COALESCE 控制字段是否更新。比如只想更新 name 和 updated_at,但保留原 created_at 不变:
INSERT INTO users (id, name, email, created_at, updated_at) VALUES (1, 'Alice', 'alice@example.com', NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, updated_at = NOW(), email = COALESCE(EXCLUDED.email, users.email);
注意:COALESCE(EXCLUDED.email, users.email) 表示“仅当新值非 NULL 才更新”,而 EXCLUDED.email 是本次 INSERT 中该字段的值(可能为 NULL);若想“仅当新值非空字符串才更新”,得写 CASE WHEN LENGTH(TRIM(EXCLUDED.email)) > 0 THEN EXCLUDED.email ELSE users.email END。
为什么 DO UPDATE 里不能用子查询或聚合?
PostgreSQL 明确禁止在 ON CONFLICT ... DO UPDATE SET 的表达式中使用子查询、窗口函数或聚合函数(如 SELECT COUNT(*) FROM ... 或 MAX())。这是执行阶段限制:此时语句处于“冲突处理上下文”,尚未进入通用 DML 执行引擎。
- 报错示例:
SET counter = (SELECT counter + 1 FROM users WHERE id = EXCLUDED.id)→subquery in UPDATE target list is not allowed - 替代方案:改用
UPDATE ... FROM+INSERT ... ON CONFLICT两步,或提前在应用层计算好值 - 若需原子性自增(如计数器),可用
SET counter = users.counter + 1—— 直接引用目标表字段是允许的
真正容易被忽略的是:当多个并发 UPSERT 同一主键时,EXCLUDED 始终代表“本次 INSERT 的原始值”,不会受其他并发事务中间更新影响;但 users.xxx 引用的是当前最新已提交值 —— 所以 SET x = users.x + 1 是安全的,而 SET x = EXCLUDED.x + 1 可能丢失累加。











