大表加列默认值 INSTANT 秒级完成
500 万行加列:INSTANT 秒级 vs 传统锁表几小时
产品要求给订单表加一个"订单来源"字段,标记订单来自 web、App 还是小程序。表有 500 万行数据,DBA 在 MySQL 5.7 上执行:
ALTER TABLE t_order ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'web' COMMENT '订单来源';这条 DDL 跑了 8 分钟,期间 MDL 锁导致业务查询排队堆积,监控告警一片飘红。加一列而已,为什么要重建整张表?
这就是 "大表加列锁表" 的经典痛点——MySQL 5.7 的 ADD COLUMN 虽然支持 INPLACE,但仍需逐行重建表数据,执行时间与表行数成正比。而 MySQL 8.0 引入的 ALGORITHM=INSTANT 将加列变成了纯元数据操作,无论表多大都是毫秒级完成。
真实场景
大表加列是生产环境最常见的 DDL 需求之一。业务迭代总要加字段:订单来源、扩展属性、标记位。在 5.7 时代,每次加列都是一次"小型手术"——低峰期执行、提前通知、提心吊胆。8.0 的 INSTANT 算法彻底解决了这个痛点。
问题分析
bad.sql
-- MySQL 5.7 传统方式加列(重建整张表)
-- 5.7 中 ADD COLUMN 默认走 INPLACE 但需要重建表数据(rebuild)
-- 过程: 创建新表结构 -> 逐行拷贝数据 -> 重命名替换 -> 释放旧表
-- 50 万行需完整拷贝,期间持有 MDL 排他锁,阻塞所有 DML
-- 生产 500 万行表锁表可达 10 分钟以上
ALTER TABLE t_order ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'web' COMMENT '订单来源';DDL 执行过程
MySQL 5.7 的 ADD COLUMN 虽然支持 ALGORITHM=INPLACE,但仍属于 rebuild 操作:
1. 获取 MDL 排他锁
2. 创建带新列的临时表结构
3. 逐行从原表拷贝数据到临时表(50 万行全量拷贝)
4. 拷贝期间降级 MDL 锁,允许并发 DML(INPLACE 模式)
5. 拷贝完成后再次获取 MDL 排他锁
6. 重命名临时表替换原表,释放旧表
7. 释放锁为什么慢
| 维度 | 5.7 传统加列 | 影响 |
|---|---|---|
| 数据拷贝 | 全表逐行拷贝 | 50 万行需完整重建,I/O 和 CPU 开销大 |
| 磁盘空间 | 需要 2 倍表空间 | 原表 + 临时表同时存在 |
| 执行时间 | 与表行数成正比 | 50 万行约 30-60 秒,500 万行约 5-10 分钟 |
| MDL 锁 | 开始和结束阶段排他 | 高并发下可能导致查询排队堆积 |
| 主从延迟 | 从库同样耗时 | 主库 10 分钟,从库也 10 分钟,数据严重滞后 |
核心问题:
- 全表数据重建:InnoDB 需要为每一行重新组织记录格式,加入新列的默认值
- 逐行拷贝:50 万行数据逐行读取、转换、写入新表,I/O 开销巨大
- 时间与行数成正比:表越大,拷贝时间越长。500 万行生产表可达 10 分钟以上
- 磁盘空间翻倍:拷贝期间原表和临时表同时存在,需要 2 倍表空间
- 主从级联延迟:主库执行 10 分钟,从库回放同样需要 10 分钟,期间从库数据严重滞后
核心认知
5.7 加列慢的本质不是"加列"这个动作慢,而是"为每一行物理写入默认值"慢。新列的默认值被逐行写进每条记录,这本质上是一次全表 UPDATE。
优化方案
good.sql
-- MySQL 8.0 INSTANT 算法加列(秒级完成)
-- ALGORITHM=INSTANT 只修改数据字典中的元数据,不触碰表数据
-- 新列的默认值记录在元数据中,查询时动态返回,无需逐行填充
-- 无论表有多少行,执行时间都是毫秒级
-- 注意: INSTANT 是 8.0.12+ 的特性,5.7 不支持
ALTER TABLE t_order ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'web' COMMENT '订单来源', ALGORITHM=INSTANT;原理
ALGORITHM=INSTANT 的执行流程完全不同:
1. 短暂获取 MDL 排他锁(毫秒级)
2. 修改数据字典中的表结构元数据(记录新列定义和默认值)
3. 释放锁
完成!不触碰任何表数据核心原理:
- 只改元数据:新列的定义(名称、类型、默认值)写入数据字典,表数据文件完全不动
- 默认值动态返回:查询时 InnoDB 发现行记录中没有新列,直接从元数据读取默认值返回
- 与表大小无关:无论 50 万行还是 5 亿行,执行时间都是毫秒级
- MDL 锁极短:仅在修改元数据的瞬间持有排他锁,业务完全无感知
传统方式 (5.7):
行记录: [id][order_no][user_id][amount][status][created_at]
加列后: 每行都要物理插入 [source] 字段 -> 全表重建
INSTANT (8.0):
行记录: [id][order_no][user_id][amount][status][created_at] <- 不变
元数据: source VARCHAR(20) DEFAULT 'web' <- 只改这里
查询时: 发现行中没有 source -> 从元数据取默认值 'web' 返回对比
| bad.sql (5.7 传统) | good.sql (8.0 INSTANT) | |
|---|---|---|
| 数据拷贝 | 全表逐行重建 | 不拷贝任何数据 |
| 磁盘空间 | 2 倍表空间 | 0 额外空间 |
| 50 万行耗时 | ~30-60 秒 | ~10 毫秒 |
| 500 万行耗时 | ~5-10 分钟 | ~10 毫秒 |
| MDL 锁持有 | 秒级~分钟级 | 毫秒级 |
| 主从延迟 | 从库同样耗时 | 从库同样毫秒级 |
| 指标 | 优化前 (bad) | 优化后 (good) |
|---|---|---|
| 访问类型 | - | - |
| 使用索引 | - | - |
| 扫描行数 | - | - |
| 附加信息 | - | - |
🚀 50 万行加列从 30-60 秒降到 10 毫秒,提升 3000+ 倍,且与表大小无关
INSTANT 支持的操作(8.0)
| 操作 | 是否支持 INSTANT |
|---|---|
| ADD COLUMN(末尾加列) | ✅ 支持 |
| RENAME COLUMN | ✅ 支持 |
| DROP COLUMN | ✅ 8.0.29+ 支持 |
| 修改列默认值 | ✅ 支持 |
| ADD INDEX | ❌ 不支持(需 INPLACE) |
| MODIFY COLUMN(改类型) | ❌ 不支持(需 INPLACE/COPY) |
避坑指南
注意事项
升级到 8.0:如果还在 5.7,大表加列是最大的 DDL 痛点之一,升级 8.0 是最彻底的解决方案。
末尾加列才支持 INSTANT:8.0.12 只支持在表末尾加列,中间插入列(AFTER/BEFORE)需 8.0.29+。
不要显式指定 COPY:
ALGORITHM=COPY会强制走最差路径,让 MySQL 自行选择最优算法。验证算法:执行前可用
ALTER TABLE ... ALGORITHM=INSTANT显式指定,不支持会直接报错而非静默退化。8.0 默认即 INSTANT:8.0 中
ADD COLUMN不指定 ALGORITHM 时默认尝试 INSTANT,无需显式声明。但显式声明更安全——不支持时会直接报错。
5.7 vs 8.0 差异
| 特性 | 5.7 | 8.0 |
|---|---|---|
| ADD COLUMN 算法 | INPLACE(需 rebuild) | INSTANT(纯元数据) |
| 加列耗时(50 万行) | 30-60 秒 | ~10 毫秒 |
| 加列耗时(500 万行) | 5-10 分钟 | ~10 毫秒 |
| 磁盘空间需求 | 2x 表空间 | 0 额外 |
| 默认值处理 | 逐行物理写入 | 元数据记录,查询时动态返回 |
8.0 的 INSTANT 是 DDL 领域的革命
INSTANT 算法将加列从"全表重建"变为"纯元数据修改",是 MySQL 8.0 最有价值的特性之一。如果你还在 5.7,大表加列的痛苦就是升级 8.0 最充分的理由。8.0.12+ 支持末尾加列 INSTANT,8.0.29+ 进一步支持任意位置加列和删列 INSTANT。
本地复现
# 默认在 MySQL 8.0 上运行
./scripts/run-case.sh 48-instant-add-column
# 在 MySQL 5.7 上运行(对比)
./scripts/run-case.sh 48-instant-add-column --ver 5.7
# 跳过造数据重跑
./scripts/run-case.sh 48-instant-add-column --no-seed