根本原因是MySQL 5.7默认执行全表重建流程:创建临时表→逐行拷贝数据→重建索引→原子重命名,全程持有MDL_EXCLUSIVE锁,阻塞所有DML和SELECT,且长事务会加剧MDL等待。

MySQL 5.7 大表 ALTER TABLE 锁表时间长,根本原因不是“慢”,而是它默认走全表重建(COPY)流程,且全程强占元数据锁(MDL),业务读写被硬性阻塞——这不是性能问题,是执行模型决定的不可避让行为。
为什么ALTER TABLE在5.7里基本等于重建表
5.7 对绝大多数 DDL 操作(如改字段长度、加索引、增列含 NOT NULL DEFAULT)不支持真正就地修改。它实际执行的是:
- 新建一个空临时表,结构按新定义生成
- 逐行拷贝原表所有数据(含重建所有索引)
- 原子替换原表名(
RENAME) - 删掉旧表
这个过程必须持有 MDL_EXCLUSIVE 锁,任何 SELECT/INSERT/UPDATE/DELETE 都会卡在 Waiting for table metadata lock 状态。哪怕只是把 varchar(255) 改成 varchar(1024),只要触发存储头字节扩容(1→2),就会退化为重建。
长事务是MDL锁等待的隐形放大器
MDL 锁是事务级的:一个未提交的 SELECT(哪怕只是 SELECT * FROM t)也会持续持有 MDL_SHARED_READ 锁。而 ALTER TABLE 必须等它释放才能拿到写锁。
-
innodb_lock_wait_timeout控制行锁等待(默认 50 秒),但对 MDL 无效 -
lock_wait_timeout才管 MDL 等待,默认是 31536000 秒(一年),所以你看到的不是“报错”,而是“永远不动” - 查长事务不能只看
SHOW PROCESSLIST,要联合INNODB_TRX和PROCESSLIST,重点找COMMAND = 'Sleep'但trx_state = 'RUNNING'的连接
ALGORITHM=INPLACE 并不等于“不锁表”
加个普通字段或普通索引时,ALGORITHM=INPLACE 可能真走 inplace,但有大量隐性降级条件:
- 新增
NOT NULL DEFAULT字段 → 表非空时必须回填,默认退化为COPY - 添加唯一索引 → 若存在重复值或含大
BLOB字段,仍可能 fallback 到重建 -
LOCK=NONE在 5.7 中仅对极少数操作有效,且要求无外键、无全文索引、无长事务 - 务必用
EXPLAIN FORMAT=JSON查执行计划,确认"alter_algorithm": "inplace"和"supports_inplace": true
pt-online-schema-change 为什么更可靠
它不依赖 MySQL 原生 DDL,而是用影子表 + 触发器绕开 MDL 写锁瓶颈:
- 建空新表(
user_new),立刻完成结构变更 - 在原表上建
INSERT/UPDATE/DELETE触发器,捕获所有增量变更 - 分块拷贝历史数据(可控 chunk size,避免 IO 打满)
- 数据追平后,用
RENAME TABLE原子切换,耗时毫秒级
真正难点不在工具本身,而在触发器生效期间应用是否容忍短暂双写、主从延迟是否可接受、以及磁盘空间是否预留了 1.5 倍冗余——这些细节比“会不会用命令”更容易导致线上翻车。


















