PostgreSQL 15 中 ON CONFLICT 性能取决于索引、锁控制与事务粒度,必须建唯一约束(如 UNIQUE(email)),推荐用 ON CONFLICT ON CONSTRAINT 精确指定;批量插入优于单行,DO UPDATE 避免子查询以防热点锁。

ON CONFLICT 在 PostgreSQL 15 中依然不能“自动加速”,性能提升完全取决于你是否让冲突检测走索引、是否避免锁升级、是否控制事务粒度——这些和 15 版本本身关系不大,但 15 的小版本修复(如 15.4+)确实堵住了几个并发场景下的锁误升级漏洞。
必须先建唯一约束,否则 ON CONFLICT 直接报错
PostgreSQL 不会凭空判断“哪列该去重”。它只响应显式定义的 UNIQUE 或 PRIMARY KEY 约束。没约束,ON CONFLICT (email) 就会报:there is no unique or exclusion constraint matching the ON CONFLICT specification。
- 建约束是硬前提,不是可选项:比如要按邮箱去重,就得执行
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email) - 约束名不是随便起的,可用
\d users查看实际生成的名称(如users_email_key),别靠猜 - 复合唯一索引(如
(tenant_id, email))也合法,但写法必须匹配:ON CONFLICT (tenant_id, email)或ON CONFLICT ON CONSTRAINT users_tenant_email_key
用约束名写法,而不是列名写法
当表里有多个唯一约束时,仅写 ON CONFLICT (email) 可能被 PostgreSQL 推断到错误的约束上(比如它优先匹配了 (email, created_at) 这种组合索引),导致行为不符合预期,甚至在 15.0–15.3 中触发计划器歧义错误。
- 推荐始终使用
ON CONFLICT ON CONSTRAINT users_email_key,意图明确,不依赖推断 - 列名写法仅在单唯一约束、且无组合索引的简单表中安全;一旦加新约束,旧 SQL 可能突然失效
- 约束名可通过查询
information_schema.table_constraints获取,适合自动化脚本生成
批量插入 + 单次 ON CONFLICT 是吞吐关键
把 100 行数据拆成 100 条单行 INSERT ... ON CONFLICT,性能会比一条含 100 行 VALUES 的语句低 5–10 倍——因为每次都要重新解析、生成执行计划、获取锁、查索引。
- 正确姿势:
INSERT INTO users (id, name, email) VALUES (1,'a','a@x'),(2,'b','b@x'),(3,'c','c@x') ON CONFLICT ON CONSTRAINT users_email_key DO UPDATE SET name = EXCLUDED.name; - 所有行共享一次索引查找,冲突检测成本摊薄,锁持有时间更短
- 如果一批数据中混用不同冲突目标(例如有的按
id、有的按email),必须拆成多个语句,不能强行塞进一条
DO UPDATE 里别嵌套子查询或函数调用
看似无害的 version = (SELECT MAX(version)+1 FROM users WHERE id = EXCLUDED.id) 会在更新时引入额外的扫描、锁竞争,甚至阻塞其他并发 ON CONFLICT 操作。
-
EXCLUDED是轻量虚拟表,但一加SELECT就变重;改用原子表达式,如version = EXCLUDED.version + 1 - 避免
NOW()以外的时间函数(如CURRENT_TIMESTAMP AT TIME ZONE 'UTC'),部分小版本中可能触发计划器退化 - 条件更新(
WHERE子句)是安全的,比如DO UPDATE SET updated_at = NOW() WHERE users.updated_at ,它不增加锁范围
真正卡住性能的从来不是语法写得对不对,而是约束有没有、索引走没走、事务包没包住不该包的数据、以及 DO UPDATE 里那行看似无害的子查询——它在高并发下会悄悄变成热点锁源。










