大表加唯一索引不必然锁表,但默认操作易因mdl锁、全量查重校验和重复数据报错而卡住;真正无锁需显式指定algorithm和lock,并彻底清理重复值。

大表加唯一索引不必然锁表,但默认 ALTER TABLE ... ADD UNIQUE KEY 在 MySQL 5.6+ 中仍可能引发短时 MDL 锁、阻塞 DML,尤其在有长事务或大查询时;真正“不锁表”需显式控制 ALGORITHM 和 LOCK,并提前清理重复数据。
为什么大表加唯一索引容易卡住或阻塞业务
不是因为“加索引本身要锁全表”,而是三个隐性瓶颈叠加:一是 Prepare/Commit 阶段需获取元数据锁(MDL),若此时有未提交事务或慢查询占着表,ALTER 会一直等待;二是唯一索引创建必须校验全量数据的唯一性,扫描主键索引 + 构建新索引的过程消耗 I/O 和 CPU,拖慢从库同步;三是遇到重复值时直接报错中断,无法跳过或提示哪几行冲突。
- 典型错误现象:
ERROR 1062 (23000): Duplicate entry 'xxx' for key 'uk_col',但没告诉你在哪条记录上 - 实际锁表现象常表现为:
SHOW PROCESSLIST中看到alter table状态卡在Waiting for table metadata lock - 影响范围不限于写入——MDL 锁会阻塞后续所有对该表的
SELECT(哪怕只是简单查询),只要它需要获取读锁
必须做的前置检查:查重 & 清理
唯一索引创建失败几乎都源于已有重复数据。MySQL 不会自动帮你去重,也不会跳过,更不会合并。这步漏掉,后面所有优化都白搭。
- 先定位重复项:
SELECT col, COUNT(*) FROM my_table GROUP BY col HAVING COUNT(*) > 1; - 确认是否允许 NULL:多个
NULL值在唯一索引中是合法的(InnoDB 行为),但业务上是否合理需人工判断 - 清理策略示例(保留最大
id的那条):DELETE t1 FROM my_table t1 INNER JOIN my_table t2 WHERE t1.id - 执行前务必在从库或备份库验证逻辑,避免误删
用 ONLINE DDL 实现近似无锁添加
MySQL 原生支持,无需第三方工具,但必须显式指定参数,否则可能退化为 COPY 模式(即锁表重建)。
- 强制使用原地算法:
ALGORITHM=INPLACE—— 避免创建临时表 - 要求零锁:
LOCK=NONE—— 允许并发读写(注意:仅对 DML 有效,DDL 自身仍需极短 MDL) - 完整语句示例:
ALTER TABLE my_table ADD UNIQUE KEY uk_email (email), ALGORITHM=INPLACE, LOCK=NONE; - 失败常见原因:表含外键、使用 MyISAM 引擎、列类型不支持在线变更(如 TEXT 列参与唯一索引)、存在长事务未释放 MDL
超大表(>1亿行)或高可用要求场景:用 gh-ost 替代
当 Online DDL 的 Row Log 回放阶段拖太久、或从库延迟已敏感时,gh-ost 是更可控的选择——它不依赖 MySQL 内部机制,而是通过 binlog 解析做增量同步,且支持 hook 中断。
- 优势在于:全程不锁原表,DML 完全不受影响;可通过
--hooks-path注入脚本,在检测到重复插入时主动退出 - 劣势:不校验全量唯一性(
INSERT IGNORE全量导入),所以**必须确保前置查重已 100% 干净**,否则静默丢数据 - 启动命令关键参数:
gh-ost --host=... --database=my_db --table=my_table --alter="ADD UNIQUE KEY uk_col(col)" --assume-rbr --allow-on-master --initially-drop-ghost-table --execute - 切表前建议先跑一次
--dry-run,观察日志中Estimated rows和Throttle metrics是否稳定
真正麻烦的从来不是语法,而是“以为加了 LOCK=NONE 就万事大吉”,结果被一个没注意到的长事务卡住两小时;或者用 gh-ost 时忘了清重,上线后发现部分用户邮箱莫名丢失。这些点没法靠文档自动提醒,得在执行前盯着 INFORMATION_SCHEMA.INNODB_TRX 和 SHOW PROCESSLIST 多看两眼。











