2787 字
约 9 分钟
14
S11-X13 · 大表 DDL 与在线变更
2024-12-31
2026-11-08

一张几千万行的表要加个字段,ALTER TABLE 敲下去,从库延迟飙到几千秒,业务开始报超时,最后 DBA 手动 kill。这不是运气问题,而是算法选错了:ALTER 在 8.0 里可能只改元数据,也可能把整张表拷一遍再改名,中间差着几个数量级。

按"算法 → 哪些操作走哪种 → INSTANT/INPLACE 的代价 → MDL → 工具 → 分步策略"拆开。


1. 三种 DDL 算法

算法 是否重建表 是否阻塞 DML 相对耗时(典型量级)
INSTANT 否,只改元数据 不阻塞 秒级以内
INPLACE 可能重建(取决于操作) 多数情况允许并发 DML 与表大小线性相关
COPY 是,建临时表逐行拷 阻塞写 最慢,且需要双倍磁盘
ALTER TABLE orders ADD COLUMN channel VARCHAR(16) NULL, ALGORITHM=INSTANT;

ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at),
  ALGORITHM=INPLACE, LOCK=NONE;

显式写 ALGORITHM= 与 LOCK= 是唯一可靠的防线。不写时 MySQL 会自动挑一个它能做到的算法——你以为在跑在线 DDL,实际可能已经退化成 COPY 并锁了写。写上之后,做不到就直接报错,而不是静默降级:

ERROR 1846 (0A000): ALGORITHM=INPLACE is not supported. Reason: ...

报错比锁表好得多。


2. 哪些操作能 INSTANT,哪些必须重建

操作 算法 备注
ADD COLUMN 在末尾 INSTANT 8.0.12 起支持
ADD COLUMN 指定 AFTER/FIRST INSTANT 8.0.29 起支持,更早版本需 INPLACE
DROP COLUMN INSTANT 8.0.29 起支持
改列 DEFAULT INSTANT 8.0.13 起支持
RENAME COLUMN INSTANT 只改元数据
加普通/唯一索引 INPLACE 可 LOCK=NONE
加全文索引 INPLACE 需 LOCK=SHARED,期间阻塞写
改列类型 / 字符集 / ROW_FORMAT COPY 必须重建
OPTIMIZE TABLE INPLACE 重建以回收空间

一条经验:改"表的结构描述"多为 INSTANT,改"每一行的字节布局"必然重建。列类型、字符集、行格式都属于后者。


3. INSTANT 的隐藏代价

INSTANT 快,但它不是免费的:

代价 说明
行格式被打上 instant 标记 新列值不进主行,读时需额外解析
删列不返还磁盘 DROP COLUMN ... ALGORITHM=INSTANT 只标记不可见,空间要重建才回收
多次 INSTANT 叠加 按 8.0.x 文档核对,早期版本对 instant 列数量有限制
老版本工具解析异常 从库或旧客户端可能读不对
-- 要让 INSTANT 留下的空洞真正回收,只能重建
ALTER TABLE orders FORCE, ALGORITHM=INPLACE, LOCK=NONE;
-- 或
OPTIMIZE TABLE orders;

重建本身是一次全表扫描 + 全表写,仍然是重操作——"先 INSTANT 后重建"只是把一次重活拆成两次,不是消除它。


4. INPLACE 的在线重建机制

INPLACE 分三段:

阶段 做什么 锁
prepare 取 MDL 排他,检查、建新索引的中间结构 极短的排他 MDL
execute 扫描聚簇索引、排序、构建新索引;期间 DML 写入 row log 允许并发 DML
commit 再次取排他 MDL,应用 row log,替换 极短的排他 MDL
SHOW VARIABLES LIKE 'innodb_online_alter_log_max_size';   -- 默认 128M
SHOW VARIABLES LIKE 'innodb_sort_buffer_size';

innodb_online_alter_log_max_size 是 DDL 期间并发 DML 的缓冲上限。写流量大或 DDL 跑得久,缓冲撑满就直接失败:

ERROR 1799 (HY000): Creating index 'idx_x' required more than
'innodb_online_alter_log_max_size' bytes of modification log. Please try again.

这个失败是安全的:表没坏,只是白跑一趟。处理是调大缓冲或低峰重跑,不是改小并发度就完事。


5. COPY 为什么锁写

COPY 的流程是:建一张带新结构的临时表 → 对原表加共享锁(阻塞写、允许读)→ 逐行拷数据 → 改名替换。阻塞写的根本原因是拷贝期间无法保证新写入也符合新结构,只能把写挡在外面。

阶段 做什么 后果
建临时表 按新结构建空表 需整表大小的磁盘
加共享锁、逐行拷 允许读、阻塞写 写入全部排队
RENAME 替换 原子改名 切换要拿 MDL

6. MDL 的排队效应

DDL 要拿元数据锁的排他模式,DML 拿共享模式。共享之间兼容,共享与排他互斥。于是出现这个链条:

时刻 事件 后果
T0 一个长查询持有共享 MDL 正常
T1 DDL 请求排他 MDL,排队 DDL 等待
T2 新查询请求共享 MDL 排在 DDL 后面
T3 新查询越积越多 连接池打满,全站超时

关键点是 T2:MDL 队列不允许后来的共享锁插到排他锁前面,否则 DDL 永远等不到。所以一个慢查询就能让后面的 DDL 变成雪崩的起点。

SELECT * FROM performance_schema.metadata_locks
WHERE OBJECT_TYPE = 'TABLE' AND OBJECT_SCHEMA = 'app'\G

SELECT * FROM performance_schema.threads WHERE PROCESSLIST_ID = 812\G
-- 先杀长查询,再执行 DDL
KILL 812;
-- 或给 DDL 的 MDL 等待设上限(默认极大)
SET SESSION lock_wait_timeout = 5;

