PostgreSQL的ON CONFLICT报错“no unique or exclusion constraint matching”根本原因是冲突目标列缺乏显式唯一约束或主键,而非语法错误;必须存在UNIQUE/PRIMARY KEY约束,普通索引或条件索引无效,且多列冲突需严格匹配索引字段顺序与定义。

PostgreSQL 的 ON CONFLICT 是唯一能原子、安全实现 UPSERT 的方式;用“先查再插/更”或触发器模拟,高并发下必然出错。
为什么 ON CONFLICT 会报 “no unique or exclusion constraint matching”
这个错误不是语法写错了,而是 PostgreSQL 明确拒绝执行——它发现你指定的冲突目标(比如 (email))背后没有对应的唯一约束或主键索引。
- 唯一约束必须显式创建,
UNIQUE或PRIMARY KEY才行;普通INDEX、NOT NULL、甚至UNIQUE INDEX WHERE active(带条件的)都不被接受 - 验证方法:在 psql 中运行
\d 表名,确认目标列旁标有UNIQUE或PK - 多列组合冲突时,必须严格按索引定义顺序写,比如索引是
ON things(a, b),就不能写成ON CONFLICT (b, a)
ON CONFLICT (col) 和 ON CONFLICT ON CONSTRAINT name 哪个更可靠
后者更可靠,尤其当表有多个唯一约束时。前者依赖 PostgreSQL 推断匹配哪个约束,容易在表达式索引或复合约束场景下失效。
- 查约束名用:
SELECT conname FROM pg_constraint WHERE conrelid = 'users'::regclass AND contype = 'u'; - 写法示例:
ON CONFLICT ON CONSTRAINT users_email_key DO UPDATE SET name = EXCLUDED.name; - 联合唯一约束也一样:
CREATE UNIQUE INDEX idx_order_user ON orders(user_id, order_date);→ 必须写ON CONFLICT (user_id, order_date)或对应约束名
DO UPDATE SET 里怎么避免覆盖已有非空值
EXCLUDED 是伪表,代表本次 INSERT 中那行数据;它字段值可能为 NULL,直接赋值会清空原值。得靠 COALESCE 或 CASE 控制更新逻辑。
- 只在新值非 NULL 时更新:
email = COALESCE(EXCLUDED.email, users.email) - 只在新值非空字符串时更新:
email = CASE WHEN LENGTH(TRIM(EXCLUDED.email)) > 0 THEN EXCLUDED.email ELSE users.email END - 禁止在
SET中用子查询、聚合函数(如SELECT COUNT(*)),这是硬性限制,不是写法问题
PostgreSQL 19 新增的 DO SELECT 实际怎么用
ON CONFLICT DO SELECT 不是独立语句,必须和 RETURNING 配合;它的作用是:插入成功时返回新行,冲突时返回已有行——省掉一次额外 SELECT。
- 写法必须带
RETURNING *或具体字段:INSERT INTO tab VALUES (1, 2) ON CONFLICT (id) DO SELECT RETURNING *; - 返回结果结构和
INSERT ... RETURNING完全一致,应用层可统一处理 - 注意:它不改变数据,只是“返回”,所以不能替代
DO UPDATE;想更新+返回,还是得用DO UPDATE ... RETURNING
最常被忽略的是:DO UPDATE 中的 WHERE 条件作用于原行(users.status = 'active'),不是新值;一旦条件不成立,整条 UPSERT 就静默跳过更新,既没插也没更,还返回成功——这种“假成功”最难排查。

















