on conflict必须基于显式唯一约束(unique/primary key),否则报“no unique or exclusion constraint”;需用\d检查约束存在,do nothing静默跳过,do update触发更新与行锁。

ON CONFLICT 必须指定唯一约束目标
PostgreSQL 不会自动识别“重复”——它只响应明确的唯一约束冲突。如果你写 ON CONFLICT (id),但表上没有 UNIQUE 或 PRIMARY KEY 约束覆盖 id 列,就会报错:there is no unique or exclusion constraint matching the ON CONFLICT specification。常见错误是以为主键列天然“被识别”,其实必须显式建索引(哪怕主键已存在,也要确认约束名是否匹配)。用 \d table_name 查看输出里的 Indexes 部分,确保目标字段出现在 UNIQUE 或 PRIMARY KEY 行中。
DO NOTHING 和 DO UPDATE 的行为差异很实际
ON CONFLICT ... DO NOTHING 是静默跳过,row_count 返回 0;DO UPDATE 即使 SET 字段值和原值完全一样,也算一次更新,row_count 返回 2(1 行插入 + 1 行更新),且会触发行锁和 WAL 日志写入。容易踩的坑包括:
- 在
DO UPDATE中漏写WHERE条件,导致无谓更新(比如时间戳被重刷、计数器被重置) - 误以为
DO NOTHING能捕获所有冲突——它只响应唯一约束,对NOT NULL、外键失败等其他错误仍会抛异常 - 用
EXCLUDED.*引用新值时,字段名拼错(如写成EXCLUDED.user_name但源数据列叫username),导致语法错误或静默设为 NULL
批量 INSERT … SELECT + ON CONFLICT 更快,但不能混用子查询更新
直接 INSERT INTO t VALUES (...), (...) 做 upsert 效率低,尤其超过几百行后。推荐走 INSERT INTO t SELECT ... FROM ... ON CONFLICT 路径。但注意:DO UPDATE SET 里不能写子查询返回多行(例如 SET x = (SELECT y FROM s WHERE s.id = EXCLUDED.id)),否则报错 more than one row returned by a subquery used as an expression。正确做法是让子查询严格返回单值,或改用 EXCLUDED.x 直接引用本次插入值。
NULL 值会让唯一约束失效,需额外处理
PostgreSQL 的唯一约束默认允许多行 NULL(因为 NULL != NULL)。如果业务上认为 “email IS NULL 也应视为重复”,不能依赖普通唯一索引。解决方案有两个:
- 建表达式唯一索引:
CREATE UNIQUE INDEX idx_users_email_nvl ON users ((COALESCE(email, ''))); - 在
ON CONFLICT中用部分索引配合:CREATE UNIQUE INDEX idx_users_email_notnull ON users (email) WHERE email IS NOT NULL;,然后ON CONFLICT ON CONSTRAINT idx_users_email_notnull
没意识到这点时,常出现“明明插了两条 NULL 邮箱,却没报冲突”的困惑——不是语法错,是语义本就如此。










