必须先有唯一约束(unique或primary key),否则on conflict直接报错;postgresql只认显式约束,不认业务逻辑唯一性,缺失需用alter table add constraint补全,复合场景须建对应复合唯一约束。

必须先有唯一约束(UNIQUE 或 PRIMARY KEY),否则 ON CONFLICT 直接报错,不是语法写错了,是表结构没准备好。
为什么 ON CONFLICT (id) 报 “no unique or exclusion constraint”?
PostgreSQL 不会凭空判断“哪列该唯一”,它只认显式定义的约束。哪怕业务上 id 绝对不重复,只要没加主键或唯一索引,ON CONFLICT (id) 就无效。
- 检查约束是否存在:
\d table_name(psql 命令)或查pg_constraint表 - 补约束示例:
ALTER TABLE users ADD CONSTRAINT users_id_key UNIQUE (id) - 复合场景(如
(user_id, event_type))必须建复合唯一索引,两个单独的UNIQUE约束不能替代
ON CONFLICT 的三种冲突目标写法怎么选?
写法不同,行为和可维护性差异明显。PostgreSQL 16 全支持,但推荐优先用约束名——最明确、最不怕重构。
-
ON CONFLICT (email):依赖列上有默认命名的唯一约束(如users_email_key),适合单列且无歧义场景 -
ON CONFLICT (user_id, event_type):要求该组合字段存在**一个**复合唯一约束,不能靠两个独立约束拼凑 -
ON CONFLICT ON CONSTRAINT users_email_key:直接引用约束名,表里有多个唯一约束时必用,避免 PostgreSQL 推断错误
DO UPDATE 里怎么安全更新新值?
必须用 EXCLUDED,它是 PostgreSQL 内部临时表,代表“本想插入但被拦下的那行”。直接写字段值或旧字段名会丢数据、硬编码、不可复用。
- 错误写法:
SET name = 'John'(写死)、SET name = name(没变)、SET name = users.name(语法错) - 正确写法:
SET name = EXCLUDED.name, updated_at = NOW() - 条件更新(PostgreSQL 16 支持):
DO UPDATE SET count = EXCLUDED.count + user_events.count WHERE user_events.status = 'active',注意WHERE是在**原记录**上判断,不是在EXCLUDED上
高并发下 ON CONFLICT 的真实锁行为和边界
它不是“查+插/更”的原子封装,而是带 speculative insert 的底层原子操作,但仍有关键边界要盯紧。
- 冲突路径上会加锁:比如
ON CONFLICT (id)会锁住对应id的索引项,其他事务对该id的写操作会被阻塞,读不受影响 -
EXCLUDED只能访问本次INSERT的值,不能跨行、不能引用子查询或 CTE - 如果
DO UPDATE触发了触发器,触发器看到的是更新后的整行,不是原始EXCLUDED行 —— 这点容易在审计日志或时间戳逻辑里翻车
最容易被忽略的是:约束必须存在且匹配,EXCLUDED 不是变量而是虚拟表,以及 WHERE 条件作用对象是原记录而非新值。这三个点一旦出错,语句表面能跑通,结果却完全不符合预期。










