必须先有唯一约束,否则ON CONFLICT直接报错;PostgreSQL仅响应真实存在的UNIQUE或PRIMARY KEY约束,不识别普通索引或业务逻辑唯一性,需用\d检查并用ALTER TABLE添加对应约束。

必须先有唯一约束,否则 ON CONFLICT 直接报错,不是语法问题,是前提条件。
为什么 ON CONFLICT (id) 报 “no unique or exclusion constraint”?
PostgreSQL 不会根据你写的列名“猜”哪里该唯一;它只响应真实存在的 UNIQUE 或 PRIMARY KEY 约束。普通索引、NOT NULL、业务逻辑上的“应该唯一”,全都不算数。
- 检查约束是否存在:
\d table_name(psql 命令),或查pg_constraint表 - 缺失时补上:
ALTER TABLE users ADD CONSTRAINT users_id_key UNIQUE (id); - 多列冲突(如
(user_id, action_type))必须建复合唯一索引,两个单列唯一约束无效
ON CONFLICT 写法选哪个?列名、组合列还是约束名?
三者都支持,但行为和可维护性差异明显:
-
ON CONFLICT (email):依赖列上有默认命名的唯一约束(如users_email_key),适合简单单列场景 -
ON CONFLICT (user_id, event_type):要求存在完全匹配字段顺序与类型的复合唯一约束;写错顺序或漏字段会失败 -
ON CONFLICT ON CONSTRAINT users_email_key:最安全,明确绑定到具体约束,尤其当表有多个唯一约束时(比如 email + phone 都唯一)
DO UPDATE SET 里怎么引用新值?
必须用 EXCLUDED —— 它是 PostgreSQL 内部临时表,代表“本想插入但被拦下的那行”。直接写字段名或变量名会出错或覆盖错值。
- 错误写法:
SET name = 'new_name'(硬编码,丢失动态值)或SET name = name(等于没更新) - 正确写法:
SET name = EXCLUDED.name, updated_at = NOW() - 条件更新(PostgreSQL 16 支持):
WHERE user_events.status = 'active',注意WHERE是在“老记录”上判断,不是在EXCLUDED上
高并发下容易忽略的锁与原子性边界
ON CONFLICT 是原子操作,但不是无锁的。它用 speculative insert 实现高效,代价是特定路径加锁:
- 冲突发生时,会在对应索引项上加锁(例如
ON CONFLICT (id)锁住该id的 B-tree 页项),其他事务对该id的 UPSERT 会被阻塞 -
EXCLUDED只能访问本次INSERT的值,不能跨行、不能引用子查询、不能调用函数返回多行 - 如果
DO UPDATE触发了触发器,触发器看到的是更新后的最终行,不是中间态










