第一次在电商库存系统执行OPTIMIZE TABLE inventory_xxx时,看到"Table does not support optimize, doing recreate + analyze instead"这个提示,我也是一头雾水。后来才发现这是InnoDB存储引擎特有的处理机制——它实际上在执行一个重建表的替代方案。
这里有个关键点:InnoDB的OPTIMIZE TABLE本质上是通过ALTER TABLE重建表结构。我做过测试,对一个包含200万条记录的订单表执行OPTIMIZE,发现MySQL自动转换成了这样的语句:
sql复制ALTER TABLE inventory_xxx ENGINE=InnoDB;
这个操作会新建一个临时表,将原表数据逐行插入,最后替换原表。整个过程会产生额外的redo log和undo log,这也是为什么生产环境需要谨慎操作的原因。有次我在业务高峰期执行这个操作,直接导致磁盘空间暴涨了30%。
我们的库存流水表每天新增50万条记录,同时会删除15天前的数据。这些记录包含多个varchar(255)字段,比如商品规格、操作备注等。当删除变长字段的记录时,InnoDB并不会立即回收空间,而是将这些空间标记为"可复用"。
但问题在于:新插入的记录长度不一定能正好填满这些空隙。比如删除了一个占用100字节的记录,新插入的记录可能只需要60字节,剩下的40字节就变成了碎片。我统计过,一个运行半年的库存表,碎片率能达到25%以上。
通过这个命令可以查看具体碎片情况:
sql复制SELECT table_name,
data_length/1024/1024 as data_mb,
index_length/1024/1024 as index_mb,
data_free/1024/1024 as free_mb,
round(data_free/(data_length+index_length)*100,2) as frag_ratio
FROM information_schema.tables
WHERE table_schema = 'inventory_db';
最直接的方法是使用InnoDB原生支持的ALTER TABLE:
sql复制ALTER TABLE inventory_xxx ENGINE=InnoDB;
这个方案我在测试环境验证过多次,但生产环境使用时要注意:
Percona的pt-online-schema-change是我们现在的主力方案。它的工作原理是:
典型使用命令:
bash复制pt-online-schema-change \
--alter="ENGINE=InnoDB" \
D=inventory_db,t=inventory_xxx \
--execute
这个工具最大的优势是几乎不影响线上业务。我们曾经用它处理过800GB的订单历史表,耗时6小时完成,期间业务完全无感知。
对于特别大的表,我们采用"新建表+数据迁移"的方案:
sql复制-- 创建新表
CREATE TABLE inventory_xxx_new LIKE inventory_xxx;
-- 分批插入数据
INSERT INTO inventory_xxx_new
SELECT * FROM inventory_xxx
WHERE create_time > DATE_SUB(NOW(), INTERVAL 15 DAY);
-- 原子切换
RENAME TABLE inventory_xxx TO inventory_xxx_old,
inventory_xxx_new TO inventory_xxx;
这个方案需要应用层配合处理双写,但可以精确控制迁移的数据范围。
对于时间序列数据,改用分区表是更彻底的解决方案。我们将库存流水表改成了按天分区:
sql复制ALTER TABLE inventory_xxx
PARTITION BY RANGE (TO_DAYS(create_time)) (
PARTITION p_202307 VALUES LESS THAN (TO_DAYS('2023-08-01')),
PARTITION p_202308 VALUES LESS THAN (TO_DAYS('2023-09-01')),
PARTITION p_max VALUES LESS THAN MAXVALUE
);
这样删除旧数据时直接truncate分区,效率极高且不会产生碎片。
最后分享一个我们正在使用的监控脚本,每天检查碎片率超过20%的表:
bash复制#!/bin/bash
mysql -e "SELECT CONCAT(table_schema,'.',table_name) as table_name,
round(data_free/(data_length+index_length)*100,2) as frag_pct
FROM information_schema.tables
WHERE engine='InnoDB'
AND data_free > 100*1024*1024
AND table_schema NOT IN ('mysql','information_schema','performance_schema')
HAVING frag_pct > 20
ORDER BY frag_pct DESC;" > /tmp/frag_report.txt
第一次执行在线DDL时,我踩过一个坑:没有检查磁盘剩余空间。那次操作差点把生产环境的磁盘撑爆。现在我们的标准操作流程是:
对于特别关键的表,我们还会先在从库执行,验证无误后再在主库操作。曾经有个同事直接在主库优化一个核心订单表,导致系统卡顿半小时,这个教训让我们制定了严格的变更流程。
为了验证不同方案的效果,我用测试环境做了组对比实验:
| 方案 | 耗时(100万记录) | 锁表时间 | 空间回收率 |
|---|---|---|---|
| OPTIMIZE TABLE | 42s | 全程锁表 | 68% |
| ALTER TABLE | 38s | 全程锁表 | 72% |
| pt-osc | 6分15秒 | 仅最后0.5秒锁表 | 70% |
| 新建表+迁移 | 4分50秒 | 仅rename时锁表 | 75% |
测试结果显示,虽然在线工具耗时更长,但对业务影响最小。这也是为什么我们现在默认使用pt-osc的原因。