lock_wait_timeout 默认值非常大,意味着 DDL 会一直等下去并堵住所有人。执行 DDL 前把它设成几秒,是在给自己留退路。


7. pt-online-schema-change 与 gh-ost

维度 pt-osc gh-ost
同步机制 在原表建三个触发器,把变更写进影子表 解析 binlog,回放到影子表
原表额外开销 每次 DML 多三次触发器执行 几乎为零,但多一个 binlog 读取连接
与已有触发器冲突 冲突,需先处理 有触发器的表直接不支持
权限要求 较高(建触发器、改表) 需要 REPLICATION 权限读 binlog
限速手段 --max-load、--critical-load、--chunk-time --max-load、--critical-load、--throttle-*
切换 RENAME TABLE 原子替换 同样靠 RENAME,配合 --cut-over
pt-online-schema-change \
  --alter "ADD COLUMN channel VARCHAR(16) NULL" \
  --max-load Threads_running=30 --chunk-time 0.5 \
  D=app,t=orders,h=127.0.0.1

切换瞬间的风险两边一样:RENAME TABLE 需要拿到新旧两张表的排他 MDL。如果此刻有长事务或长查询,切换会卡住,而工具已经把写引流到影子表一会儿了——这段窗口里丢写是最常见的翻车方式。对策是先确认 Threads_running 平稳、无长事务,再切;或改用 --cut-over=two 这类显式两步切换。


8. 分步变更策略

能不重建就不重建。加字段、改字段这类变更,用 expand-contract 拆成六步:

阶段 动作 可回滚点
1 expand ADD COLUMN new_col ... NULL,ALGORITHM=INSTANT 随便删
2 双写 应用同时写旧列与新列 关掉双写即可
3 回填 分批把旧列值刷进新列 停回填即可
4 切读 读路径改用新列 切回旧列
5 停旧写 观察一个业务周期后停止写旧列 重新开双写
6 contract DROP COLUMN old_col,ALGORITHM=INSTANT 需恢复则重新加
-- 阶段 1
ALTER TABLE orders ADD COLUMN channel VARCHAR(16) NULL, ALGORITHM=INSTANT;
-- 阶段 6(8.0.29+ 可直接 INSTANT)
ALTER TABLE orders DROP COLUMN old_channel, ALGORITHM=INSTANT;

每一步都可回滚,代价是工期变长。改列类型(如 VARCHAR(32) → VARCHAR(64))无法用 INSTANT,只能走这套流程,这是绕开 COPY 的正路。


9. 回填的限速与分批

手段 写法 注意
主键区间分批 WHERE id BETWEEN ? AND ? 区间固定,最可控
游标分批 WHERE id > ? ORDER BY id LIMIT 500 适合边回填边有新增
副本延迟保护 监控 Seconds_Behind_Master 即停 需要外部脚本
UPDATE orders SET channel = 'legacy'
WHERE id > 0 AND id <= 100000 AND channel IS NULL
ORDER BY id LIMIT 500;

分批回填的关键不是批大小,而是每批都是一个独立小事务:不要包在一个大事务里,否则 undo 与锁会一起膨胀,反倒制造长事务。


10. 变更前后检查清单

检查项 命令 判断
表大小 SELECT table_rows, data_length, index_length FROM information_schema.TABLES WHERE ... 估算 DDL 时长与磁盘需求
磁盘余量 df -h 至少要留 1.2 倍表大小(COPY 需 2 倍)
从库延迟 SHOW REPLICA STATUS 变更前必须接近 0
长事务 SELECT * FROM information_schema.INNODB_TRX 有超过几十秒的直接推迟
长查询 SELECT * FROM information_schema.PROCESSLIST WHERE TIME > 60 先清理,避免 MDL 排队
业务低峰 看访问曲线 DDL 起始时间点
备份 逻辑或物理备份 大变更前必须有一次可恢复点

11. 排错:五种"DDL 把库搞挂了"

现象 原因 处理
ALTER 卡住不返回 等 MDL,前面有长查询 查 metadata_locks,kill 长查询
报 1846 不支持 INPLACE 操作本身不支持该算法 换算法或改分步方案,别硬上
报 1799 row log 溢出 并发 DML 超过缓冲 调大 innodb_online_alter_log_max_size 或低峰重跑
DDL 期间主从延迟暴涨 单条 DDL 在从库串行回放 分片、分批、低峰;必要时 pt-osc --max-load
加索引把磁盘打满 新索引与临时排序文件同时占用 预留空间,先看 df 再动手

12. 边界:什么时候该换方案

场景 建议 理由
表几十亿行、改列类型 影子表 + binlog 同步(gh-ost) 单机 COPY 已不可行
需要跨实例迁移 逻辑复制 / DTS DDL 只是迁移的一部分
分区键调整 重建分区表 无在线算法
只增字段、业务允许可空 INSTANT 足够 秒级完成,零风险

一句话收束:能用 INSTANT 就别用 INPLACE,能用 INPLACE ... LOCK=NONE 就别用 COPY;做不到时,把一次大 DDL 拆成六步可回滚的小变更。


13. 自测

  1. 不写 ALGORITHM= 时 MySQL 会怎么选算法?为什么危险?
  2. 哪三类操作一定需要重建表?
  3. DROP COLUMN 用 INSTANT 之后,磁盘空间什么时候才真的回来?
  4. innodb_online_alter_log_max_size 撑满会导致什么?是数据损坏还是执行失败?
  5. 一个长查询是怎么把后续所有查询都堵在 DDL 后面的?
  6. pt-osc 与 gh-ost 在同步机制上的差别,各带来什么限制?
S11-X13 · 大表 DDL 与在线变更
https://www.clxhxhhr.top/posts/5884/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。