做过一次千万级订单表加字段之后,我彻底改掉了直接执行ALTER TABLE的习惯。当时业务方提需求说得很轻巧——“就在订单表加个备注列而已”,但真把那条SQL丢到生产库上跑,DML队列瞬间排起长队,慢查询监控先报警,紧接着连接数被打满。那次教训让我把在线DDL的原理、第三方工具方案、各种边界情况全部啃了一遍,也才有了下面这套"尽量不锁表"的完整打法。如果你手里也有大表加字段类似的活,这篇文章应该能帮你少走不少弯路。
1. 一个简单的ALTER TABLE,为什么能让千万级订单表"卡死"半小时
先说结论:加字段这个操作本身并不复杂,复杂的是MySQL如何把它落到一张已经堆了几千万行数据的物理表上。搞清楚这一点,你才知道哪些环节会锁、锁多久、能不能绕开。
1.1 锁表到底在锁什么:MDL锁与COPY算法的本质
MySQL锁的表现形式很多,但对一张千万级订单表执行ALTER TABLE ADD COLUMN时,最要命的是两类锁:
- Metadata Lock(元数据锁,简称MDL):MySQL Server层为了保证表结构一致而加的锁。执行DDL期间,该表的所有DML操作都要等待MDL释放。
- InnoDB层面的表锁/行锁:当DDL需要重建表时,InnoDB在复制数据阶段会对原表加排他锁,阻塞所有写入和更新操作。
这里有个很关键的点:MDL锁早在MySQL 5.5就有了,目的是防止DML在DDL执行过程中读写到结构不一致的数据。简单理解,MDL就是一个"修路期间只准行人走、不准车辆进"的临时管制——路修完了才放行。所以哪怕InnoDB层面已经支持了在线DDL,MDL切换的瞬间如果拿不到锁,依然会把后面所有请求堵住。
至于COPY算法,它是最原始也最直观的方式:把整张表的数据一行行拷到一张新表,拷完之后用新表替换旧表。假设订单表有2000万行、每行平均1KB的数据,这就意味着要发生2000万次IO写入。在普通云盘上,这种复制速度慢得让人抓狂,几十GB的表跑三四十分钟是常态。这个过程你基本动不了其他东西。
1.2 "在线DDL"的隐藏代价:5.6/5.7/8.0各版本的真实表现
很多人听到"在线DDL"就以为不锁表、可以随便执行,这是最大的误解。MySQL从5.6开始引入了ALGORITHM和LOCK参数,但这个"在线"的程度是有条件的:
| 操作类型 | ALGORITHM | 是否允许DML | 是否需要重建表 | 典型适用版本 |
|---|---|---|---|---|
| 加普通列(放在表尾) | INSTANT | 是 | 否 | MySQL 8.0.12+ |
| 加普通列(非表尾) | INPLACE | 是 | 否(5.6/5.7会重建) | 5.6+/8.0 |
| 加索引 | INPLACE | 是 | 否 | 5.6+ |
| 修改列类型 | COPY | 否 | 是 | 全部版本 |
| 改字符集 | COPY | 否 | 是 | 全部版本 |
注意看表里那一列"是否允许DML"。MySQL 5.6/5.7针对"加列"这种操作,官方文档说是INPLACE,可以支持并发DML,但在实际操作中,它会先在内部做一次全表重建,只是在重建期间允许一部分读请求进来。真正执行的时候你依然能看到Copy to tmp table这样的状态——这是在做全量数据拷贝,只是你这个会话没把DML完全堵死而已。
到了MySQL 8.0,事情才有点改观。8.0.12之后引入了INSTANT算法,只要新列加在表的末尾、不涉及字段顺序调整、没有加索引,它可以在秒级完成,只修改元数据,不触碰数据文件。但前提是你得先升级到8.0,而且用的还是MySQL官方版。如果公司还在跑5.7,这个红利就吃不到。
1.3 一张千万级订单表的DDL耗时估算,以及为什么"就加一列"会引爆线上
有人会觉得:"加个字段而已,怎么可能半小时?"我给你算笔账。假设订单表t_orders有1800万行数据,平均行长1.5KB,那表的大小大约在27GB左右。这里的行长包含所有字段的字节数以及InnoDB内部的一些开销。
如果走COPY算法全量重建:
- 机械盘或普通SSD的写入吞吐按200MB/s算,27GB数据复制一遍需要约138秒,也就是2分多钟;
- 但InnoDB重建不只是复制,还要构建索引、更新数据字典碎片,实际效率通常是理论值的1/3到1/2;
- 再加上在线DDL在复制过程中会不断写入undo日志、redo日志,磁盘IO又被打薄。
所以一台IOPS不到1000的普通云主机上,27GB的表重建跑个15到30分钟,一点都不夸张。而在这半小时里,所有针对这张表的INSERT/UPSERT请求都在等MDL锁释放,连接池的活跃连接全部堆积,很快连接数打满,接着就是雪崩式的超时和报错。
这就是为什么"千万级订单表新增字段"从来不是一个SQL问题,而是一个容量规划问题、一个变更管理问题、一个高可用问题。你不是在改一列数据,你是在动整张表的物理存储结构。
2. 不锁表加字段的三条路线:在线DDL、pt-osc、gh-ost到底怎么选
在动手之前,先把可选方案摊开来看。不同公司的基础设施、数据库版本、业务容忍度不一样,选型结果差别很大,千万别照抄别人的命令。
2.1 路线一:低峰期直接ALTER TABLE——什么时候能用
如果你的订单表就一个几十万行的中小表,或者你已经升级到MySQL 8.0且新列放在表尾,那么直接执行一条带上显式参数的ALTER TABLE是最省事的做法。真的不要为了炫技把所有表都搞成pt-osc,杀鸡用牛刀还容易引入不必要的风险。
举一个可以直接跑的场景:
ALTER TABLE `t_orders` ADD COLUMN `user_remark` VARCHAR(255) NULL DEFAULT NULL COMMENT '用户备注' AFTER `id`, ALGORITHM=INSTANT, LOCK=NONE;重点是显式指定ALGORITHM=INSTANT和LOCK=NONE,让MySQL明确知道你的意图。如果实例版本不支持INSTANT,或者表结构已经不满足INSTANT条件(比如表上存在全文索引或某些特定列类型),MySQL会直接报错,而不是悄悄降级。让它报错,总比让它锁你半小时强。
那什么情况下"低峰期直接ALTER"变得不现实?我归纳三条:
- 表大小超过50GB,重建时间无法压进业务变更窗口;
- 业务7x24小时在线,根本没有"低峰期"这一说;
- 主从架构下,直接在主库执行
ALTER TABLE,DDL会同步阻塞从库的SQL线程,造成主从延迟飚升。
尤其第三条很多人会忽略。你以为在主库执行DDL只有主库忙,实际上在binlog复制模式下,从库重放DDL时同样要走一遍表重建,从库的复制会卡很久,导致主从延迟不可控。高延迟期间如果主库突然宕机,你的恢复RPO/RTO会很难看。所以大表DDL有时候不是让人害怕锁主库,而是害怕拖垮整个复制链路。
2.2 路线二:pt-online-schema-change——靠触发器同步增量
pt-osc是Percona Toolkit里最出名的工具之一,线上大表DDL的第一选择。我早期给订单表加字段,基本都是靠它活下来。
它的核心思路是"影子表+触发器":
- 根据原表结构创建一张新表,在新表上执行你要做的
ALTER TABLE; - 在原表上创建三个触发器,分别捕获
INSERT、UPDATE、DELETE,把增量变更实时同步到新表; - 按主键范围分批把原表历史数据拷贝到新表,每批之间sleep一小段时间,降低对磁盘IO的冲击;
- 所有数据拷贝完毕后,对原表加锁,做最后一次增量同步,然后执行
RENAME TABLE原子地切换原表和新表; - 删除原表和触发器。
这个方案的最大优势是:原表在整个过程中一直是可读可写的,DML操作被触发器转发一份到新表,业务无感知。代价则是触发器带来了双写放大效应——原来是一条写入,现在是原表+新表各写一份,磁盘IO和CPU开销都会上升。
触发器本身也可能成为性能瓶颈。我之前在一张高频更新的订单表上用pt-osc,UPDATE语句多的时候,触发器里的逻辑会拖慢主线事务,高峰期延迟感特别明显。所以用pt-osc,必须配合流量控制和观察窗口,这个后面实操部分我详细讲。
2.3 路线三:gh-ost——无触发器的Binlog回放方案
gh-ost的全称是GitHub's Online Schema Migration,是GitHub开源的无触发器在线DDL工具。和pt-osc最大的不同在于,它不依赖触发器,而是通过解析Binlog来捕获增量变更。
它的数据流是:
- 生成一张影子表,执行结构变更;
- 从主库或从库拉取Binary Log,解析出所有针对原表的变更事件;
- 在拷贝历史数据的同时,把Binlog增量事件持续回放到影子表上;
- 最后申请短暂的锁,切换表名。
无触发器意味着没有双写放大,对原表性能的干扰更小。但这里有一个硬性门槛:binlog必须开启且格式为ROW。如果你们的实例因为历史原因用的是binlog_format=STATEMENT或者MIXED,gh-ost是玩不转的。
另外gh-ost默认会读写Binlog服务,对Binlog的存储和网络都有额外压力。我见过有人在主库网络带宽比较紧张的环境里强上gh-ost,结果Binlog拉取速度跟不上产生速度,工具自己报错退出。所以它更适合对Binlog体系有良好基础的环境。
2.4 三条路线的选型对比:风险、速度、依赖条件
直接给你一张对比表,照着选就行:
| 维度 | 直接ALTER TABLE | pt-osc | gh-ost |
|---|---|---|---|
| 是否需要重建表 | 是(8.0 INSTANT除外) | 是 | 是 |
| 是否阻塞写操作 | 会(短则分钟级,长则半小时+) | 否 | 否 |
| 额外写入放大 | 无 | 有(触发器双写) | 无 |
| 依赖条件 | 版本支持INSTANT/INPLACE | 无强依赖 | binlog_format=ROW |
| 对主库压力 | 高(重建IO密集) | 中等(受chunk-size调节) | 中低(受Sleep/threads调节) |
| 主从延迟风险 | 高 | 中 | 中 |
| 推荐场景 | 表小/8.0版本/可空窗 | 5.6/5.7大表通用方案 | 对触发器敏感的环境 |
我的习惯是:5.6/5.7的千万级大表一律走pt-osc,把--max-lag和--critical-load调保守一点;8.0环境优先考察INSTANT条件;gh-ost用在那些触发器、或者触发器会拖累业务的表上。工具是死的,你才是那个做决策的人。
3. 实战:用pt-online-schema-change给千万级订单表加列的全流程
下面这段以一张真实的订单表为例,跑一遍完整的pt-osc流程。表名用t_orders,目标是在status字段后面加一个user_remark列。
3.1 动手前的检查清单:磁盘、主从、超时参数一个都不能少
我在生产环境执行任何在线DDL之前,都会先跑一遍下面的检查项,缺一个都不敢动手。这不是流程化表演,每一个检查项背后都有真实的事故:
- 磁盘空间:检查数据目录的剩余空间,至少要有目标表当前大小的1.5倍。pt-osc会创建影子表,影子表在拷贝过程中不断变大,峰值时几乎等于原表大小;同时Binlog、临时文件也在膨胀。我在4.1会具体讲磁盘怎么爆的。
- 主从状态:确认所有从库SQL线程正常,
Seconds_Behind_Master接近0。如果主从本来就有延迟,一遍执行下去延迟只会更夸张,而且影子表的变更也会传到从库,加重负担。 - binlog与配置:
binlog_format=RBT(实际是ROW)、binlog_row_image=FULL。pt-osc创建触发器同步增量数据,如果Row格式没有记录全字段,某些场景下同步不完整。 - 外键与触发器:用
SHOW CREATE TABLE看是否已经有触发器,用information_schema.KEY_COLUMN_USAGE查外键依赖。pt-osc不是没办法处理外键,但默认遇到就直接拒绝,需要额外参数,风险也高。有外键的表,我通常建议走gh-ost或者先和外键业务方确认。 - 超时参数:会话级别的
lock_wait_timeout建议改大。原表如果正被一个长事务占用,MDL锁拿不到,pt-osc会在等待超时后直接放弃。我一般设成3600秒起步。
检查下大概长这样:
-- 1. 看表结构和触发器 SHOW CREATE TABLE `t_orders`; SELECT TRIGGER_NAME FROM information_schema.TRIGGERS WHERE EVENT_OBJECT_TABLE = 't_orders'; -- 2. 看外键 SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE REFERENCED_TABLE_NAME = 't_orders'; -- 3. 看磁盘 df -h /var/lib/mysql这套检查跑完,基本能过滤掉80%的潜在事故。
3.2 命令怎么写:核心参数逐一拆解
检查没问题,就开始写执行命令。我给你一条完整的生产可用命令,再逐段解释关键参数:
pt-online-schema-change \ --user=admin \ --password=xxxxx \ --host=127.0.0.1 \ --alter="ADD COLUMN user_remark VARCHAR(255) NULL DEFAULT NULL COMMENT '用户备注' AFTER status" \ --charset=utf8mb4 \ --max-lag=10 \ --check-interval=5 \ --chunk-size=1000 \ --critical-load="Threads_running=50" \ --max-load="Threads_running=30" \ --recursion-method=hostname \ --no-check-replication-filters \ --execute \ D=testdb,t=t_orders几个核心参数的作用:
--alter:只写"从当前结构演进到目标结构"的那部分,pt-osc会在影子表上执行这句SQL。注意不要带ALTER TABLE前缀,也不要加ALGORITHM参数。--chunk-size=1000:每批拷贝的行数。行数太少会导致拷贝进度太慢,行数太多单批事务太大,锁持有时间变长。1000是一个比较稳的起步值,如果IO好可以调到2000-5000,如果IO差就降。--max-lag=10:压测从库的复制延迟上限,超过10秒工具会自动放慢拷贝节奏。这个参数是给自己留后路的,必须设。--critical-load="Threads_running=50":当数据库中活跃线程数超过50时直接暂停工具,防止拖垮业务。很多DBA会忽略,我建议一定写。--recursion-method=hostname:pt-osc需要探测从库列表来控制延迟,这里按照hostname特征匹配从库。如果从库用了不同域名规则,需要改成processlist或dsn方式,否则探测不到从库,--max-lag就是摆设。--no-check-replication-filters:跳过复制过滤器的检查,这个参数风险略高,只有在你自己确认主从复制没有过滤规则时才建议加。--execute:正式执行。不加这个参数的话,工具只做安全检查并打印将要执行的SQL,不会真的跑,相当于dry-run模式。我第一次跑这个工具时一定先dry-run一遍,看看打印出来的SQL和预想是否一致。
这里要特别说一句:第一次用pt-osc,建议先在公司预发环境或者克隆的测试实例上跑一遍。工具本身的打印信息很详细,但有些错误只有真跑一遍才会暴露,比如触发器权限不足、从库探测失败、字符集冲突等。你不可能在生产表上边试错边调整,那是在拿订单数据开玩笑。
3.3 执行过程真实长什么样:Copy to tmp table与RENAME切换
执行之后,你会在会话里看到类似这样的日志,我来逐段解释它代表什么:
Operation, tries, wait, tries, wait Copying rows: 1000, 1, 2, 3...工具会反复循环"读取一批主键范围 -> 拷贝到影子表 -> sleep一小段时间 -> 读取下一批",直到全部拷贝完成。
期间你用SHOW PROCESSLIST去观察,会看到两种典型状态:
Copy to tmp table:这是InnoDB在执行批量插入影子表的物理写入阶段,IO消耗最大、最容易让磁盘报警的阶段。Waiting for table metadata lock:这是原表上当前有长事务导致MDL锁获取不到的情况。看到这个状态别慌,先查information_schema.INNODB_TRX看哪个事务在持锁,评估是等还是终止那个长事务。
拷贝完成之后,pt-osc会进入最关键的切换阶段:
- 在原表上加一个短暂的表级写锁;
- 比较原表和影子表当前的数据偏移,把最后一批增量同步过去;
- 执行
RENAME TABLE t_orders TO t_orders_old, t_orders_new TO t_orders,这一步是原子的; - 删除三个触发器,完成。
从RENAME执行开始到结束,整体耗时就几十毫秒。这是整套方案里唯一一个真正的锁表窗口,但因为它极其短暂,业务基本上无感知。只要确保这几十毫秒内没有大批量写操作正好砸上去,就不会造成问题。
3.4 执行过程中的"流量闸门":如何控制节奏而不影响订单写入
很多人以为pt-osc只有起始和结束两个状态,中间完全交给工具自己跑,这是错误的认知。工具内部有非常细的控制逻辑,你要学会用这些逻辑和业务写入"打配合"。
我用的一个经典组合是把--chunk-size和--max-load联动起来。假设业务高峰期每秒写入约5000条订单,我会把--chunk-size稳定在800到1200之间,这样单次拷贝事务的耗时控制在30到50ms,触发器转发增量数据造成的额外延迟几乎不可感知。如果观察到业务的P99写入延迟出现了跳动,我会手动把--chunk-size降到300到500,同时增大--sleep参数,让每批之间的静默时间更长。
-- 如果业务流量上来了,可以临时降低拷贝速度 --sleep=1 --chunk-size=500如果该表上有大量UPDATE类型的操作,触发器执行路径会更长,这时候我会把--chunk-size压到200,宁可迁移跑慢一点,也不要让订单写入的RT出现毛刺。整个迁移跑个3小时还是5小时,对你的价值远远不如"存量用户无感知"这一点重要。
4. 纸上谈兵都是好的,实测中的坑与完整排查链路
命令背得再熟,真上生产还是会遇到各种意外。下面这几个坑我都踩过,每条我都会把"现象 -> 排查 -> 修复"的完整链路写出来,你遇到类似的情况可以照着查。
4.1 第一坑:跑着跑着磁盘满了,为什么影子表会"越吃越胖"
现象:pt-osc执行到40%左右,磁盘使用率从60%直接飙到93%,然后工具报No space left on device退出。
排查链路:
- 先用
df -h看数据目录所在分区的使用率,确认是否真的写满了; - 用
du -sh /var/lib/mysql/testdb/t_orders_new.ibd查影子表的物理文件大小,你会发现它几乎和原表一样大; - 继续查
du -sh /var/lib/mysql/undo_*,如果看到undo日志也在膨胀,那说明拷贝期间有大量事务反复更新了同一批行,导致undo版本链暴涨。
根因:影子表是完全复制原表结构的新表,拷贝过程中体积接近原表。如果原表27GB,你至少得预留27GB的额外空间。再加上undo日志、Binlog的增量,预留空间按原表1.5到2倍来规划一点都不夸张。
修复方案:
- 如果磁盘还差一口气,先清理无用的Binlog和慢日志占位,见缝插针;
- 调小
--chunk-size,减少单批事务量,能够显著降低redo/undo的写入速率; - 如果实在撑不住,允许工具停在那里等运维扩容,扩容完成后再用相同命令继续跑。pt-osc不是一次性的事务,它支持断点续跑,工具会基于影子表当前的状态继续拷贝。
这里提醒一句:跑大表迁移前,把数据目录所在磁盘监控的告警阈值从90%调到70%。等你看到90%告警再处理,手慢一点就等于眼睁睁看着目标表被写满,那种无力感我不想你再体验。
4.2 第二坑:主从延迟持续拉高,从库复制线程追不上
现象:pt-osc在跑,主库业务也没明显抖动,但SHOW SLAVE STATUS里Seconds_Behind_Master从0一路涨到300秒,甚至更高。
排查链路:
- 先区分是哪个环节慢了:用
SHOW SLAVE STATUS\G看SQL_Running_State字段,如果显示Waiting for dependent transaction to commit或者Copy to tmp table on a replica,基本可以判定是影子表的DML在从库上重放时也需要写一份,加重了从库IO负担; - 看从库的
top,确认磁盘IO是否已经在95%以上。触发器在从库一样生效,所以主库的每条变更到从库都要执行两遍,这个放大效应在主从环境中被进一步放大; - 确认是不是
--max-lag没有真正生效。如果--recursion-method=hostname匹配不到从库,工具就探测不到延迟,等于这台从库的延迟没有进入控制闭环。
修复方案:
- 确认
--recursion-method=hostname配置正确,确保工具可以感知从库延迟; - 降低
--chunk-size到500以下,给从库的SQL线程喘息空间; - 调大
--max-lag的容忍范围,让工具在延迟达到之前就主动放慢甚至暂停拷贝; - 如果延迟已经很高且追不上,考虑直接
kill掉pt-osc进程,让主库先行恢复,之后用同一个命令重新续跑。影子表还在,不会被删除,重启后会接着拷贝。
记住一个原则:分配给迁移的IO资源是有限的,它会和正常业务抢资源。你的迁移节奏要把从库延迟当作第一信号源,而不是等到业务告警了才去处理。
4.3 第三坑:触发器冲突与外键依赖,工具直接拒绝执行
现象:pt-osc一启动就报类似于下面这样的错误:
Table `testdb`.`t_orders` has triggers on it. The --preserve-triggers option must be specified.或者:
Child table(s) referencing this table: t_order_items. Foreign key constraints may be in the way.排查链路:
- 用
SHOW TRIGGERS LIKE 't_orders'确认表上是否已经有业务触发器; - 用
information_schema.KEY_COLUMN_USAGE查清楚外键来自哪张表、是哪种级别(CASCADE/RESTRICT); - 判断这些触发器/外键是否是迁移链路上的硬依赖。
这里掉进过一个比较深的坑:订单表上有一个记操作日志的触发器,业务依赖它把每次订单状态变更写入审计表。pt-osc默认不允许有触发器的表做迁移,因为它自己也要建触发器,两个触发器名字可能冲突。我当时图省事,直接在命令里加了--preserve-triggers,结果迁移完成后,原触发器和pt-osc创建的触发器叠加在一起,业务日志双写了一份,审计数据直接膨胀。
正确做法是:
- 先和应用团队确认触发器的功能,看能否在迁移窗口内先手工接管,然后删除原触发器;
- 让pt-osc带着自己的触发器完成迁移;
- 迁移完成后再重建业务触发器。
外键的处理逻辑类似。如果订单表被t_order_items通过order_id外键引用,并且存在级联删除,那么核心风险在于RENAME TABLE瞬间外键约束会被短暂断开,因为这相当于原表被改了个名,子表的外键定义会临时找不到父表。pt-osc提供了--alter-foreign-keys-method=auto来控制这个处理流程,但对高规格外键依赖的表,我再怎么也不太推荐直接上工具,更稳妥的选择是先评估外键是否可以去掉重建,或者换gh-ost来跑。
4.4 迁移完成后的验证:不是RENAME成功就万事大吉
很多人以为看到RENAME TABLE跑完就结束了,其实真正要花的功夫在验证阶段。我做在线DDL的验证分三层,一层比一层严格:
第一层:结构验证。对比新旧表结构,确认新增列、默认值、注释、索引都符合预期。
SHOW CREATE TABLE `t_orders`;第二层:数据行数验证。统计新旧表的行数是否一致,但千万级表用COUNT(*)会非常慢。正确做法是取各自表的自增ID范围和information_schema.TABLES中的TABLE_ROWS估算值,或者用CHECKSUM TABLE做快速比对。pt-osc在切换前已经做过一次基于主键范围的行数校验,但迁移后依然建议人工抽查几段关键数据。
第三层:灰度放量验证。别让全量流量立刻打到新表上,先让一部分读流量或者内部报表先查几天,确认无查询异常、无慢sql、无业务方反馈后,再把老表t_orders_old清理掉。清理老表也是门学问,不要直接DROP TABLE一把梭,它占着几十GB磁盘,如果删除中途崩溃,重启时InnoDB要回滚那个大DROP操作,极其难受。你可以用:
-- 确认业务无依赖后,分成两步,先改个名再慢慢删 RENAME TABLE t_orders_old TO t_orders_dropme; -- 如果你用的是5.6以上,可以开instant drop table把删除变成元数据级 ALTER TABLE t_orders_dropme FLUSH, DROP TABLE t_orders_dropme;如果磁盘空间紧张,也可以把老表RENAME到其他库,再在低峰期执行DROP。因为老表已经不再有任何引用,拖几天再删也不影响业务,但能让你有个后悔药可以吃。
跑完这次订单表加字段,我最大的体会是:千万级表上的任何DDL都不应该当成"一条SQL"去考虑,而要当作一个完整的小项目——有选型论证、有前置检查、有执行窗口、有验证回滚。工具只是执行载体,真正让线上安稳度过的,是你对锁原理的理解、对参数的精调、以及在意外来临时能不能冷静地按链路排查。
最后分享一个小技巧:每次做这类大表变更,我都会把pt-online-schema-change的dry-run输出完整保存下来,连同当天的主从状态、磁盘用量一起归档。半年后再遇到类似需求时翻出来对照,很多参数可以直接复用。这个习惯帮我省了不少事,也让你在下次变更时不用从零开始踩坑。