编程 ON CONFLICT 的冲突目标怎么推断:从 no unique or exclusion constraint 报错到 SQLite 的两义

2026-10-06 00:03:24

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 是删+插)。

参考文档:

推荐文章

程序员茄子在线接单