唯一索引字段更新冲突需用特定语法处理:mysql用insert on duplicate key update,postgresql用insert ... on conflict do update,逻辑删除应设计复合唯一索引如(username, deleted_at)。

更新唯一索引字段时触发冲突,本质是业务想“改值”却撞上了别人已占的坑——不能靠绕开或忽略,得明确告诉数据库:“如果这个新值已被占用,请按我指定的方式处理”。
UPDATE 语句直接改唯一字段必然报错
MySQL/PostgreSQL 都不允许 UPDATE 某列使其值变成已存在的唯一值,哪怕目标行本身没变。错误不是“你不能改”,而是“你改完后整表不满足唯一性”。
- MySQL 报
ERROR 1062 (23000): Duplicate entry 'xxx' for key 'uk_xxx' - PostgreSQL 报
ERROR: duplicate key value violates unique constraint "uk_xxx" - SQL Server 报错误号
2627或2601 - DB2 报
SQLCODE = -803
此时不能用普通 UPDATE 硬上,必须换策略。
MySQL:用 INSERT ON DUPLICATE KEY UPDATE 替代 UPDATE
这不是语法糖,是唯一能原子化完成“尝试更新唯一键值 + 冲突时兜底”的方式。前提是目标字段有 PRIMARY KEY 或 UNIQUE 约束。
- 把你要改的那行“重插一遍”:用
INSERT INTO ... VALUES (...)带上新值 - 在
ON DUPLICATE KEY UPDATE中写明:冲突时更新哪些非唯一字段(比如updated_at = NOW()),或保留原值(col = col) - 注意:
VALUES(col)引用的是本次INSERT的原始值,不是当前行旧值 - 自增 ID 仍会跳号——这是该语法固有行为,别指望它省 ID
示例(想把用户邮箱从 old@ 改成 new@,但 new@ 可能已被注册):
INSERT INTO users (id, email, name) VALUES (123, 'new@example.com', '张三') ON DUPLICATE KEY UPDATE name = VALUES(name), updated_at = NOW();
PostgreSQL:必须显式指定 ON CONFLICT 目标
PG 不接受模糊匹配,ON CONFLICT 后必须写明冲突依据——要么是唯一约束名(推荐),要么是完整列组合。
- 如果唯一约束是联合索引(如
(tenant_id, email)),就不能只写ON CONFLICT (email),得写ON CONFLICT (tenant_id, email)或ON CONFLICT ON CONSTRAINT uk_tenant_email - 用
EXCLUDED伪表访问本次想插入但被拒的值,比如email = EXCLUDED.email - 加
WHERE可防无谓锁:例如只在旧邮箱为空时才覆盖,WHERE users.email IS NULL -
DO NOTHING最轻量,适合纯去重;DO UPDATE会持行锁,高并发下慎用
示例(基于约束名 uk_user_email):
INSERT INTO users (id, email, name) VALUES (123, 'new@example.com', '张三') ON CONFLICT ON CONSTRAINT uk_user_email DO UPDATE SET name = EXCLUDED.name, updated_at = NOW();
逻辑删除场景:唯一索引要设计成可复用的
如果业务允许“删了还能重用”,就别让唯一索引锁死全生命周期。常见破局点:
- 把单列唯一索引改成复合索引,加入逻辑删除标记或时间戳,例如
(username, deleted_at),其中deleted_at IS NULL表示有效,IS NOT NULL表示已删——这样同名用户删后再注册就能过 - 避免用
is_deleted TINYINT和唯一索引组合,因为0/1只有两个值,无法区分多个已删记录 - 若用软删,
deleted_at DATETIME DEFAULT NULL是更安全的选择,配合UNIQUE (username, deleted_at)真正实现“一人一活号”
真正难的不是语法怎么写,而是判断哪一层该做决策:数据库用约束保底线,应用层用重试控节奏,架构层用索引设计消冲突——三者漏掉任何一层,都可能在凌晨三点收到告警。











