bigywater.tech
常见报错 · 3

MySQL ERROR 1062 Duplicate entry:Abc 和 abc 也算重复,REPLACE 会让 id 变,失败的 INSERT 还会吃掉自增 id

ERROR 1062: Duplicate entry 看起来很直白,但真正查起来问题很多:报错里 for key 说的是哪个索引?为什么两个看起来不一样的值也冲突?INSERT IGNORE、ON DUPLICATE KEY UPDATE、REPLACE 到底有什么区别?为什么报错之后自增 id 出现了空洞?这篇在本机一个个复现。

350 / 400
8 个线程“先 SELECT 再 INSERT”抢同一批邮箱:有唯一索引时 INSERT 报 1062 的次数(表里留 50 行);没有唯一索引则是 400 行
5
REPLACE 命中唯一冲突之后,a@x.com 那一行的 id(原来是 1,hits 也从 5 回到 0)
5 / 8 / 10
依次让 3 次 INSERT 失败、2 次 INSERT IGNORE 被跳过、1 次 ODKU 命中之后,下一次成功插入行拿到的 id

报错文本怎么读

实测 · 本机执行输出
  主键冲突: insert into u(id, email, a, b) values (1, 'y@x.com', 2, 2)
      -> IntegrityError: (1062, "Duplicate entry '1' for key 'u.PRIMARY'")
  唯一索引冲突: insert into u(email, a, b) values ('x@x.com', 3, 3)
      -> IntegrityError: (1062, "Duplicate entry 'x@x.com' for key 'u.uk_email'")
  联合唯一索引冲突: insert into u(email, a, b) values ('z@x.com', 1, 1)
      -> IntegrityError: (1062, "Duplicate entry '1-1' for key 'u.uk_ab'")
  没起名的 unique:insert into w values (2, 'k')
      -> IntegrityError: (1062, "Duplicate entry 'k' for key 'w.email2'")
  唯一索引列允许多个 NULL:连续插入两行 email=NULL -> ok;表里 email 为 NULL 的行数 = 2

for key 后面是“表名.索引名”,联合唯一索引的冲突值用 - 连接,没起名的 unique 用第一个列名当索引名。唯一索引允许多个 NULL。

不一样的值也重复

实测 · 本机执行输出
  默认字符集和排序规则: ('utf8mb4', 'utf8mb4_0900_ai_ci')
  排序规则                                   大小写      重音       末尾空格    
  utf8mb4_0900_ai_ci(8.0 以后的默认)          重复(1062) 重复(1062) 不重复     
  utf8mb4_general_ci                     重复(1062) 重复(1062) 重复(1062)
  utf8mb4_bin                            不重复      不重复      重复(1062)

唯一索引按列的排序规则比较:默认的 utf8mb4_0900_ai_ci 不区分大小写和重音,但区分末尾空格;utf8mb4_bin 区分大小写,却忽略末尾空格。

多行 INSERT 与 INSERT IGNORE

实测 · 本机执行输出
  insert into m values (1,..),(2,..),(3,..)(第 2 行冲突,表里原来有 id=2)-> IntegrityError: (1062, "Duplicate entry '2' for key 'm.PRIMARY'")
  表里现在的行: ((2, 'old'),)(第 1、3 行没有写进去)
  insert ignore 同样三行 -> ok,影响行数 2
  show warnings: (('Warning', 1062, "Duplicate entry '2' for key 'm.PRIMARY'"),)
  表里现在的行: ((1, 'n1'), (2, 'old'), (3, 'n3'))(id=2 仍然是 old,冲突的那行被悄悄丢掉)

普通 INSERT 一行冲突整条回滚;INSERT IGNORE 跳过冲突行,留下 warning,新值被丢掉而不报错,需要自己检查 warning。

ODKU 与 REPLACE

实测 · 本机执行输出
  起始: ((1, 'a@x.com', 'A', 5),)
  ON DUPLICATE KEY UPDATE 改 name -> 影响行数 2;表: ((1, 'a@x.com', 'A2', 5),)(id、hits 保留)
  再来一次,值没有变化 -> 影响行数 0
  新邮件 b@x.com(没有冲突,走插入) -> 影响行数 1
  REPLACE INTO(email 冲突) -> 影响行数 2;表: ((4, 'b@x.com', 'B', 0), (5, 'a@x.com', 'A3', 0))(a@x.com 的 id 变了,hits 回到默认 0)

ON DUPLICATE KEY UPDATE 原地更新,保留 id 和没提到的列,影响行数更新为 2、没变化为 0、插入为 1。REPLACE 先删后插,id 变,没写的列回到默认值。

自增 id 的空洞

实测 · 本机执行输出
  innodb_autoinc_lock_mode = 2
  先插 a(id=1),再让 3 次 insert k=a 报 1062,再插 b: ((1, 'a'), (5, 'b'))
  再让 2 次 insert ignore k=a 被跳过,再插 c: ((1, 'a'), (5, 'b'), (8, 'c'))
  再来 1 次 ON DUPLICATE KEY UPDATE(命中 b,没有变化),再插 d: ((1, 'a'), (5, 'b'), (8, 'c'), (10, 'd'))

InnoDB 默认的锁模式下,触发唯一冲突的语句无论最后报错、被忽略还是转成更新,都消耗了自增 id。id 不会因此重复,但不连续。

先查后插与唯一索引

实测 · 本机执行输出
  没有唯一索引:SELECT 看到“还没有”的次数 400 / 400,INSERT 报 1062 的次数 0,表里一共 400 行,不同的邮箱 50 个
  有唯一索引:SELECT 看到“还没有”的次数 400 / 400,INSERT 报 1062 的次数 350,表里一共 50 行,不同的邮箱 50 个
  结论:“先查后插”只是一次检查,不是约束;唯一索引才是。1062 在这里是数据库在拦重复,代码里捕获它(或者直接用 INSERT … ON DUPLICATE KEY UPDATE)才是正解

检查和插入之间有时间差,并发时每个线程都看到“还没有”。唯一索引在这段空隙里依然有效,1062 就是它在拦截。

分步演示

唯一键冲突
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / MySQL 26.10.0 与 Redis 8.10.2(Homebrew,每个实验自己起实例,端口 9150 以上,只监听 127.0.0.1,不碰本机已有的实例)/ Python 3.15.0 + PyMySQL(按各篇注明用到哪些及版本);报错文本都是真实复现时原样捕获的;计时用 time.perf_counter,数字每次运行会有小幅抖动。本篇用一个自己起的 MySQL 26.10.0 实例(默认设置,库字符集 utf8mb4 / utf8mb4_0900_ai_ci),PyMySQL 的 unix socket 连接,影响行数取自 PyMySQL 的 cursor.rowcount(未设置 CLIENT_FOUND_ROWS)
    • 没有验证:MySQL 5.7 与 8.0 / 8.4 上报错文本里 for key 的格式差异(这里只有 26.10.0 上的写法);唯一索引冲突在 SERIALIZABLE / READ COMMITTED 隔离级别下的加锁与死锁;INSERT … SELECT 和 LOAD DATA 遇到 1062 的表现;innodb_autoinc_lock_mode = 0 或 1 时空洞的情形;批量插入时 IGNORE 对别的错误(数据过长、类型转换)的吞没范围;Python 以外的驱动(JDBC 抛出的异常类型)
    京ICP备2026067502号-1京公网安备11010502063487号