ON CONFLICT 的冲突目标怎么推断:从 no unique or exclusion constraint 报错到 SQLite 的两义
INSERT 撞上唯一约束或排除约束时会直接抛错。ON CONFLICT 就是给这条语句指定一个替代动作,把错误变成另一种结果,也就是 UPSERT(UPDATE or INSERT)。DO NOTHING 只是跳过该行插入;DO UPDATE 更新与拟插入行冲突的现有行。
ON CONFLICT DO UPDATE 保证结果要么是 INSERT、要么是 UPDATE,高并发下也是二者之一(前提是没有其他独立错误)。
语法
INSERT INTO table_name [ AS alias ] [ ( column_name [, ...] ) ]
{ DEFAULT VALUES | VALUES (...) | query }
[ ON CONFLICT [ conflict_target ] conflict_action ]
[ RETURNING ... ]
conflict_target:
( { index_column_name | ( index_expression ) } [ COLLATE collation ] [ opclass ] [, ...] ) [ WHERE index_predicate ]
ON CONSTRAINT constraint_name
conflict_action:
DO NOTHING
DO UPDATE SET { column_name = { expression | DEFAULT } | ( ... ) = ( ... ) } [, ...] [ WHERE condition ]
冲突目标与索引推断
conflict_target 可以做唯一索引推断:由列/表达式加可选的 index_predicate 组成。所有唯一索引,只要在不考虑顺序的情况下完全包含 conflict_target 指定的列/表达式,就会被推断为仲裁索引;非部分唯一索引也会被推断。推断失败直接报错。
两种 action 对目标的要求不一样:
DO NOTHING:conflict_target可选,省略时处理所有可用约束与唯一索引的冲突。DO UPDATE:必须提供conflict_target。
通常更推荐写成 ON CONFLICT ON CONSTRAINT constraint_name,名字写死,不依赖推断。
INSERT 带 ON CONFLICT DO UPDATE 是「确定性」语句:不允许对任何单个现有行影响多于一次,否则报 cardinality violation(基数违背)。也就是说,拟插入的行在受仲裁索引约束的属性上不应彼此重复。
标准 SQL 里 INSERT ... ON CONFLICT 是扩展,标准替代是 MERGE。
excluded 伪表
DO UPDATE 的 SET 和 WHERE 子句既可以用表名(或别名)访问现有行,也可以用特殊表 excluded 访问拟插入的行。
- 读取目标表中对应
excluded的列需要 SELECT 权限。 - 常见错误写法是硬编码
SET name='new_name',正确写法是SET name=EXCLUDED.name, updated_at=NOW()。 EXCLUDED只能访问本次 INSERT 的值,不能跨行或引用子查询结果。DO UPDATE触发的触发器看到的是更新后的行,不是原始 EXCLUDED 行。- 批量插入多行时每行独立判断冲突,
EXCLUDED对每行都有效,行间互不影响。
常见报错:no unique or exclusion constraint matching the ON CONFLICT specification
这是设计前提问题,不是语法错误。PostgreSQL 不允许凭空指定哪列算冲突,只认已存在的 UNIQUE / PRIMARY KEY / 排除约束(EXCLUDE),普通索引无效。
ON CONFLICT (id) 能生效的前提是 id 列上已有主键或唯一索引。业务上「应该唯一」但没加约束,就会撞上这个报错。
检查约束是否存在:
\d table_name
或者查 pg_constraint。补约束:
ALTER TABLE users ADD CONSTRAINT users_id_key UNIQUE (id);
复合去重(比如按 (user_id, action_type))必须建复合唯一索引,不能只靠两个单独的唯一约束。
三种写法:
ON CONFLICT (email)
ON CONFLICT (user_id, event_type) -- 要求该组合列存在复合唯一约束
ON CONFLICT ON CONSTRAINT users_email_key
第三种最明确,表上有多个唯一约束时不会产生歧义。
锁与性能边界
带唯一索引的表可能被并发会话阻塞——当并发操作锁定或修改与待插入唯一索引值匹配的行时。ON CONFLICT 不阻塞其他事务读取,但冲突路径上会对相应索引项加锁。
性能来源不是语法本身,而是底层索引能快速定位冲突(speculative insertion)。
自增列要留意:SERIAL 主键即使走 DO UPDATE,序列值依然会被消耗,因为需要先生成可能的值。不想浪费 ID,可以用其他唯一键作冲突目标。
另外两点:视图不可直接 ON CONFLICT,只能在基表上执行;权限上需要 INSERT,用 DO UPDATE 还需要 UPDATE。
SQLite 里 ON CONFLICT 有两义
第一义是 CREATE TABLE 约束上的 ON CONFLICT 子句(非标准扩展),3.0.0 之前就有。冲突算法有 ROLLBACK、ABORT、FAIL、IGNORE、REPLACE,默认 ABORT,适用于 UNIQUE / NOT NULL / CHECK / PRIMARY KEY,不适用于 FOREIGN KEY。在 INSERT/UPDATE 里关键字 ON CONFLICT 被替换为 OR:
INSERT OR IGNORE INTO t ... -- 而不是 INSERT ON CONFLICT IGNORE
第二义是 UPSERT 里的 ON CONFLICT,3.24.0(2018-06-04)加入,遵循 PostgreSQL 语法并泛化。3.35.0(2021-03-12)进一步泛化:允许多个 ON CONFLICT 子句,也允许无冲突目标的 DO UPDATE。
ON CONFLICT 子句按顺序检查,每行只运行第一个匹配冲突目标的子句,触发后该行后续子句被跳过。最后一个 ON CONFLICT 子句可以省略冲突目标,用来捕获前面未匹配的唯一性约束失败。
UPSERT 只针对唯一性约束(UNIQUE / PRIMARY KEY / 唯一索引),不会介入 NOT NULL、CHECK、外键失败,也不介入触发器实现的约束。DO UPDATE 的冲突解决算法恒为 ABORT,DO UPDATE 里再遇约束违规,整个语句回滚。
INSERT OR REPLACE 的问题在于它是删除旧行再插入新行,会触发 DELETE 触发器,自增 id 可能改变。
解析歧义:INSERT ... SELECT ... ON CONFLICT(x) 时解析器无法判断 ON 是引入 UPSERT 还是 JOIN 的 ON。解决方式是 SELECT 语句始终带 WHERE,哪怕是:
INSERT INTO t SELECT * FROM s WHERE true ON CONFLICT (id) DO NOTHING;
ON CONFLICT IGNORE 的坑:被忽略的行永远不会插入,但程序可能以为写成功,必须检查受影响行数。excluded.column 引用的是尝试插入的新值。
与 MySQL 对照
MySQL 用 INSERT ... ON DUPLICATE KEY UPDATE,语法不同,不支持部分索引冲突。SQLite 支持 INSERT OR REPLACE / INSERT OR IGNORE,行为与 PostgreSQL 的 ON CONFLICT 不完全一致(REPLACE 是删+插)。
参考文档: