必须先加唯一索引,否则insert ignore、on duplicate key update、replace into均退化为普通插入;唯一索引是防重语法生效的强制前提,无索引则不拦截、不报错、不生效。

必须先加唯一索引,否则所有防重语法都失效
没有 UNIQUE 或 PRIMARY KEY 约束,INSERT IGNORE、ON DUPLICATE KEY UPDATE、REPLACE INTO 全部退化为普通插入——不报错、不拦截、不生效。这是最常被跳过的前提,也是后续所有逻辑崩塌的起点。
加索引不是“可选优化”,而是强制前置动作:
-
ALTER TABLE users ADD UNIQUE KEY uk_phone (phone);(单列) -
ALTER TABLE orders ADD UNIQUE KEY uk_user_time (user_id, created_at);(组合列)
注意:索引列不能包含 NULL 值(除非业务明确允许空值且需去重),否则 UNIQUE 对多个 NULL 不判重。
INSERT IGNORE 适合“只写不管结果”的埋点/日志场景
它在冲突时静默跳过,不报错、不更新、不影响后续语句执行,返回 rowCount() 为 0。适用于数据可丢、状态无需反馈的轻量写入。
- PHP 中用法:
$stmt = $pdo->prepare("INSERT IGNORE INTO events (event_id, payload) VALUES (?, ?)"); - 风险点:它会忽略所有约束冲突(主键、唯一键、
NOT NULL),可能掩盖字段缺失等真实问题 - 不适合需要区分“新增”和“已存在”的业务判断逻辑
ON DUPLICATE KEY UPDATE 更可控,但要注意死锁和返回值含义
它在冲突时触发更新,哪怕只是 SET id = id 这种空操作,也能通过影响行数(1=插入,2=更新)做业务分支判断。
- 安全空更新写法:
INSERT INTO users (phone, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE phone = VALUES(phone); - 高并发下多个事务同时插相同值,MySQL 会对冲突行加锁;若插入顺序不一致(如 A 插
phone='1'后等'2',B 反之),极易触发死锁 -
LAST_INSERT_ID()返回的是插入那条记录的 ID,不是更新行的 ID,别混淆
绝对不要用“先 SELECT 再 INSERT”
无论加不加事务、设不设 SERIALIZABLE 隔离级别,SELECT 和 INSERT 是两个独立语句,中间存在不可消除的竞态窗口。两个请求几乎同时查到“不存在”,接着都插入成功——这是必然,不是概率。
- MyBatis、Laravel Eloquent、Django ORM 的
get_or_create类方法,底层若走两步 SQL,同样踩坑 - SQL Server 的
NOT EXISTS子查询写法(INSERT ... SELECT ... WHERE NOT EXISTS)看似原子,实则仍依赖唯一索引才能真正防重;没索引时只是多绕一次,不解决本质问题 - 唯一能靠得住的,永远是数据库层的约束 + 原子写入语法
真正麻烦的不是语法怎么写,而是当业务字段一开始没设唯一索引、线上表已有重复数据、又不敢停机重建时,如何平滑补救——这时候得先清理脏数据,再加约束,最后切流量,每一步都得灰度验证。










