结论先行:「给大表加列」和「给大表加索引」是两条完全不同的成本曲线。加列在 MySQL 8.0 上多数场景可以走 INSTANT——只改元数据、不重建表、不回填数据,1.8 亿行的表也能 0.4s 完成、对业务 DML 零阻塞;但它有一道版本雷区(8.0.17 之前根本不存在、8.0.17~8.0.28 只能加在表末尾)和几个降级陷阱(带表达式默认值、加列同时改其他列)。选错姿势,一次「加个字段」就能退化成几小时的 COPY 重建、把变更窗口直接打穿。这篇文章把「加列」从加索引的语境里拆出来单独量化讲清楚。
一、现象与背景
去年给一套支付对账系统做迭代。核心表 t_pay_flow:
- 规模:1.8 亿行、单表 96GB、InnoDB、MySQL 8.0.34(云主机 16C64G、NVMe 云盘)
- 业务方提的「小需求」:加一列
channel_sub_id VARCHAR(64) NOT NULL DEFAULT ''记录渠道子单号,再加一列retry_cnt INT NOT NULL DEFAULT 0。 - 团队第一反应是「大表,肯定要走 gh-ost / pt-osc,准备一个 8 小时的夜间窗口」。
结果我直接在低峰用原生 ALTER 跑了一遍,两条列一起加,全程 0.4s,SHOW PROCESSLIST 里 DML 一条没卡,主从延迟 0.1s 都没见着尖刺。业务方以为我偷偷切了从库——没有,就是 ALGORITHM=INSTANT。
对比:如果这两列走 ALGORITHM=INPLACE 强制重建,按 96GB 全表拷贝 + 双写放大算,保守估计 1h40min 起步(参考同规模加索引的实测,见下),且 commit 阶段要抢 MDL 写锁,高 DML 下随时可能 Lock wait timeout exceeded。
一句话:同样是 ALTER,加列和加索引的代价差了一个数量级,根因是 InnoDB 能不能「只动元数据」。
二、原理:为什么加列能 INSTANT、加索引不能
InnoDB 的表结构变更分三档算法,关键看「要不要重写数据页」:
- COPY:建新表 → 逐行拷数据 → 改名。全表读 + 全表写,大表基本不可用。
- INPLACE:原地改结构 + 回填数据。加索引就属于这档——因为二级索引是独立 B+ 树,必须把聚簇索引的每一行都扫一遍、逐条塞进新索引,绕不开全表 I/O。
- INSTANT:只改 data dictionary(数据字典)里的列定义,不动任何数据页。新列的值对存量行是「逻辑上存在、物理上不存储」——读的时候按默认值补,写的时候才落盘。
为什么加列能走这条快路径?因为 InnoDB 的行格式是「按列位置编码」但允许「尾部追加列」:在表末尾加一列,存量行的物理字节完全不用动,只需在字典里记「第 N+1 列 = 默认值」。这就是 INSTANT 的本质——加列是「改说明书」,加索引是「搬全部书」。
这里引陈清和-34 的一句话很到位:「INSTANT 只改元数据、秒级完成,但只支持部分列操作(加减列、改列类型里的安全子集),加二级索引不支持 INSTANT」——加列和加索引的分界线就在这里。
而 INSTANT 加列的「逻辑默认值」机制,正是它零 I/O、零锁的来源:不需要为 1.8 亿行逐行写默认值,因为默认值只在「读到还没写过的行」时由引擎现场补。
三、具体做法(按收益/成本排序)
方案 1:原生 INSTANT 加列(首选,收益最大、成本最低)
-- 显式声明算法 + 锁级别,避免静默降级到 INPLACE/COPY
ALTER TABLE t_pay_flow
ADD COLUMN channel_sub_id VARCHAR(64) NOT NULL DEFAULT '',
ADD COLUMN retry_cnt INT NOT NULL DEFAULT 0,
ALGORITHM=INSTANT, LOCK=NONE;
LOCK=NONE 的语义是「做不到无锁就直接报错」——比 DBA 肉眼盯 PROCESSLIST 安全得多。配合会话变量给收尾阶段兜底:
SET lock_wait_timeout = 5; -- 万一降级,收尾拿锁最多等 5s,拿不到回滚而非硬等
SET innodb_lock_wait_timeout = 5;
实测:两条列一起加 0.4s,information_schema.innodb_tablespaces 里 INSTANT_COLS 从 0 → 2,业务 DML 无感。
验证 INSTANT 是否真的生效(而不是静默降级):
SELECT TABLE_NAME, INSTANT_COLS FROM information_schema.innodb_tablespaces WHERE TABLE_NAME = 't_pay_flow'; -- INSTANT_COLS>0 说明走了 INSTANT或看
SHOW CREATE TABLE,INSTANT 加列不会留下ALGORITHM注释,而 INPLACE 重建后SHOW ENGINE INNODB STATUS会有 DDL 痕迹。
方案 2:必须加在中间位置 → 看版本决定走法
-- 加到指定列后面(不是表末尾)
ALTER TABLE t_pay_flow
ADD COLUMN channel_sub_id VARCHAR(64) NOT NULL DEFAULT '' AFTER order_no,
ALGORITHM=INSTANT, LOCK=NONE;
这里是最大的版本雷区(详见下一节)。
方案 3:INSTANT 不支持的变更 → gh-ost / pt-osc 兜底
当变更不能走 INSTANT(改列类型、加列带表达式默认值、5.7 老库)时,才上影子表工具。但注意陈星河-382 的实测结论:「pt-osc 的 trigger 机制天然引入 2x 写入放大 + 串行化瓶颈」——高写入吞吐场景下,它的「安全」是用「慢 + 主从延迟尖刺」换来的,不是免费的。能用 INSTANT 就别碰工具,能用原生 INPLACE 就少碰 pt-osc。
四、踩过的坑(反直觉,全是版本/语法降级)
-
8.0.17 之前根本没有 INSTANT 加列:MySQL 5.7 全系列加列一律 COPY/INPLACE 重建,1.8 亿行就是几小时。8.0.17 才引入 INSTANT ADD/DROP COLUMN。所以「能不能秒级加列」第一问是
SELECT VERSION();,别默认支持。 -
8.0.17 ~ 8.0.28:INSTANT 加列只能加在表末尾。
AFTER order_no这种指定位置的写法,在这两个版本区间会静默降级到 INPLACE(不报错!),1.8 亿行直接 1h+。8.0.29 起才支持任意位置 + 一次加多列的 INSTANT。我 8.0.34 上AFTER能秒级完成,但同集团一个 8.0.21 的库跑同样的 SQL 卡了两小时——根因就是这条版本线。 -
带表达式默认值不支持 INSTANT:
ADD COLUMN gmt_create DEFAULT (NOW())这种DEFAULT (expr)生成列默认值,会被踢出 INSTANT,退化成 INPLACE。想要「创建时间」就老老实实让应用层填,或加普通列后业务回填,别用生成式默认值图省事。 -
INSTANT DROP COLUMN 不释放空间、且不可逆:
ALTER TABLE ... DROP COLUMN走 INSTANT 时只是把列「标记删除」,底层数据页的字节还在,INSTANT_COLS会累计增长;想真正回收空间得再做一次OPTIMIZE TABLE(那是 COPY 重建,又回到几小时)。所以「加一列又删一列」并不能让表变回原样。 -
加列和改列混在一条 ALTER 里,整条降级:
ADD COLUMN a INT, MODIFY COLUMN b BIGINT这种,只要 MODIFY 那一支不能 INSTANT,整条语句全部退化成 INPLACE,你新加的那列也陪跑全表重建。变更要拆条:能 INSTANT 的列单独一条,不能的另开一条。
五、结论与可复用清单
加列的成本,取决于三件事:版本、位置、是否纯加列。决策顺序:
- 先确认版本:
SELECT VERSION();——8.0.17+ 才有 INSTANT,5.7 别指望秒级。 - 判断能否纯 INSTANT:只加列(或安全子集的改类型)、无表达式默认值 → 直接
ALGORITHM=INSTANT, LOCK=NONE,秒级、零阻塞,这是默认姿势,别默认上 gh-ost。 - 要加在中间:8.0.29+ 可 INSTANT;8.0.17~8.0.28 会静默降级 → 要么挪到表末尾,要么接受 INPLACE 重建(按窗口排期)。
- 必须重建(改类型 / 5.7 / 表达式默认值):才上 gh-ost / pt-osc,且按「最慢从库 + 高写入放大」预估耗时,别按主库算。
可复用清单:
- [ ] 加列前先跑
SELECT VERSION();,把「能不能 INSTANT」写进变更单 - [ ] 默认
ALGORITHM=INSTANT, LOCK=NONE,让「做不到无锁」直接报错而不是降级 - [ ] 用
information_schema.innodb_tablespaces.INSTANT_COLS验证是否真走了 INSTANT - [ ] 8.0.17~8.0.28 项目:INSTANT 加列只允许表末尾,
AFTER一律避开 - [ ] 变更拆条:能 INSTANT 的列单独一条,改列/表达式默认值另开一条,避免整条降级
- [ ] INSTANT DROP COLUMN 后想回收空间,记得
OPTIMIZE TABLE是 COPY 重建,按大表排期 - [ ] 加列 ≠ 加索引:前者可零 I/O,后者必然全表 I/O,别用同一套「大表必须上工具」的直觉套两者
一句话收尾:大表加列的解法不是「准备一个 8 小时窗口」,而是「先问版本、再问算法,能 INSTANT 就别让 InnoDB 搬表」——90% 的「加字段」都不该付出重建表的代价。
环境:MySQL 8.0.34 / InnoDB / t_pay_flow 1.8 亿行 / 96GB;0.4s 为低峰实测,INPLACE 1h40min 为同规模加索引实测外推,均已脱敏。INSTANT 版本边界引自 MySQL 官方 release notes。