MySQL 存储空间与索引运维实战:从空间排查到在线扩容
适用版本:MySQL 5.7 / 8.0(InnoDB)
读者对象:DBA、运维、后端同学
核心结论先放在这里:库空间看磁盘和
information_schema;索引不会整棵装进内存;生产扩盘通常不用重启 MySQL;OPTIMIZE TABLE是整表重建,大表上很慢。
线上磁盘告急时,最常见的三个误判是:
- 以为「删了数据,文件就会变小」
- 以为「索引会常驻内存,所以索引越大查询越快」
- 看到
DATA_LENGTH ≈ INDEX_LENGTH就觉得统计坏了,准备整库OPTIMIZE
这三项里,真正该先做的永远是同一件事:把空间账算清,再决定扩盘、删索引,还是重建表。
文章目录
- [MySQL 存储空间与索引运维实战:从空间排查到在线扩容](#MySQL 存储空间与索引运维实战:从空间排查到在线扩容)
-
- [1. 运维视角:空间问题分三层](#1. 运维视角:空间问题分三层)
- [2. 怎么看某个库、某张表占了多少空间](#2. 怎么看某个库、某张表占了多少空间)
-
- [2.1 某个库一共多大](#2.1 某个库一共多大)
- [2.2 这个库里谁最大](#2.2 这个库里谁最大)
- [2.3 对比所有业务库](#2.3 对比所有业务库)
- [2.4 和磁盘对账](#2.4 和磁盘对账)
- [3. 怎么看索引占用的空间](#3. 怎么看索引占用的空间)
-
- [3.1 按表看:二级索引合计](#3.1 按表看:二级索引合计)
- [3.2 按「单个索引」估算(MySQL 8.0+)](#3.2 按「单个索引」估算(MySQL 8.0+))
- [3.3 和 MyISAM 不要混着理解](#3.3 和 MyISAM 不要混着理解)
- [4. 数据和索引差不多大,正常吗](#4. 数据和索引差不多大,正常吗)
-
- [4.1 为什么二级索引可以长到这个份上](#4.1 为什么二级索引可以长到这个份上)
- [4.2 比值怎么读](#4.2 比值怎么读)
- [4.3 先确认索引有没有人用](#4.3 先确认索引有没有人用)
- [5. 索引加载内存的真实原则](#5. 索引加载内存的真实原则)
-
- [5.1 按页加载,不是整棵树](#5.1 按页加载,不是整棵树)
- [5.2 按需加载(lazy load)](#5.2 按需加载(lazy load))
- [5.3 LRU:热页留下,冷页淘汰](#5.3 LRU:热页留下,冷页淘汰)
- [5.4 容易混淆的几个机制](#5.4 容易混淆的几个机制)
- [5.5 运维含义](#5.5 运维含义)
- [6. 生产环境存储是动态扩容的吗,要不要重启](#6. 生产环境存储是动态扩容的吗,要不要重启)
-
- [6.1 结论](#6.1 结论)
- [6.2 磁盘层:生产最常见的做法](#6.2 磁盘层:生产最常见的做法)
- [6.3 MySQL 自己:文件会自动涨,但不会自动加磁盘](#6.3 MySQL 自己:文件会自动涨,但不会自动加磁盘)
- [6.4 什么时候还是要停机](#6.4 什么时候还是要停机)
- [6.5 扩盘现场的两个坑](#6.5 扩盘现场的两个坑)
- [7. OPTIMIZE TABLE 整个过程是不是很慢](#7. OPTIMIZE TABLE 整个过程是不是很慢)
-
- [7.1 它实际在干什么](#7.1 它实际在干什么)
- [7.2 会慢到什么程度](#7.2 会慢到什么程度)
- [7.3 会不会锁表](#7.3 会不会锁表)
- [7.4 什么时候值得做](#7.4 什么时候值得做)
- [7.5 生产上更稳妥的做法](#7.5 生产上更稳妥的做法)
- [8. 生产处置顺序:先观察,再动手](#8. 生产处置顺序:先观察,再动手)
- [9. 监控与容量规划清单](#9. 监控与容量规划清单)
- [10. 速查 SQL 附录](#10. 速查 SQL 附录)
- [11. 总结](#11. 总结)
1. 运维视角:空间问题分三层
MySQL 的「占用空间」经常被混成一句话。生产上必须拆开:
| 层级 | 你真正看到的 | 谁负责变大 / 变小 |
|---|---|---|
| 磁盘 / 云盘 | df、云监控、PVC |
基础设施。MySQL 不会自己变出一块新盘 |
| 表空间文件 | datadir 下的 .ibd、redo、binlog |
InnoDB 随写入 自动涨 ;删除数据 通常不自动缩 |
| 逻辑统计 | DATA_LENGTH / INDEX_LENGTH |
元数据估算,用来对比和定位,不是精确到字节的计费账单 |
三层对不上,是日常排障的常态:
- 控制台刚扩完盘,
df还是旧值 ------ 文件系统没resize information_schema显示表不大,磁盘却满了 ------ binlog、临时文件、undo、备份打满了另一条路径DELETE了 50% 行,.ibd几乎没变 ------ InnoDB 只是把页标成可复用,没有还给操作系统
原则:先 df 和数据目录,再看库表统计,最后才谈 OPTIMIZE 和删索引。
2. 怎么看某个库、某张表占了多少空间
2.1 某个库一共多大
sql
SELECT
TABLE_SCHEMA AS db_name,
COUNT(*) AS table_cnt,
ROUND(SUM(DATA_LENGTH) / 1024 / 1024, 2) AS data_mb,
ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) AS index_mb,
ROUND(SUM(DATA_FREE) / 1024 / 1024, 2) AS free_mb,
ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS used_mb,
ROUND(SUM(DATA_LENGTH + INDEX_LENGTH + DATA_FREE) / 1024 / 1024, 2) AS total_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db';
| 字段 | 含义 | 运维上怎么用 |
|---|---|---|
data_mb |
数据(InnoDB 含主键聚簇索引) | 行数据的主体 |
index_mb |
二级索引合计 | 不含主键 |
free_mb |
表内可复用空洞 / 预分配未用 | 高了才考虑重建收缩 |
used_mb |
数据 + 二级索引 | 日常对比用这个 |
total_mb |
含碎片 | 更接近「这张表文件大概多重」 |
InnoDB 的 TABLE_ROWS、DATA_LENGTH 是 估算值。量级判断够用;要对账到文件,看磁盘。
2.2 这个库里谁最大
sql
SELECT
TABLE_NAME,
ENGINE,
TABLE_ROWS,
ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,
ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb,
ROUND(DATA_FREE / 1024 / 1024, 2) AS free_mb,
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mb,
ROUND(INDEX_LENGTH / GREATEST(DATA_LENGTH, 1), 2) AS idx_data_ratio
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db'
ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC
LIMIT 30;
生产里 80% 的空间问题,都能在前 10 张表里定位完。不要一上来扫全实例做 DDL。
2.3 对比所有业务库
sql
SELECT
TABLE_SCHEMA AS db_name,
COUNT(*) AS table_cnt,
ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS used_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')
GROUP BY TABLE_SCHEMA
ORDER BY SUM(DATA_LENGTH + INDEX_LENGTH) DESC;
2.4 和磁盘对账
sql
SHOW VARIABLES LIKE 'datadir';
独立表空间(innodb_file_per_table=ON,现在默认就是)时,库目录可以直接量:
bash
du -sh /var/lib/mysql/your_db
du -sh /var/lib/mysql/your_db/* | sort -h | tail -20
对账时把这些也算进去,它们经常比业务表更先把盘写满:
| 路径 / 文件 | 典型内容 |
|---|---|
ib_logfile* / redo |
刷盘延迟、大事务会顶高 |
binlog.* |
保留天数过大是第一杀手 |
ibtmp* |
磁盘临时表 |
undo 表空间 |
长事务导致膨胀 |
| 备份目录 | 逻辑备份和数据目录放同一块盘 |
单表也可以:
sql
SHOW TABLE STATUS FROM your_db LIKE 'your_table'\G
看 Data_length、Index_length、Data_free。
3. 怎么看索引占用的空间
3.1 按表看:二级索引合计
INDEX_LENGTH 就是该表 二级索引 占用的空间。InnoDB 主键一般算在数据里,不单独计入 这一项。
sql
SELECT
TABLE_SCHEMA AS db_name,
TABLE_NAME AS table_name,
ROUND(DATA_LENGTH / 1024 / 1024, 2) AS data_mb,
ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS index_mb,
ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS total_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db'
ORDER BY INDEX_LENGTH DESC;
看某个库全部索引一共多大:
sql
SELECT
TABLE_SCHEMA,
ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) AS index_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db'
GROUP BY TABLE_SCHEMA;
3.2 按「单个索引」估算(MySQL 8.0+)
INDEX_LENGTH 给不出「idx_user_ctime 到底多少 GB」。InnoDB 统计表可以估:
sql
SELECT
s.database_name,
s.table_name,
s.index_name,
ROUND(SUM(s.stat_value * @@innodb_page_size) / 1024 / 1024, 2) AS index_mb
FROM mysql.innodb_index_stats s
WHERE s.database_name = 'your_db'
AND s.stat_name = 'size'
GROUP BY s.database_name, s.table_name, s.index_name
ORDER BY index_mb DESC
LIMIT 50;
stat_name = 'size':该索引占用的页数- 乘
innodb_page_size(默认 16KB)≈ 估算大小 PRIMARY是聚簇索引,和行数据是同一棵树,不要把它和二级索引重复加总去跟INDEX_LENGTH对
看某个索引:
sql
SELECT
index_name,
ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) AS index_mb
FROM mysql.innodb_index_stats
WHERE database_name = 'your_db'
AND table_name = 'your_table'
AND index_name = 'idx_xxx'
AND stat_name = 'size';
统计可能滞后。要更接近当前值:
sql
ANALYZE TABLE your_db.your_table;
生产大表上 ANALYZE 也会吃 IO,放到低峰。
3.3 和 MyISAM 不要混着理解
| 引擎 | INDEX_LENGTH |
主键怎么算 |
|---|---|---|
| InnoDB | 主要是二级索引 | 主键在 DATA_LENGTH |
| MyISAM | 更接近真实 .MYI 文件 |
数据和索引分文件 |
现在生产几乎都是 InnoDB。用 MyISAM 的心智模型去读 InnoDB 的两个 LENGTH,会系统性误判。
4. 数据和索引差不多大,正常吗
很常见,不等于统计坏了。
InnoDB 里:
DATA_LENGTH≈ 整行数据 + 主键(聚簇索引)INDEX_LENGTH≈ 所有二级索引之和
两者接近,只说明:二级索引加起来,已经和表数据(含主键)一个量级。
4.1 为什么二级索引可以长到这个份上
二级索引叶子节点存的是 索引列 + 主键值(用来回表)。表上有 N 个二级索引,主键就被重复存 N 份。
特别容易把索引做大的情况:
- 二级索引多(4~8 个常见,再多就容易赶超数据)
- 主键太宽 :UUID、
varchar(36)、多列联合主键。每个二级索引都要带着它 - 索引列很长 :
varchar(255)、姓名+手机+地址这类组合索引 - 联合索引字段过多,几乎覆盖半张表
- 前缀冗余 :有
idx(a,b)又有idx(a) - 行本身很瘦(几个 INT),索引相对就会显得很大
一个直观例子:行 100 字节,主键 36 字节 UUID,3 个二级索引各自再带一份主键,仅索引侧就可能接近甚至超过行数据。
4.2 比值怎么读
用前面的 idx_data_ratio(INDEX_LENGTH / DATA_LENGTH):
| 比值 | 怎么判断 |
|---|---|
| 0.2 ~ 0.8 | 多数 OLTP 表正常 |
| ≈ 1.0 | 二级索引偏多或偏宽,值得拆开看 |
| > 1.5 | 大概率冗余索引、主键过宽,或覆盖索引加列过多 |
先找最大的几张表,再 SHOW INDEX:
sql
SHOW INDEX FROM your_db.your_table;
重点看:列长度、是否重复前缀、主键类型、唯一索引是否多余。
4.3 先确认索引有没有人用
sql
SELECT
object_schema,
object_name,
index_name,
count_star
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'your_db'
AND count_star = 0
AND index_name IS NOT NULL
AND index_name != 'PRIMARY';
count_star = 0 只说明 自实例启动以来没被用到 ,不是「永远没用」。刚重启、刚加的索引、只在月末报表用的索引,都会误伤。把它当成 候选清单,对照慢查询、代码、定时任务后再删。
处理优先级(从收益高、风险低往下):
- 删确认冗余的索引(有
(a,b)且查询都能走它,才考虑去掉单独的(a)) - 主键改为
BIGINT自增,UUID 放普通唯一索引 ------ 能同时缩小 所有 二级索引 - 超长
varchar改为前缀索引或哈希列(必须评估能否覆盖查询) - 未使用索引列入下个变更窗口
不要因为「索引和数据一样大」就整库 OPTIMIZE。 那只会把同样大的索引再重建一遍,磁盘和主从都会痛一次,比值几乎不变。
5. 索引加载内存的真实原则
InnoDB 不会在启动时把整棵索引一次性加载进内存。原则就一句话:
按页按需读入 Buffer Pool,热页留下,冷页按 LRU 淘汰。
5.1 按页加载,不是整棵树
数据和二级索引都按 16KB 页 组成 B+ 树。
- SQL 走到哪条路径,才把哪些页读进来
- 根节点、中间节点访问频繁,更容易常驻
- 叶子页在不在内存,取决于有没有被查到
Buffer Pool 启动后是空的(或从 dump 恢复)。第一次查询才会把相关页读盘。
5.2 按需加载(lazy load)
| 场景 | 进内存的内容 |
|---|---|
| 等值查询走二级索引 | 该索引路径上的内部页 + 对应叶子页,再回表读聚簇索引页 |
| 覆盖索引 | 只读二级索引页,不读数据页 |
| 全表扫描 | 主要读聚簇索引叶子页,二级索引通常不进内存 |
| 范围扫描 | 连续叶子页,可能触发预读 |
建了但没被查询用到的索引,不会自动进内存。 它只在写入时增加维护成本,并在磁盘上占空间。
5.3 LRU:热页留下,冷页淘汰
Buffer Pool 用改进版 LRU(young / old 两段):
- 刚读入的页先放 old 区,避免一次全表扫描把热索引挤掉
- 一段时间后再被访问,才升到 young 区
- 内存不够时,从 LRU 尾部淘汰最久没用的页
所以:常查的索引页会自然留在内存;很少用的索引即使加载过,也会被换出。
对 Buffer Pool 来说,主键和二级索引都是页,没有「索引优先于数据」的单独策略。谁被访问得多,谁就占内存。
5.4 容易混淆的几个机制
Adaptive Hash Index
对反复等值查找的热点页,InnoDB 会在内存里建哈希,把部分 B+ 树查找变成近似 O(1)。这是加速,不是把整棵索引再拷一份;范围扫描用不上。
MyISAM Key Buffer
MyISAM 才更接近「索引专门进内存」:key_buffer_size 只缓存索引,数据靠 OS 页缓存。InnoDB 没有 单独的索引缓存,数据和索引共用 innodb_buffer_pool_size。
Buffer Pool Dump(预热)
sql
SET GLOBAL innodb_buffer_pool_dump_at_shutdown = ON;
SET GLOBAL innodb_buffer_pool_load_at_startup = ON;
关闭时 dump 热点页,启动后再加载。这是把 上次的热页 提前读回来,仍然不是加载全部索引。
5.5 运维含义
- 索引不是「常驻内存」的保证,只是被访问过的页 可能 在内存里
- 想让查询稳,关键是工作集(热数据 + 热索引)能放进 Buffer Pool
innodb_buffer_pool_size通常给机器内存的 50%~70%(本机还跑其他进程就再降)- 索引建太多:浪费磁盘、拖慢写入;不会因为建了就自动占满内存
看 Buffer Pool 里页类型分布(实例很大时这个查询本身也有成本):
sql
SELECT
PAGE_TYPE,
COUNT(*) AS pages,
ROUND(COUNT(*) * @@innodb_page_size / 1024 / 1024, 2) AS mb_est
FROM information_schema.INNODB_BUFFER_PAGE
GROUP BY PAGE_TYPE
ORDER BY pages DESC;
SHOW ENGINE INNODB STATUS 里的 BUFFER POOL AND MEMORY 更适合日常看命中率、脏页、LRU。
6. 生产环境存储是动态扩容的吗,要不要重启
要分开两层:磁盘能不能变大 ,和 MySQL 要不要重启。日常说的「库空间不够了、扩盘」,指的是磁盘容量,不是改 MySQL 参数。
6.1 结论
| 扩的是什么 | 是否动态 | 一般要不要重启 MySQL |
|---|---|---|
| 云盘 / LVM / K8s PVC 把磁盘做大 | 可以在线扩 | 通常不用 |
| RDS「自动扩容存储」 | 可以自动 | 不用 |
| InnoDB 表空间文件变大(数据往里写) | 自动增长 | 不用 |
| 加一块新盘并改数据目录 | 不算热扩容 | 要停机或主从切换 |
改 innodb_buffer_pool_size 等内存参数 |
跟存储无关 | 5.7 常要重启;8.0 部分可在线 |
6.2 磁盘层:生产最常见的做法
MySQL 只是往文件系统里写 .ibd / redo / binlog。空间够不够,取决于底下这块盘。
云主机云盘(阿里云、AWS EBS、腾讯云等)
- 控制台把云盘从 500GB 扩到 1TB,多数支持在线扩容
- 扩完后还要在操作系统里把分区 / 文件系统拉大,例如:
bash
# 示例,具体设备名和文件系统以现场为准
growpart /dev/vda 1
resize2fs /dev/vda1 # ext4
# xfs_growfs /data # xfs
- MySQL 不用重启,新空间马上能写
- 注意:有的「换磁盘类型」(高效云盘 → SSD)可能要卸载盘,那就会中断
LVM
lvextend + resize2fs / xfs_growfs,在线完成,MySQL 不用重启。
K8s PVC
StorageClass 开启 allowVolumeExpansion: true 后可以改大 PVC。扩的是卷,Pod / MySQL 一般不用重启,取决于 CSI 是否支持在线扩。
物理机直连单盘
单块盘写满了,不能「热变大」。换盘、加盘做 LVM/RAID、迁数据目录,通常要停机窗口。
6.3 MySQL 自己:文件会自动涨,但不会自动加磁盘
默认 innodb_file_per_table=ON:
- 每张表一个
.ibd,数据增多文件自动变大,不用重启、不用人工扩表空间 - 前提是 磁盘还有空闲
- 系统表空间若开了
autoextend,同样自动追加、不重启
所以:
- 表空间文件:自动涨
- 磁盘容量:不会因为 MySQL 自动变大 ;盘满就报
No space left on device
自建 MySQL 没有 RDS 那种「使用率超 80% 自动加 100GB」。要靠监控 + 人工扩云盘,或脚本调云厂商 API 再 resize2fs。
托管库(阿里云 RDS、AWS RDS 等)可以把自动扩容打开:设阈值、步长、上限和冷却时间。实例一般不重启,按量计费。
6.4 什么时候还是要停机
这些不是「把盘做大」,而是改存储布局:
- 数据目录从
/data1迁到/data2 - 单盘改成数据 / binlog / redo 分盘
- 不支持在线扩的存储,或更换磁盘类型需要卸载
- 共享表空间改独立表空间等需要重建的操作
扩容本身通常 不用重启 MySQL。重启往往是因为迁盘、换盘类型,或改了需要重启的参数。
6.5 扩盘现场的两个坑
- 控制台加了容量,
df没变 ------ 只扩了云盘,没扩文件系统。这是最高频事故。 - InnoDB 删数据不会立刻把空间还给 OS ------
.ibd可能还很大,需要重建表才会收缩。盘已经 90%,这时去做OPTIMIZE,重建期间还要再占一份表大小,可能直接把盘写爆。
先扩盘,再考虑收缩。永远不要在磁盘剩余不够「一张表大小」时对大表做重建。
7. OPTIMIZE TABLE 整个过程是不是很慢
对大表来说,往往很慢。慢的是「整表重建」,不是扫一下碎片。
7.1 它实际在干什么
InnoDB 里:
sql
OPTIMIZE TABLE t;
基本等价于:
sql
ALTER TABLE t ENGINE=InnoDB;
-- 或 ALTER TABLE t FORCE;
过程大致是:
- 新建临时表(或 inplace 重建)
- 把旧表数据 逐行拷过去,同时重建主键和所有二级索引
- 期间新写入要额外处理(row log / 增量追平)
- 最后用新表替换旧表,释放旧
.ibd
耗时跟 表大小 + 索引数量 成正比,不是跟「删了多少行」成正比。一张 100GB 的表,即使只删了 1% 数据,也要按 100GB 来重建。
7.2 会慢到什么程度
粗经验(SSD、负载不高,仅供量级感,不能当 SLA):
| 表大小 | 常见耗时量级 |
|---|---|
| 几百万行、几百 MB | 秒到一两分钟 |
| 几千万行、几十 GB | 几十分钟到数小时 |
| 上百 GB / 上千 GB | 数小时到过夜,必须单独评估 |
还会更慢:二级索引多、HDD / 低 IOPS 云盘、主库持续写入、从库还要再重放一遍 DDL、表上有大字段。
7.3 会不会锁表
这点比「慢」更关键。
MySQL 5.6+ InnoDB 多数情况下是 online DDL:
- 重建期间 还可以 SELECT / INSERT / UPDATE / DELETE
- 不是整个过程都锁死
- 最后切换瞬间会有 短暂 MDL 锁(秒级,若有长事务可能更久)
- 会多占大约 一份表空间(100GB 表重建时磁盘上可能短暂接近 200GB)
- CPU、IO、Buffer Pool 都会被打高,业务会变慢
所以:不是全程不可用,但是全程很重;结束时有短锁。 有长查询、未提交事务时,最后一下切换会一直等锁,看起来像卡死。
上线前先查:
sql
SELECT * FROM information_schema.INNODB_TRX\G
SHOW PROCESSLIST;
有跑了几小时的事务,先处理事务,再谈 DDL。
7.4 什么时候值得做
先看碎片,而不是凭感觉:
sql
SELECT
TABLE_NAME,
ROUND(DATA_LENGTH/1024/1024, 2) AS data_mb,
ROUND(INDEX_LENGTH/1024/1024, 2) AS index_mb,
ROUND(DATA_FREE/1024/1024, 2) AS free_mb,
ROUND(DATA_FREE / (DATA_LENGTH + INDEX_LENGTH + 1) * 100, 2) AS frag_pct
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db'
ORDER BY DATA_FREE DESC;
经验上:
- 碎片 5%~10%、
DATA_FREE不大:不必优化 - 大批量
DELETE后.ibd明显虚高、磁盘确实紧:才考虑重建 - 小表可以低峰直接
OPTIMIZE - 大表不要在主库裸跑
7.5 生产上更稳妥的做法
pt-online-schema-change/gh-ost:影子表 + 追增量,可限流、可暂停、切换更可控- 从库重建再切换:避免主库扛重建 IO
- 分区表走
DROP PARTITION:按时间删冷数据是秒级还空间,比 OPTIMIZE 合适得多 - 大批量删除后换「新建表 + 导热数据 + RENAME」:和 OPTIMIZE 本质类似,但可以自己分批、限速
DELETE 很快;把磁盘还给操作系统 才是这场重建。这就是为什么大家会觉得 OPTIMIZE 特别慢。
8. 生产处置顺序:先观察,再动手
磁盘告警时,建议按这个顺序,而不是直接 OPTIMIZE。
1. df / 云监控:哪块盘、使用率、inode
2. du 数据目录:.ibd、binlog、临时文件、undo、备份谁最大
3. information_schema:哪个库、哪张表、数据 vs 索引 vs DATA_FREE
4. innodb_index_stats:是某几个宽索引,还是整表数据
5. 判断动作:
- binlog 过多 → 立刻:调 expire_logs_days / binlog_expire_logs_seconds
- 磁盘真不够 → 先在线扩盘 + resize 文件系统
- 碎片极大且磁盘已有余量 → 低峰重建(工具 / 从库)
- 索引比数据还大 → 审索引和主键,不要先 OPTIMIZE
- 长事务顶着 undo → 先杀/等事务,收缩是后话
一句话对照:
| 现象 | 优先动作 | 错误动作 |
|---|---|---|
df 90%,binlog 占一半 |
缩短 binlog 保留 | OPTIMIZE 业务表 |
| 数据和索引 1:1 | 审冗余索引 / 主键宽度 | 整库重建 |
DELETE 后文件不缩 |
先扩盘,再择机重建大表 | 磁盘 5% 剩余时开 OPTIMIZE |
| 查询变慢、Buffer Pool 命中低 | 加内存 / 缩小工作集 | 幻想「把所有索引 load 进内存」 |
云盘已扩,df 不变 |
growpart + resize2fs |
重启 MySQL |
9. 监控与容量规划清单
建议至少覆盖这些指标,告警不要只看「磁盘 80%」一条。
| 指标 | 为什么要看 |
|---|---|
| 数据盘使用率、inode | 容量和「文件数打满」是两类故障 |
| binlog 目录使用率 | 经常独立把盘写满 |
各库 used_mb、Top 表 total_mb |
知道增长来自哪张表 |
idx_data_ratio Top N |
索引膨胀早期发现 |
| Buffer Pool 命中率、脏页、pending reads | 内存工作集是否放得下 |
| 长事务时长、undo 大小 | 空间膨胀的隐形来源 |
| 从库延迟 | DDL / 重建期间的健康度 |
容量规划经验:
- 云盘不要等到 90% 再扩。大表重建、加索引、
ALTER都可能短期再吃 1 倍表大小 - 数据、binlog、备份尽量分盘或分桶
- 增长曲线按「最大表」外推,不要按整库平均值
- 能分区的日志表、流水表,优先分区,把「还空间」从 OPTIMIZE 变成
DROP PARTITION
10. 速查 SQL 附录
把 your_db / your_table 换成现场对象即可。
sql
-- A. 某库空间总览
SELECT
TABLE_SCHEMA AS db_name,
ROUND(SUM(DATA_LENGTH)/1024/1024, 2) AS data_mb,
ROUND(SUM(INDEX_LENGTH)/1024/1024, 2) AS index_mb,
ROUND(SUM(DATA_FREE)/1024/1024, 2) AS free_mb,
ROUND(SUM(DATA_LENGTH+INDEX_LENGTH)/1024/1024, 2) AS used_mb
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db';
-- B. Top 表 + 索引/数据比
SELECT
TABLE_NAME,
ROUND(DATA_LENGTH/1024/1024, 2) AS data_mb,
ROUND(INDEX_LENGTH/1024/1024, 2) AS index_mb,
ROUND(DATA_FREE/1024/1024, 2) AS free_mb,
ROUND(INDEX_LENGTH/GREATEST(DATA_LENGTH,1), 2) AS idx_data_ratio
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db'
ORDER BY (DATA_LENGTH+INDEX_LENGTH) DESC
LIMIT 30;
-- C. 单索引估算(MySQL 8)
SELECT
table_name, index_name,
ROUND(SUM(stat_value * @@innodb_page_size)/1024/1024, 2) AS index_mb
FROM mysql.innodb_index_stats
WHERE database_name = 'your_db' AND stat_name = 'size'
GROUP BY table_name, index_name
ORDER BY index_mb DESC
LIMIT 50;
-- D. 可能未使用的索引(需结合业务确认)
SELECT object_schema, object_name, index_name, count_star
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE object_schema = 'your_db'
AND count_star = 0
AND index_name IS NOT NULL
AND index_name != 'PRIMARY';
-- E. 数据目录
SHOW VARIABLES LIKE 'datadir';
SHOW VARIABLES LIKE 'innodb_page_size';
11. 总结
把这篇文章收成六句,现场够用:
- 看空间 :
information_schema.TABLES看库表量级,du/df和文件对账;binlog、临时表、undo 经常比业务表先把盘写满。 - 看索引大小 :
INDEX_LENGTH是二级索引合计,主键在DATA_LENGTH里;要拆到某个索引用mysql.innodb_index_stats。 - 数据和索引一样大:通常是二级索引多或主键宽,先审索引,不要先整表重建。
- 索引进内存:页级按需进入 Buffer Pool,LRU 保热汰冷;没有「启动时装载全部索引」这回事。
- 生产扩容 :扩的是磁盘,云盘 / LVM / RDS 多数在线完成,MySQL 一般不用重启;扩盘后记得扩文件系统。表文件会随写入自动涨,但盘满了引擎不会自己加盘。
- OPTIMIZE:InnoDB 上等于重建表,大表很慢、很吃 IO,还要预留约 1 倍空间。小表低峰可以做;大表用 online 工具、从库重建或分区删除。磁盘剩余不够时,先扩盘再收缩。
空间问题看起来像存储,做起来是 观测 → 判断增长来源 → 选最小动作。扩盘、清 binlog、删冗余索引、重建一张表,四者成本差一个数量级。选对动作,比把每条 SQL 背下来更重要。