MySQL 存储空间与索引运维实战:从空间排查到在线扩容

MySQL 存储空间与索引运维实战:从空间排查到在线扩容

适用版本:MySQL 5.7 / 8.0(InnoDB)

读者对象:DBA、运维、后端同学

核心结论先放在这里:库空间看磁盘和 information_schema;索引不会整棵装进内存;生产扩盘通常不用重启 MySQL;OPTIMIZE TABLE 是整表重建,大表上很慢。

线上磁盘告急时,最常见的三个误判是:

  1. 以为「删了数据,文件就会变小」
  2. 以为「索引会常驻内存,所以索引越大查询越快」
  3. 看到 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_ROWSDATA_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_lengthIndex_lengthData_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 份。

特别容易把索引做大的情况:

  1. 二级索引多(4~8 个常见,再多就容易赶超数据)
  2. 主键太宽 :UUID、varchar(36)、多列联合主键。每个二级索引都要带着它
  3. 索引列很长varchar(255)、姓名+手机+地址这类组合索引
  4. 联合索引字段过多,几乎覆盖半张表
  5. 前缀冗余 :有 idx(a,b) 又有 idx(a)
  6. 行本身很瘦(几个 INT),索引相对就会显得很大

一个直观例子:行 100 字节,主键 36 字节 UUID,3 个二级索引各自再带一份主键,仅索引侧就可能接近甚至超过行数据。

4.2 比值怎么读

用前面的 idx_data_ratioINDEX_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 只说明 自实例启动以来没被用到 ,不是「永远没用」。刚重启、刚加的索引、只在月末报表用的索引,都会误伤。把它当成 候选清单,对照慢查询、代码、定时任务后再删。

处理优先级(从收益高、风险低往下):

  1. 删确认冗余的索引(有 (a,b) 且查询都能走它,才考虑去掉单独的 (a)
  2. 主键改为 BIGINT 自增,UUID 放普通唯一索引 ------ 能同时缩小 所有 二级索引
  3. 超长 varchar 改为前缀索引或哈希列(必须评估能否覆盖查询)
  4. 未使用索引列入下个变更窗口

不要因为「索引和数据一样大」就整库 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 运维含义

  1. 索引不是「常驻内存」的保证,只是被访问过的页 可能 在内存里
  2. 想让查询稳,关键是工作集(热数据 + 热索引)能放进 Buffer Pool
  3. innodb_buffer_pool_size 通常给机器内存的 50%~70%(本机还跑其他进程就再降)
  4. 索引建太多:浪费磁盘、拖慢写入;不会因为建了就自动占满内存

看 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 扩盘现场的两个坑

  1. 控制台加了容量,df 没变 ------ 只扩了云盘,没扩文件系统。这是最高频事故。
  2. 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;

过程大致是:

  1. 新建临时表(或 inplace 重建)
  2. 把旧表数据 逐行拷过去,同时重建主键和所有二级索引
  3. 期间新写入要额外处理(row log / 增量追平)
  4. 最后用新表替换旧表,释放旧 .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 生产上更稳妥的做法

  1. pt-online-schema-change / gh-ost:影子表 + 追增量,可限流、可暂停、切换更可控
  2. 从库重建再切换:避免主库扛重建 IO
  3. 分区表走 DROP PARTITION:按时间删冷数据是秒级还空间,比 OPTIMIZE 合适得多
  4. 大批量删除后换「新建表 + 导热数据 + 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. 总结

把这篇文章收成六句,现场够用:

  1. 看空间information_schema.TABLES 看库表量级,du / df 和文件对账;binlog、临时表、undo 经常比业务表先把盘写满。
  2. 看索引大小INDEX_LENGTH 是二级索引合计,主键在 DATA_LENGTH 里;要拆到某个索引用 mysql.innodb_index_stats
  3. 数据和索引一样大:通常是二级索引多或主键宽,先审索引,不要先整表重建。
  4. 索引进内存:页级按需进入 Buffer Pool,LRU 保热汰冷;没有「启动时装载全部索引」这回事。
  5. 生产扩容 :扩的是磁盘,云盘 / LVM / RDS 多数在线完成,MySQL 一般不用重启;扩盘后记得扩文件系统。表文件会随写入自动涨,但盘满了引擎不会自己加盘。
  6. OPTIMIZE:InnoDB 上等于重建表,大表很慢、很吃 IO,还要预留约 1 倍空间。小表低峰可以做;大表用 online 工具、从库重建或分区删除。磁盘剩余不够时,先扩盘再收缩。

空间问题看起来像存储,做起来是 观测 → 判断增长来源 → 选最小动作。扩盘、清 binlog、删冗余索引、重建一张表,四者成本差一个数量级。选对动作,比把每条 SQL 背下来更重要。

相关推荐
hz567891 小时前
视频会议终端的核心功能与采购指南
运维·音视频·信息与通信·智能硬件
xiaoxiangsiyan1 小时前
现代大型虚拟化智慧园区整体架构EVPN‑VXLAN落地全解
运维·网络·学习·架构
嵌入式小能手1 小时前
飞凌嵌入式ElfBoard-Shell脚本编写基础-特殊变量
linux·运维·服务器
智购科技无人售货机工厂1 小时前
2026自动售货机触摸屏驱动开发:从I2C协议到多点触控校准的工程实践~YH
android·驱动开发·python·单片机·云原生·django
呉師傅1 小时前
关于惠普LaserJet1018打印机在Windows11上打印颜色偏淡的问题解决方法
运维·服务器·windows·计算机外设·电脑
九硕智慧建筑一体化厂家1 小时前
楼宇智能优选!KNX总线系统打造稳定高效智能控制体系
运维·人工智能·笔记·智慧城市
正在走向自律1 小时前
数采与虚拟仿真技术在电力智能运维赛道的实践探索
运维·数据采集·虚拟仿真·技能竞赛·人机协同·惯性动捕·电力智能运维
又见情义2 小时前
RK3568 Android 13驱动适配-WiFi&BT模块(RTL8822CS)
android·arm开发·驱动开发
qq_11360149352 小时前
CopyEscape Docker容器逃逸(CVE-2026-17106)
运维·docker·容器