你负责的MySQL实例磁盘告警了,你脑子里第一个问题肯定是:到底是哪个库、哪张表把空间吃满了?我见过太多人到了这一步才开始手忙脚乱地翻旧笔记,一台台服务器登进去,系统盘眼看就要写满,却连一条准确的容量统计SQL都写不出来。查看mysql数据库容量大小这个需求,听起来很简单,好像一条SQL跑完就能交差,但真正做到准确、可解读、可监控,绕不开information_schema的统计逻辑,也绕不开物理文件与逻辑大小之间的差异。这篇文章就按我平时处理问题的真实路径来写,先解决“怎么查”,再解决“查得准不准”,最后分享一个我自己在用的巡检脚本和几个典型坑。
1. 为什么要盯数据库容量:一张容量报表背后的运维刚需
先说结论:容量统计不是DBA一个人的事,后端开发和运维都得会。很多问题,比如磁盘告警、慢查询突然变多、备份时间拉长、云资源成本超支,最后追根溯源都是“空间被某个库或某张表悄悄吃掉了”。下面三个场景我几乎每年都会遇到。
1.1 磁盘告警:你半夜起床的样子真的很狼狈
某天凌晨2点,监控平台突然弹出一条“磁盘使用率超过90%”的告警。你登录服务器,df -h一看,挂载盘还剩不到10G,而业务还在不断写入。这时候你判断不出是谁占的空间,就只能先做应急:清binlog、挪日志、腾临时空间,但根本问题没解决,第二天告警大概率还会来。
我经历过最难受的一次,是一个跑了三年的订单库,某天晚上大批量任务把一张流水表写爆了,整个实例的数据目录直接把系统盘撑满。登录上去后我第一时间要回答的问题就是:哪个库涨得最快?哪个表在疯狂插入?如果当时能像喝水一样自然地写出information_schema的容量统计SQL,定位时间能缩短一半。
所以,掌握容量查询能力,本质上是在给“半夜救火”做准备。
1.2 容量规划与成本核算:老板问“能撑多久”你要能答
业务要上新功能,预估数据量每天增长50G,老板问“现有磁盘够撑多久?”如果你只会df -h,只能回一句“磁盘还剩500G”,这是不够的。你需要知道当前实例的数据总量、各库的环比增长速度、binlog保留策略占了多少空间,然后推算出扩容时间点。
还有一个容易被忽视的需求:成本分摊。很多公司多个业务线共用一个MySQL实例,财务要按库统计资源消耗,这时候你就得能从实例维度按库输出容量报表。没有现成的监控面板,一条容量统计SQL就是你最靠谱的武器。
1.3 表膨胀与碎片:一个被低估的性能隐患
容量不只是空间问题,还会直接影响性能。InnoDB表频繁更新、删除后,会产生碎片(data_free),碎片多了,全表扫描的IO开销会变大,数据页的利用率会下降。一张明明只存了5G有效数据的表,可能占了10G物理空间。
这种情况如果不定期体检,你很难发现。等我写到data_free这个字段时,你会看到它是怎么帮你判断“该不该做表重建”的。
2. 最基础的查询方法:information_schema 一查到底
MySQL把表相关的元数据放在information_schema.tables里,这是我们做容量统计的核心数据源。它里面有每个库、每张表的行数估算、数据长度、索引长度和碎片大小,一条GROUP BY就能出报表。
2.1 一条SQL把全实例所有库的容量列出来
我给这套查询起名叫“肌肉记忆SQL”,因为你应该做到不用想就能写出来:
SELECT table_schema AS '数据库', ROUND(SUM(data_length + index_length + data_free) / 1024 / 1024, 2) AS '总容量MB', ROUND(SUM(data_length) / 1024 / 1024, 2) AS '数据MB', ROUND(SUM(index_length) / 1024 / 1024, 2) AS '索引MB', ROUND(SUM(data_free) / 1024 / 1024, 2) AS '碎片MB', COUNT(*) AS '表数量' FROM information_schema.tables GROUP BY table_schema ORDER BY SUM(data_length + index_length + data_free) DESC;这条SQL会把整个实例所有库按容量从大到小排出来。数据量大或者想更直观时,可以换算成GB:
SELECT table_schema AS '数据库', ROUND(SUM(data_length + index_length + data_free) / 1024 / 1024 / 1024, 2) AS '总容量GB' FROM information_schema.tables GROUP BY table_schema ORDER BY 总容量GB DESC;我在现场排查时一般先用第二条快速扫一圈,确定哪个库有问题,再钻进单库单表里去定位。这里有个细节:在MySQL 8.0里给SELECT里的别名加中文,排序时可以直接引用别名,但在5.7某些版本里会报错,稳妥起见还是用完整表达式排序比较好。
2.2 看懂字段:data_length、index_length、data_free 分别代表什么
这三个字段才是这条SQL的灵魂。很多人会用但不理解,结果就被误导。
data_length:对InnoDB来说,它表示聚簇索引叶子节点占用的字节数,可以近似理解为“数据页的总大小”。对MyISAM来说,它直接对应数据文件的长度。index_length:二级索引占用的字节数。InnoDB的每个索引都是一棵B+树,data_length是主键索引树,index_length就是所有辅助索引树加起来的大小。data_free:已分配但尚未被使用的空间,也就是碎片。表现为表中删除大量行、页分裂后留下来的空位。这部分空间逻辑上属于表,但可以复用,不过不会自动返还给操作系统。
可以打个生活化的比方:data_length是“卧室里摆满家具的面积”,index_length是“衣柜和储物间的面积”,data_free是“客厅里没摆东西的空地”。整张表的物理占用,就像整个房子的建筑面积,除了套内面积,还要算上走廊、墙壁和公共区域,这也是下一节逻辑容量对不上物理容量的原因之一。
另外要注意table_rows字段。很多人拿它当实际行数,这是不对的,InnoDB对它的统计是基于采样估算的,误差可能达到百分之几十。判断一张表多大,请用字节数,别用行数。
2.3 为什么统计结果和实际磁盘占用对不上
如果你查出来某个库总容量是100G,然后去服务器上du -sh /var/lib/mysql/库名,发现只有80G,或者反过来是120G,别慌,这是正常的。
原因主要有三点:
- information_schema里统计的是逻辑大小,是InnoDB按页计算出来的理论值,不是物理文件大小。
- InnoDB有预分配机制。段空间按区(extent)分配,一个区默认1MB,即使表只写了1行,也可能占用一个区。空闲页会留在段里等待复用,这些不会体现在逻辑统计里,都会算进物理文件里。
- 如果多张表共用了共享表空间(比如关闭了
innodb_file_per_table,或者系统表在ibdata1里),那么逻辑上看着分散的表,物理上都挤在同一个文件里,du根本看不出哪张表占了多少。
所以,结论是:information_schema的统计适合“横向对比和趋势分析”,要确认真实磁盘占用,必须结合操作系统层面的文件查看。这两套手段配合起来,才是完整的容量视角。
3. 精细到库和表:查清楚到底谁在吃空间
全实例报表只能帮你锁定“嫌疑库”,接下来要钻到表级别。绝大多数容量问题的根源,最后都能定位到某几张“大表”。
3.1 查看指定数据库下每个表的容量并排序
在确定了目标库后,我习惯跑下面这条:
SELECT table_name AS '表名', engine AS '引擎', table_rows AS '估算行数', ROUND((data_length + index_length + data_free) / 1024 / 1024, 2) AS '总容量MB', ROUND(data_length / 1024 / 1024, 2) AS '数据MB', ROUND(index_length / 1024 / 1024, 2) AS '索引MB', ROUND(data_free / 1024 / 1024, 2) AS '碎片MB' FROM information_schema.tables WHERE table_schema = '你的库名' ORDER BY (data_length + index_length + data_free) DESC LIMIT 20;输出结果按容量倒序,前20张基本就能还原出这个库的空间分配。我见过最典型的案例,是某系统的“日志流水表”占了整个库85%的空间,业务方根本不知道。看到这张表后,后续的归档、清理、分区方案才能有的放矢。
3.2 用 SHOW TABLE STATUS 快速查看单表信息
如果你只想确认某一张具体表的情况,不用去information_schema里拼字段,直接:
SHOW TABLE STATUS LIKE 'orders'\G这个命令会显示一行结果,里面同样包含Data_length、Index_length、Data_free、Rows等字段。\G是为了让输出纵向展开,字段多的时候比普通表格模式好读得多。
我最常用它的是在变更前:比如要做一次OPTIMIZE TABLE或者ALTER TABLE,先看一眼Data_free,如果碎片占比很高,那就值得做;如果很低,重建表的收益就很小,没必要冒着锁表风险去折腾。
3.3 识别大索引和冗余索引
很多人查容量只看数据大小,忽略了索引。实际上,索引容量很容易超过数据容量。我处理过一个表:数据25G,索引40G,50%以上的索引空间来自三个重复的联合索引。减少这些冗余索引后,表总容量直接下降了近20G,插入性能也改善了不少。
定位大索引,可以单独把index_length拉出来排序:
SELECT table_name AS '表名', ROUND((data_length / 1024 / 1024), 2) AS '数据MB', ROUND((index_length / 1024 / 1024), 2) AS '索引MB', ROUND((index_length / data_length) * 100, 2) AS '索引占比%' FROM information_schema.tables WHERE table_schema = '你的库名' AND data_length > 0 ORDER BY index_length DESC LIMIT 20;当索引占比非常高,比如超过60%甚至100%,就该用SHOW INDEX FROM 表名去检查索引列表了。常见问题是:
- 同一个字段既建了单列索引,又出现在联合索引首位。
- 前缀索引与普通索引功能重叠。
- 低基数字段(如状态字段)也建了索引,基本不会被优化器使用。
对这些索引做减法,不仅能释放容量,还能降低写入放大,属于“查容量”延伸出来的高价值操作。
4. 物理文件维度:binlog、redo log、系统库和临时文件
从情报价值来说,information_schema只负责“库表内部”,但MySQL实例的磁盘占用,还包含一堆“库表之外”的文件。这些文件才是真正让你磁盘告警的原因。
4.1 binlog 可能比你的数据还大
binlog是二进制日志,记录所有变更操作,用于主从复制和时间点恢复。它的增长速度往往被严重低估。我见过一个只写业务库的实例,binlog总大小比所有表加在一起还大。尤其是大批量UPDATE或DELETE时,如果没走索引,一条语句可能会产生几十GB的binlog。
查看当前binlog文件列表和大小:
SHOW BINARY LOGS;查看是否开启,以及保留策略:
SHOW VARIABLES LIKE 'log_bin'; SHOW VARIABLES LIKE 'binlog_expire_logs_seconds'; SHOW VARIABLES LIKE 'expire_logs_days';MySQL 5.7用expire_logs_days,8.0推荐用binlog_expire_logs_seconds,单位不同,别搞混。清理binlog的操作是PURGE BINARY LOGS TO 'mysql-bin.000012';或者PURGE BINARY LOGS BEFORE '2024-01-01 00:00:00';。但千万不能在从库还在追日志的时候乱删,否则会导致复制中断,这属于基本禁忌。
4.2 redo/undo 日志和共享表空间
redo log是InnoDB崩溃恢复用的,默认以ib_logfile0、ib_logfile1存在,MySQL 8.0.30以后改到#innodb_redo目录。它的大小一般由innodb_log_file_size控制,虽然不像binlog那样飞速增长,但也占着一块不小的固定磁盘空间。
undo log保存事务回滚需要的旧版本数据,独立成undo表空间。长事务、大事务会让undo膨胀,事务一直不提交,undo文件就可能一直涨。曾经有一次我排查磁盘满,发现undo表空间涨到几十G,根源是一个应用开了事务后忘了提交,跑了整整一夜。
共享表空间ibdata1也值得关注。如果你发现实例里所有表都开着innodb_file_per_table=ON,但ibdata1却非常大,多半是历史遗留问题,比如早期关闭过独立表空间,或大量使用临时表。查看参数:
SHOW VARIABLES LIKE 'innodb_file_per_table'; SHOW VARIABLES LIKE 'innodb_data_file_path';4.3 系统库与临时文件不要忽略
mysql库存了系统权限表、help表、时区表等,performance_schema和sys库也有自己的磁盘占用。虽然单看都不大,但胜在数量多,长时间版本升级、监控采集如果积累了大量统计表数据,也会贡献几个G。
临时文件更不能漏。MySQL执行大排序、大临时表时,会往tmpdir写临时文件。默认tmpdir经常是/tmp,如果/tmp挂载在系统盘上,一次大查询就能把根分区撑爆。查看:
SHOW VARIABLES LIKE 'tmpdir';还有一个常见的隐形杀手是慢查询日志。如果你开启了slow_query_log且long_query_time设得很低,日志文件可能以每天几个G的速度增长。看参数:
SHOW VARIABLES LIKE 'slow_query_log'; SHOW VARIABLES LIKE 'slow_query_log_file'; SHOW VARIABLES LIKE 'long_query_time';4.4 用 du/ls 查看真实物理占用
数据库层面的逻辑统计搞完后,一定要回到操作系统看一眼真实文件布局。我常用的命令组合:
du -sh /var/lib/mysql/*这会列出数据目录下所有文件、目录、binlog的大小。注意表的数据文件不直接在/var/lib/mysql/库名/下,而是在/var/lib/mysql/库名/表名.ibd,所以更细一层:
du -ah /var/lib/mysql/你的库名/ | sort -rh | head -20这条命令按文件大小倒序,能直接告诉你哪张表的.ibd文件最大。这一步和information_schema的统计结果相互印证,你才会真正对实例的物理占用心里有底。
5. 实战:写一个一键容量巡检脚本
看完前面这些,你可能已经能手动查了。但人肉查询最大的问题是“不持续”。磁盘是一天天涨的,等到告警才看,永远被动。所以我把容量查询做成脚本,定时跑,每天生成一份报告。
5.1 脚本设计:库级+表级双维度输出
脚本核心逻辑很简单:先用信息汇总SQL输出每个库的总容量数据,再针对Top库输出Top20大表。我用bash写的版本大概是这样的:
#!/bin/bash # mysql_capacity_report.sh MYSQL_BIN="mysql" MYSQL_HOST="127.0.0.1" MYSQL_PORT="3306" MYSQL_USER="monitor" MYSQL_CNF="/etc/mysql_monitor.cnf" OUT_FILE="/tmp/mysql_capacity_report_$(date +%Y%m%d).txt" # 库级容量统计 $MYSQL_BIN --defaults-extra-file="$MYSQL_CNF" -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -N -e " SELECT '====== Database Capacity ======'; SELECT table_schema, ROUND(SUM(data_length + index_length + data_free) / 1024 / 1024 / 1024, 2) AS total_gb FROM information_schema.tables GROUP BY table_schema ORDER BY total_gb DESC; " > "$OUT_FILE" # 表级容量Top20 $MYSQL_BIN --defaults-extra-file="$MYSQL_CNF" -h"$MYSQL_HOST" -P"$MYSQL_PORT" -u"$MYSQL_USER" -N -e " SELECT '====== Top 20 Tables ======'; SELECT table_schema, table_name, ROUND((data_length + index_length + data_free) / 1024 / 1024 / 1024, 2) AS total_gb, ROUND(data_free / 1024 / 1024, 2) AS free_mb FROM information_schema.tables ORDER BY (data_length + index_length + data_free) DESC LIMIT 20; " >> "$OUT_FILE" echo "report saved: $OUT_FILE"--defaults-extra-file指定客户端配置文件,可以把账号密码放在单独文件里,避免在命令行中暴露。文件权限要设置成600:
[client] user=monitor password=你的强密码提示:千万别把数据库密码直接写在shell脚本正文里,尤其是脚本还会进版本库的情况下;用
defaults-extra-file是更稳妥的做法。
5.2 加入告警阈值和定时任务
脚本输出报告只是第一步,我更建议在报告里直接给出“风险等级”。比如某库总体容量超过磁盘容量的75%,就在报告里打WARN;超过90%,打ALERT。实现方式就是在SQL外面包一层awk判断:
TOTAL_GB=$(du -sb /var/lib/mysql | awk '{print $1}' 2>/dev/null)不过这属于快速判断,更标准的做法是结合监控系统,把脚本输出的容量值推送到监控平台,由监控平台统一报警。社区常见的采集方案都会从information_schema.tables拉取容量指标,你可以按同样的思路把这条SQL挂在采集器里。
定时任务我通常放在凌晨业务低峰期执行:
0 3 * * * /usr/local/bin/mysql_capacity_report.sh >/dev/null 2>&1不要每次现查,把每天的报告留存下来,一个月后你就有了一条容量增长趋势线,这是后续规划扩容最珍贵的依据。
5.3 权限与安全实践
容量巡检账号不要用root,单独创建一个只读账号:
CREATE USER 'monitor'@'127.0.0.1' IDENTIFIED BY '你定义的安全密码'; GRANT SELECT, SHOW DATABASES ON *.* TO 'monitor'@'127.0.0.1';如果还想让脚本查看binlog大小,需要额外给REPLICATION CLIENT权限:
GRANT REPLICATION CLIENT ON *.* TO 'monitor'@'127.0.0.1';权限最小化原则在这里同样适用。这个账号只能读元数据,碰不到业务数据,即使脚本被泄露,风险也可控。
6. 常见问题与排查技巧实录
写到这里,再分享几个我实打实踩过的坑。这些问题如果你没遇到过,会觉得“不就是一个SQL吗怎么会这样”;遇到了才会知道,实践才是检验真理的唯一标准。
6.1 information_schema 查询很慢或统计为0
有段时间我跑容量统计SQL,发现某些大表显示0,小表显示正常。后来查资料并验证发现,InnoDB对表统计信息有缓存和采样机制,在表刚创建、刚崩溃恢复、或者统计信息还没更新时,字段可能不准确。
解决办法是:
ANALYZE TABLE 你的表名;这个语句会重新统计表的索引信息,通常跑完后information_schema里的值就正常了。不过要注意,在超大表上执行ANALYZE TABLE虽然不会锁表重建,但也可能带来一定IO压力,建议低峰期操作。
还有一次是查询information_schema本身特别慢。那是因为系统库里的临时表处理机制,加上表数量过万,GROUP BY全表会有点卡。优化思路是缩小范围,先限定table_schema,再统计,别总是全实例硬扫。
6.2 容量显示很小但磁盘满了
这是最迷惑人的情况。information_schema统计所有库加起来才50G,但df -h显示磁盘用了200G。差出来的150G在哪?我排查过几次,排名靠前的元凶基本是:
- binlog。有些环境遗忘清理策略,保留了几百个binlog文件。
- 慢查询日志、错误日志、审计日志。特别是审计日志开着的合规环境,一天几个G很正常。
- 临时文件和排序文件。大查询跑完没释放。
- 备份文件。有人把逻辑备份直接放到数据目录里,挨着
ibdata1放,谁看了都头皮发麻。
所以排查顺序应该是:du -sh /var/lib/mysql/*先看整体布局,再SHOW BINARY LOGS看binlog,然后查日志参数和备份脚本。别一上来就盯着业务表薅。
6.3 删了数据但空间没释放
业务表删掉几百万行“历史数据”,结果du一看,表文件一点没变小。这不是没删干净,是InnoDB的机制在作祟:删除行只是把页里的记录标记为可复用,数据文件的大小不会自动收缩。data_free会明显升高,你可以跑一下:
SELECT table_name, ROUND(data_free / 1024 / 1024, 2) AS free_mb, ROUND((data_length + index_length) / 1024 / 1024, 2) AS used_mb FROM information_schema.tables WHERE table_schema = '你的库名' ORDER BY free_mb DESC;如果碎片占比很高,就需要重建表来收缩物理文件,例如:
OPTIMIZE TABLE 你的表名;或者等价地:
ALTER TABLE 你的表名 FORCE;但这两个操作都会在重建期间对表加锁,大表一定要安排在维护窗口。如果表特别大且业务不能接受停机,可以考虑在线表结构变更工具来重建表,但也得评估额外资源消耗。总之,删数据不等于省空间,这句话值得写进团队规范里。
6.4 OPTIMIZE TABLE 用还是不用
很多人的习惯是磁盘一不够就OPTIMIZE TABLE,这其实不对。先判断碎片率:
SELECT table_name, ROUND(data_free / 1024 / 1024, 2) AS free_mb, ROUND((data_length + index_length + data_free) / 1024 / 1024, 2) AS total_mb, ROUND(data_free / (data_length + index_length + data_free) * 100, 2) AS frag_pct FROM information_schema.tables WHERE table_schema = '你的库名' AND data_free > 0 ORDER BY frag_pct DESC LIMIT 20;我自己有一个粗略判断:碎片率超过30%且表总容量超过1G,才值得做重建;碎片率低于10%的表,重建收益很小,反而会白白增加一次IO高峰。另外,对MyISAM表执行OPTIMIZE时的锁表行为比InnoDB更明显,用之前务必看清引擎类型。
我的习惯是:先用这句SQL找出“高碎片表名单”,然后按容量从大到小排序,分批在低峰期处理,而不是对全库所有表无差别优化。
7. 一些只有踩过坑才懂的小经验
最后说点工具之外的东西。容量统计这件事,技术难度不高,真正难的是“提前发现”和“持续积累”。我这些年有个习惯,新接手一个MySQL实例,第一件事就是拉一次容量基线,存下来。之后每个月初再拉一次,对比上个月,哪些库翻倍了,哪些表增长异常,一眼就能看出来。这个习惯让我在不少业务出问题之前就提前发出了预警。
另一个经验是,容量最大的那些表,往往不是核心业务表,而是日志表、流水表、临时结果表。对这些表要优先考虑生命周期管理:能分区的分区,能归档的归档,能定期清理的加上调度任务。别等到几十个G了再想办法,那会非常被动。
还有一点要提醒:如果你用的是云数据库,控制台通常会展示存储空间使用量,但它是按整个实例维度算的,不会细到某张表。你依然得靠information_schema这条老路去定位问题。所以不论环境怎么变化,SQL基本功不能丢。
多跑几遍上面的SQL,把原理搞清楚,再配一个简单的巡检脚本,容量问题就不会再让你半夜被叫醒了。