当单表数据量达到百万甚至亿级别时,任何不经规划的 ALTER TABLE 或大量 DELETE 都可能瞬间把数据库拖垮。大表操作的核心理念只有一条:最小化对线上业务的影响,避免长时间锁表和主从延迟。 下面分别从 DDL 变更、大数据量删除、以及通用的表结构变更三个维度给出具体可行的规范。
24.4.1 DDL 变更:如何安全地修改大表结构
在 MySQL 5.6 之前,大部分 DDL 操作(如加列、修改列类型)都会锁表并重建整个表,大表一锁就是几分钟甚至几小时,业务直接停摆。从 5.6 开始 InnoDB 支持在线 DDL(Online DDL),允许在变更过程中继续读写,但具体支持程度和操作有关。
1. 优先选择 MySQL 8.0 的 INSTANT DDL
如果你的数据库版本是 MySQL 8.0.12 及以上,部分 DDL 操作可以通过 ALGORITHM=INSTANT 完成,例如添加新的普通列、设置默认值、修改索引可见性等。它只更改数据字典,不触及行数据,毫秒级完成,完全无锁。
ALTER TABLE order_info ADD COLUMN discount_type tinyint DEFAULT 0, ALGORITHM=INSTANT;
执行前务必通过 EXPLAIN 检查是否支持,或者尝试执行,MySQL 会直接报错提示。
2. 利用 Online DDL,但必须控制好并发
对于仍需复制表数据的操作(如扩展 VARCHAR 长度、增加主键等),可以使用 ALGORITHM=INPLACE 并在低峰期执行。主要的坑在于它会持有元数据锁(MDL),如果此时有长事务未提交,DDL 会被阻塞,进而阻塞后续所有请求。执行前务必:
- 检查
information_schema.INNODB_TRX确保没有长时间未提交的事务。 - 设置
lock_wait_timeout很短的超时,避免 DDL 无限等待。 - 使用
ALGORITHM=INPLACE, LOCK=NONE尽量允许并发读写。
3. 推荐使用 pt-online-schema-change(Percona Toolkit)
对于需要全表拷贝且对稳定性要求极高的环境(尤其是 5.7 及更早版本),公认最安全的方案是使用 pt-online-schema-change。它的原理是创建一张新表,在旧表上创建触发器同步增量数据,分批拷贝数据,最后原子性切换表名。整个过程对业务几乎透明,且可以随时暂停。
pt-online-schema-change \
--alter "ADD COLUMN remark varchar(200) DEFAULT NULL" \
D=order_db,t=order_info \
--execute \
--max-load Threads_running=30 \
--critical-load Threads_running=50 \
--chunk-size 5000
必须监控负载,一旦超过阈值工具会自动暂停。切换表名时会有极短暂的正确性窗口,但完全可以接受。
4. 禁止直接在生产环境执行 DDL 的五条铁律
- 禁止在业务高峰期执行任何可能锁表的 DDL。
- 禁止在不了解 Online DDL 支持程度的情况下直接
ALTER TABLE。 - 禁止不检查钉钉/监控就执行大表 DDL。
- 禁止对正在被大量访问的表执行
OPTIMIZE TABLE(它会重建表并锁表)。 - 如果可能,优先在从库先执行一次,验证脚本和影响时间。
24.4.2 大数据量删除:避免跑死数据库的清理策略
一次性删除几百万或几千万行数据,会瞬间产生巨大的 Undo Log、Redo Log、主从延迟,甚至把 Buffer Pool 的热点数据全部冲掉,导致整体性能雪崩。
规范做法只有两个字:分批。
1. 基于主键分批删除(最稳妥)
DELETE FROM order_log WHERE create_time < '2023-01-01' AND id BETWEEN 1 AND 10000;
每次删除几千到一万条,中间 sleep 几百毫秒,循环进行。可以用脚本或存储过程实现:
REPEAT
DELETE FROM order_log WHERE create_time < '2023-01-01' LIMIT 1000;
SELECT ROW_COUNT() INTO @deleted;
DO SLEEP(0.5);
UNTIL @deleted = 0 END REPEAT;
务必注意:如果删除条件不能走到索引,LIMIT 同样会扫描大量无用数据,因此必须确保 WHERE 条件有合适索引。
2. 利用 BETWEEN 分割主键区间
-- 先获取最大最小主键
SELECT MIN(id), MAX(id) FROM order_log;
-- 每 10000 条删除一批
DELETE FROM order_log WHERE id BETWEEN 110000 AND 120000;
3. 使用分区表实现瞬间清理
如果已经用了分区表(按日期 RANGE 分区),可以直接 TRUNCATE PARTITION 或 DROP PARTITION,毫秒级完成,不产生任何 Undo Log 负担。这是大日志表设计时的最佳选择。
4. 特殊场景:保留少量数据,删掉大部分
可以先将需要保留的数据 INSERT INTO ... SELECT 到新表,再 RENAME TABLE 交换,最后 DROP 旧表。比直接 DELETE 快得多,但要注意停机窗口和主从同步逻辑。
5. 绝对不能做的事
- 绝对不要
DELETE FROM huge_table WHERE ...不加 LIMIT。 - 不要在删除后立即执行
ALTER TABLE,Undo 还没清理完,磁盘空间也不会立即释放。 - 删除后空间不会自动回收(InnoDB 会标记为可重用),必须等后续插入填充,或者执行
OPTIMIZE TABLE回收磁盘,但大表慎重执行。
24.4.3 表结构变更:修改字段类型、添加/修改索引
1. 修改字段类型
- 扩大 VARCHAR 长度:在 8.0 中某些情况下可以用 INSTANT,但 5.7 需要 COPY 整表。对于大表,强烈建议使用
pt-online-schema-change。 - 修改字段类型(如 INT 转 BIGINT):必定要重建表。如果该列涉及外键或索引,成本更高。需要提前规划好维护窗口,并充分测试执行时间。
- 设置默认值:8.0 可以 INSTANT,5.7 也需要 COPY。小改动也尽量用 pt-osc。
2. 添加/删除索引
- 创建索引:从 MySQL 5.6 起,创建索引支持 Online DDL(
ALGORITHM=INPLACE, LOCK=NONE),但索引构建过程中仍会消耗大量服务器 I/O 和 CPU 资源,可能拖慢主库。必要时可以在从库先创建,然后主从切换,或者使用pt-online-schema-change的--alter参数添加(它创建新表时会自动创建索引)。 - 删除索引:删除索引通常是即时操作(
ALGORITHM=INPLACE),影响极小。但务必确认索引不再需要,可以先用ALTER TABLE ... ALTER INDEX idx_name INVISIBLE(8.0 支持)观察几天,无影响后再彻底删除。
3. 表拆分重构
若需要水平或垂直拆分表,通常涉及数据迁移,必须通过脚本逐步搬移,配合双写验证。这类操作不是单纯的 DDL,而是一项数据迁移工程,规范要点包括:
- 灰度切换,先双写新旧表,确保数据一致。
- 读流量逐步切到新表,观察性能。
- 最终移除旧表。
24.4.4 大表操作通用检查清单
执行任何大表操作前,请逐项确认:
- [ ] 操作已在从库或等比例测试环境执行一遍,并记录了耗时。
- [ ] 确认当前 MySQL 版本对操作的支持级别(INSTANT / INPLACE / COPY)。
- [ ] 检查长事务和未提交的会话(
SHOW PROCESSLIST+INNODB_TRX)。 - [ ] 确认磁盘剩余空间足够(至少为表大小的 1.5 倍),防止重建表导致磁盘写满。
- [ ] 已关闭
foreign_key_checks和unique_checks(如果是在维护窗口内单独操作,可以临时关闭以加速,但必须注意数据一致性)。 - [ ] 开启慢查询日志记录阈值调低,便于及时发现异常。
- [ ] 通知相关方,做好回滚预案(如果是表结构变更可回退到备份结构)。
- [ ] 对于大数据量删除,预先评估主从延迟与磁盘 IO 压力。
大表没有银弹,但分批、利用在线工具、提前验证、控制并发这十二条真经足以让你安全度过 90% 的险境。牢记一点:当你觉得操作可能太快时,先慢下来,让数据库慢慢完成工作,就赢了一大半。