on conflict报错“no unique or exclusion constraint”是因为缺少显式唯一约束或主键,而非语法错误;必须存在unique/primary key约束,普通索引无效,且多列冲突需严格匹配索引字段顺序与定义。

必须先有唯一约束,否则 ON CONFLICT 直接报错,不是语法问题,是设计前提。
为什么 ON CONFLICT (id) 会报 “no unique or exclusion constraint”?
PostgreSQL 不允许你凭空指定“哪列算冲突”,它只认已存在的唯一性约束(UNIQUE 或 PRIMARY KEY)或排除约束(EXCLUDE)。ON CONFLICT (id) 能生效,前提是 id 列上已有主键或唯一索引;如果只是普通字段,哪怕业务逻辑上“应该唯一”,也必须显式加约束。
- 检查约束是否存在:
\d table_name(psql 命令),或查pg_constraint表 - 缺失时补上:
ALTER TABLE users ADD CONSTRAINT users_id_key UNIQUE (id); - 复合场景(如按
(user_id, action_type)去重):必须建复合唯一索引,不能只靠两个单独的唯一约束
ON CONFLICT 的三种冲突目标写法及适用场景
写法不同,行为和兼容性有差异。PostgreSQL 16 中三者都支持,但推荐优先用约束名——最明确、最不易出错。
PostgreSQL 18.4 官方 Ubuntu 安装包现已发布,这是目前最新的稳定版本。推荐通过官方 APT 仓库安装:先执行 sudo apt update 更新索引,再运行 sudo apt install postgresql-18 即可完成部署。新版本引入了异步 I/O 子系统,在顺序扫描与 VACUUM 场景下性能提升显著,同时支持 UUID v7 原生生成函数与虚拟生成列。
-
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,而不是直接写变量名或旧值。这是 UPSERT 原子性的关键——EXCLUDED 是 PostgreSQL 内部临时表,代表“本想插入但被拦下的那行数据”。
- 错误写法:
SET name = 'new_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的索引项) -
EXCLUDED只能访问本次 INSERT 的值,不能跨行或引用子查询结果 - 如果
DO UPDATE部分触发触发器,那些触发器看到的是更新后的行,不是原始EXCLUDED行 - 批量插入多行时,每行独立判断冲突,
EXCLUDED对每一行都有效,但不同行之间不互相影响
真正容易被忽略的是:约束必须存在且被索引支撑,否则整个语句失败;而一旦约束存在,PostgreSQL 就能用索引快速定位冲突,这才是性能来源——不是语法本身快,是底层机制快。










