前阵子半夜被值班电话叫醒,某业务服务器磁盘使用率飙到了90%。登上机器第一件事,就是打开mysql命令行,用SQL命令查看mysql数据库中各业务库及对应库中表的大小,十几分钟后就锁定了一张疯狂膨胀的日志表,占掉了大半空间。这类需求说出来很简单,但真正上手写SQL时,很多人会卡在字段不熟、单位换算、结果不准这些点上。这篇文章就从我的真实排查过程出发,把查看业务库大小、表大小的SQL命令、原理和坑一次性讲透,适合运维、后端开发以及所有需要做容量规划的朋友收藏。
1. 从一次磁盘告警说起:数据库的“体重”信息藏在哪里
1.1 磁盘将满时,最怕的就是“盲猜”
磁盘告警这种场景,谁遇谁知道。表面上只是磁盘使用率超标,背后却是连锁反应:写不进去事务、备份失败、慢查询变多,甚至数据库直接只读。此时最忌讳的就是凭经验猜“哪个库大”,或者一张张表去SELECT COUNT(*),那样既慢又不准确,还可能把本来就不宽裕的IO雪上加霜。
正确做法是用SQL直接读取元数据,几秒钟就能看到全貌。MySQL在安装启动后,就自带一个叫information_schema的“数据库”,它不是真正的业务库,而是MySQL的元数据中心。里面有一张TABLES表,记录了实例中每个库、每张表的行数估算值、数据字节数、索引字节数、碎片字节数和存储引擎等。盯着这张表做统计,就是我们的第一条捷径。
1.2 information_schema.TABLES:一份现成的“户口本”
很多初学者以为要统计表大小,得去翻数据目录下那些.ibd文件,其实MySQL早就替我们把账记好了。information_schema是MySQL的服务层虚拟库,不占磁盘存储,它里面的数据是MySQL运行时动态汇总出来的。TABLES表就相当于整个实例所有表的户口本,每一行对应一张真实存在的表。
我常用的核心字段就这么几个,先列出来,后面所有SQL都离不开它们。
| 字段名 | 含义 | 单位 | 备注 |
|---|---|---|---|
| TABLE_SCHEMA | 表所属库名 | 无 | 相当于MySQL里的database |
| TABLE_NAME | 表名 | 无 | 有时会带视图名 |
| ENGINE | 存储引擎 | 无 | 常见InnoDB、MyISAM |
| TABLE_ROWS | 行数估算值 | 行 | InnoDB下不一定准确 |
| DATA_LENGTH | 数据部分占用字节 | 字节 | 不是物理文件大小 |
| INDEX_LENGTH | 索引部分占用字节 | 字节 | 辅助索引、二级索引等 |
| DATA_FREE | 碎片/空闲空间 | 字节 | 可以辅助判断碎片率 |
| CREATE_TIME | 建表时间 | 时间 | 排查可归档老表有用 |
一张表的总大小,理解上就是DATA_LENGTH加上INDEX_LENGTH。数据页和索引页在InnoDB里都是按16KB页管理的,这个值虽然不能百分百等于磁盘上物理文件的大小,但作为容量统计和横向对比,精度已经足够。
1.3 为什么可以放心用SQL统计,而不是扫文件
有人会问,直接去数据目录执行du -sh,不是更“真实”吗?文件系统看到的确实是物理占用,但它有几个硬伤:第一,InnoDB如果开启了innodb_file_per_table,每张表一个.ibd文件,看上去很清晰,但还有共享表空间ibdata1、undo日志、binlog、临时文件,它们混在一起,你很难分清哪个业务库占了多少;第二,MySQL 8.0默认每张表独立表空间,可如果老实例是共享表空间模式,文件系统层面根本分不清表;第三,做自动化巡检脚本时,SQL可以跨实例统一执行,而文件系统命令每台机器路径不同,管理成本高。
所以布局上我一般以SQL统计为主,文件系统复核为辅。SQL统计快速定位“哪个库大、哪个表大”,文件系统验证“物理磁盘到底被谁吃了”。两者结合,才算是一个完整的容量排查闭环。
2. 以库为单位体检:一条SQL扫出所有业务库的总大小
2.1 最基础的汇总SQL长什么样
先解决第一个问题:整个实例下,每个业务库分别占了多少空间?思路很简单,在TABLES表上按TABLE_SCHEMA分组,把数据长度和索引长度分别SUM一下。下面这条SQL是我最常用的起点,先把所有库的总大小算出来:
SELECT TABLE_SCHEMA AS 库名, ROUND(SUM(DATA_LENGTH) / 1024 / 1024, 2) AS 数据大小MB, ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) AS 索引大小MB, ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS 总大小MB FROM information_schema.TABLES GROUP BY TABLE_SCHEMA ORDER BY 总大小MB DESC;这条SQL执行完,你会看到类似于下面的输出,最大的库排在最前面。注意这里用的是ROUND保留两位小数,单位是MB,读起来很直观:
+----------+--------------+--------------+------------+ | 库名 | 数据大小MB | 索引大小MB | 总大小MB | +----------+--------------+--------------+------------+ | db_order | 10240.50 | 2048.25 | 12288.75 | | db_user | 5120.00 | 1024.00 | 6144.00 | | db_log | 4096.12 | 512.80 | 4608.92 | +----------+--------------+--------------+------------+有了这个列表,排查顺序就很清晰了。哪个库最大,优先去看哪个库内的表分布,而不是每个库都翻一遍。
2.2 别忽略单位换算,一字之差就多三个零
我发现很多初学者对1024这个数字不敏感,总以为除以1000就够了。10MB除以1000除以1000算出来反而混乱。这里统一强调:1KB等于1024字节,1MB等于1024KB,1GB等于1024MB。如果你想要GB级别的结果,就把总大小除以1024三次;想要TB级别,就除以1024四次。
以“总大小MB”为例,SQL里写SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024,是因为DATA_LENGTH和INDEX_LENGTH的单位是字节。如果写出/ 1024 * 1024,结果就完全错了,这类细节最容易在调试时浪费半小时。如果业务库特别多,数字特别大,也可以直接用GB输出:
SELECT TABLE_SCHEMA AS 库名, ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 / 1024, 2) AS 总大小GB FROM information_schema.TABLES GROUP BY TABLE_SCHEMA ORDER BY 总大小GB DESC;2.3 排除系统库,加上HAVING过滤大库
实际生产实例里,总会带上mysql、information_schema、performance_schema、sys这四个系统库。统计业务库容量时,它们通常不是我们关注的重点,尤其是information_schema本身是一堆动态视图,统计进去会干扰结果。所以我的习惯是加一个NOT IN过滤:
SELECT TABLE_SCHEMA AS 库名, ROUND(SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS 总大小MB FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') GROUP BY TABLE_SCHEMA ORDER BY 总大小MB DESC;如果只想看超过某个阈值的库,例如总大小超过10GB,就在GROUP BY后面加一行HAVING。HAVING是在分组之后过滤,不能用WHERE,因为SUM是聚合函数的结果。写成HAVING SUM(DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024 > 10240,容量告警的“大库上榜名单”一秒就出来了。
3. 精确定位“重量级选手”:单库内每张表的容量查询
3.1 列出一个业务库下所有表的大小
库的维度看清后,接下来要下钻到表。一句话:在TABLES表上不要GROUP BY库,直接按库名过滤,然后把每张表的DATA_LENGTH和INDEX_LENGTH单独展示。以我在生产环境经常遇到的情况来举例,某订单库有几百张表,我要找出其中最大的十张,SQL是这样的:
SELECT TABLE_SCHEMA AS 库名, TABLE_NAME AS 表名, ENGINE AS 存储引擎, TABLE_ROWS AS 行数, ROUND(DATA_LENGTH / 1024 / 1024, 2) AS 数据大小MB, ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS 索引大小MB, ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS 总大小MB FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'db_order' ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC LIMIT 20;LIMIT 20主要是防止表数量太多时终端刷屏。我这里用TABLE_SCHEMA = 'db_order',实际使用时替换成你自己的库名。执行结果会按总大小从大到小排列,一眼就能看出哪张表是“体积王”。
3.2 全实例大表排名Top N:跨库抓最占空间的对象
有时候我们不是只关心某个库,而是想在整个实例范围内找Top N大表。做法也很直接,把WHERE里的库名条件去掉,只保留排除系统库的条件,再按总大小倒序LIMIT N。我在做实例级巡检时经常用这条:
SELECT TABLE_SCHEMA AS 库名, TABLE_NAME AS 表名, TABLE_ROWS AS 行数, ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS 总大小MB FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC LIMIT 20;这样做有个额外好处:很多公司一个实例可能挂了十几个业务库,业务方报“查询慢”时,我们往往以为是要优化某个表,结果一查才发现是另一个库的某张大表把磁盘IO拖垮了。全实例排名能帮你跳出业务视角,从全局看到真正的瓶颈。
3.3 指定单表查询,以及分区表的特殊说明
如果已经明确了某张大表,想单独看它的各项属性,可以加两个条件,同时指定库名和表名:
SELECT TABLE_NAME AS 表名, ENGINE, TABLE_ROWS, 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 = 'db_order' AND TABLE_NAME = 't_order_detail';这里提醒一句:如果是一张分区表,TABLES表里展示的是整张逻辑表的汇总值,看不到每个分区的分布。想知道每个分区的大小,要去查information_schema.PARTITIONS表,类似这样:
SELECT TABLE_SCHEMA, TABLE_NAME, PARTITION_NAME, TABLE_ROWS, ROUND(DATA_LENGTH / 1024 / 1024, 2) AS 分区数据MB, ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS 分区索引MB FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA = 'db_order' AND TABLE_NAME = 't_order_log' ORDER BY 分区数据MB DESC;用这个结果能快速定位占用最大的分区,再决定是清理老分区还是调整分区策略。
4. 让统计更进一步:用SQL拼出一份完整容量体检报告
4.1 数据量和索引量分开看,判断优化方向
日常巡检,只看一个“总大小”还不够。数据大和索引大背后的处理逻辑完全不同。数据大通常意味着业务增长快、历史数据多,需要考虑归档或者分区;索引大往往说明索引建得太多或者字段冗余,甚至有多个索引覆盖了相同前缀列,此时优化索引结构反而比扩容更划算。
所以我会把数据量和索引量分开统计。一条SQL把每个库的数据大小和索引大小都输出,再计算索引占比,就能大致看出库的“健康画像”:
SELECT TABLE_SCHEMA AS 库名, ROUND(SUM(DATA_LENGTH) / 1024 / 1024, 2) AS 数据MB, ROUND(SUM(INDEX_LENGTH) / 1024 / 1024, 2) AS 索引MB, ROUND(SUM(INDEX_LENGTH) / NULLIF(SUM(DATA_LENGTH), 0) * 100, 2) AS 索引占比pct FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') GROUP BY TABLE_SCHEMA ORDER BY 数据MB DESC;这里用NULLIF防止某个库没有数据时出现除数为0的报错。索引占比如果常年高居40%以上,就需要拿出来专门评审一下索引设计。我曾经遇到过一个库,数据只有20GB,索引却有15GB,最后发现一张表上堆了6个联合索引,其中两个完全冗余,删掉后索引空间直接减少三分之一,写入性能也明显提升。
4.2 结合行数与平均行长,判断业务增长的合理性
TABLE_ROWS虽然是一个估算值,但它仍然很有参考意义。把数据长度除以行数,可以得出一个粗略的“平均行长”,这个值可以帮助我们判断一张表的数据形态:如果一张表平均行长只有200字节,它大概率是纯流水表;如果平均行长有2KB以上,说明表里存了不少大字段,比如长文本、JSON、BLOB。
查询时把行数和平均行长放在一起看:
SELECT TABLE_SCHEMA AS 库名, TABLE_NAME AS 表名, TABLE_ROWS AS 估算行数, ROUND(DATA_LENGTH / NULLIF(TABLE_ROWS, 0), 2) AS 数据平均行字节, ROUND((DATA_LENGTH + INDEX_LENGTH) / NULLIF(TABLE_ROWS, 0), 2) AS 含索引平均行字节 FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'db_user' ORDER BY 估算行数 DESC LIMIT 20;必须声明,这些数字不是精确值,InnoDB的行数本身就是统计信息采样得来的,但它足以用来判断数据形态和增长趋势。如果某个表的估算行数每周翻一倍,哪怕平均行长再短,很快也会变成容量炸弹,这时候就该提前跟业务方聊归档策略了。
4.3 结合DATA_FREE看碎片率
DATA_FREE这个字段常被忽略,但它对InnoDB表很有参考价值。它表示表空间中的空闲页大小,单位是字节。注意区分两种模式:开启innodb_file_per_table时,碎片主要来自反复增删改造成的页分裂和空页;未开启时,碎片看的是共享表空间,参考意义会弱一些。
判断一张表是否需要整理碎片,可以看碎片字节占总大小的比例。比如一张200GB的大表,DATA_FREE显示有20GB,说明页内碎片比较多,可以考虑在低峰期用ALTER TABLE表名ENGINE = InnoDB重建表。但重建表需要额外的磁盘空间,并且会锁表或占用大量IO,要不要做,得结合业务容忍度来判断。
4.4 一条可复用的大报表SQL
把前面的维度整合起来,可以拼出一条比较完整的容量体检SQL。我在自己的巡检工具里保存了下面这版,基本能做到“一次执行,全貌尽出”:
SELECT TABLE_SCHEMA AS 库名, TABLE_NAME AS 表名, ENGINE AS 引擎, TABLE_ROWS AS 估算行数, ROUND(DATA_LENGTH / 1024 / 1024, 2) AS 数据MB, ROUND(INDEX_LENGTH / 1024 / 1024, 2) AS 索引MB, ROUND((DATA_LENGTH + INDEX_LENGTH) / 1024 / 1024, 2) AS 总占用MB, ROUND(DATA_FREE / 1024 / 1024, 2) AS 碎片MB, ROUND(DATA_LENGTH / NULLIF(TABLE_ROWS, 0), 2) AS 数据平均行字节 FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') ORDER BY (DATA_LENGTH + INDEX_LENGTH) DESC LIMIT 100;这条SQL可以覆盖绝大多数日常容量查看需求。它把库名、表名、引擎、行数、数据大小、索引大小、总大小、碎片全部列出来,哪个环节有异常一目了然。
5. 统计数字不“真”的坑:information_schema的边界与对策
5.1 行数是估算值:同一张表两次查询两个结果
新手最容易踩的坑,就是以为TABLE_ROWS是精确行数。实际上,InnoDB引擎的TABLE_ROWS是根据索引统计信息估算出来的,不是实时COUNT(*)的结果。特别是经历过大量增删改之后,它可能和实际行数相差很大,几十万行估算成上百万行都不稀奇。MySQL 8.0之后还引入了information_schema_stats_expiry参数,默认86400秒,也就是24小时内,统计信息会从缓存读取,你看到的值可能是“昨天的”。而MyISAM表由于维护了精确行数,情况会好一些。
想验证真实行数,唯一可靠的方式是SELECT COUNT(),但大表跑COUNT()会消耗大量IO,不要在业务高峰期对千万级以上大表执行。我的经验是:TABLE_ROWS用来观察趋势和排优先级足够,但做精确容量报表时,一定标注清楚这是估算值。
5.2 删了数据,容量却不降反升?
这是第二个高频困惑。DELETE掉大量行之后,跑SQL发现表的大小没有明显下降,甚至有时反而感觉更大了。原因在于InnoDB删除行时,磁盘上被删除的记录只是标记为删除,对应的数据页并不会立刻还给操作系统,空间会变成可复用状态留在表空间里,DATA_FREE字段就会体现出来。你看到的“表大小”可能还是20GB,但里面其实有5GB的空洞。
想真正把物理空间收缩下来,常规手段是重建表,比如ALTER TABLE tbl ENGINE = InnoDB,或者使用OPTIMIZE TABLE tbl。但这两个操作都会重建表并重新组织数据,期间需要跟表大小相当的额外磁盘空间,而且会对生产环境造成压力。所以我的建议是:收缩空间这种事要放在维护窗口做,并且先确认DATA_FREE确实大再有动作。
5.3 权限、刷新、缓存:让数据更接近真相
information_schema也不是“想查什么就能查什么”。如果连接账号权限不足,TABLES表里只会呈现该账号有权限访问的库表,这会导致统计结果“看起来很小”,误以为磁盘空间够用。做容量巡检的账号,最好有全局的PROCESS和SELECT权限,否则结论会失真。
MySQL 8.0中,如果希望统计信息更接近最新,可以临时关闭统计信息缓存:
SET GLOBAL information_schema_stats_expiry = 0;不过这个参数是全局的,改完会影响所有会话的行为,生产环境谨慎。更温和、更精准的做法是单独对某张表执行ANALYZE TABLE,让它的统计信息立即更新,例如:
ANALYZE TABLE db_order.t_order_detail;如果表数量非常多,手工一个个执行不现实。可以先查询出所有需要分析的语句,再批量执行。SQL拼接出ANALYZE语句的思路类似这样:
SELECT CONCAT('ANALYZE TABLE `', TABLE_SCHEMA, '`.`', TABLE_NAME, '`;') FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys');把结果导出后按需执行即可。注意,ANALYZE TABLE在表很大时也会消耗一定IO,尽量错峰执行。
5.4 全实例扫描也有开销,别在高峰期反复跑
information_schema的TABLES表本质上是动态元数据,每次查询都会让MySQL去扫描所有库的表目录。一个实例如果表数量很多,比如上万张表,这种查询同样会产生不小的开销。尤其不要把这类SQL写进业务代码的请求链路里,也不要在每天业务最旺盛的时候做全实例巡检。
我个人的巡检节奏是:每天凌晨跑一次全实例容量SQL,结果落库留档,白天只在接到告警时才做精确查询。这样做既不干扰业务,又保留了趋势数据。
6. 双保险验证:把SQL统计和文件系统结合起来看
6.1 从数据目录印证各库各表真实占用
SQL统计虽快,但有些场景需要用文件系统做交叉验证。比如有同事反馈某库物理占用已经超过300GB,可SQL算出来只有200GB,此时就该用文件系统确认一下。首先确认数据目录位置和每表独立表空间是否开启:
SHOW VARIABLES LIKE 'datadir'; SHOW VARIABLES LIKE 'innodb_file_per_table';确认数据目录后,执行系统命令:
du -sh /var/lib/mysql/db_order ls -lh /var/lib/mysql/db_order/*.ibd第一行命令看库目录总大小,第二行可以看到每张InnoDB表的物理文件大小。注意,只有innodb_file_per_table开启时,每张表才有独立的.ibd文件;如果关闭,数据会写在ibdata1共享表空间里,无法直接从文件区分单库单表。即使开启,undo、binlog、重做日志等也都不在这些目录里,文件系统统计只能作为辅助验证,不能完全替代SQL统计。
6.2 建一个容量历史表,让趋势自动说话
单次查看只能知道“现在有多大”,可容量问题最忌讳的就是等到磁盘报警才处理。我建议把每天的库级统计结果存到一张历史表里,连续记录一周、一个月后,增长趋势自然浮现。建表的思路很朴素:
CREATE TABLE IF NOT EXISTS schema_size_history ( id INT AUTO_INCREMENT PRIMARY KEY, stat_date DATE NOT NULL, schema_name VARCHAR(64) NOT NULL, data_bytes BIGINT NOT NULL, index_bytes BIGINT NOT NULL, total_bytes BIGINT NOT NULL, UNIQUE KEY uk_date_schema (stat_date, schema_name) ) ENGINE=InnoDB;每天定时插入一条记录:
INSERT INTO schema_size_history (stat_date, schema_name, data_bytes, index_bytes, total_bytes) SELECT CURDATE(), TABLE_SCHEMA, SUM(DATA_LENGTH), SUM(INDEX_LENGTH), SUM(DATA_LENGTH + INDEX_LENGTH) FROM information_schema.TABLES WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') GROUP BY TABLE_SCHEMA;注意唯一键是stat_date和schema_name,如果一天内脚本被重复执行,会触发唯一键冲突。可以在末尾加ON DUPLICATE KEY UPDATE更新当日值,也可以直接用cron保证每天只跑一次。而后想查看某个库的增长情况,直接按日期排序就能描出曲线:
SELECT stat_date, ROUND(total_bytes / 1024 / 1024 / 1024, 2) AS 总大小GB FROM schema_size_history WHERE schema_name = 'db_order' ORDER BY stat_date;有这份历史记录,就能提前预判一个库“按当前增速,多少天后会触顶”,这才是容量管理真正值钱的部分。
6.3 定位到大表之后,常见的下一步动作
找到大表只是第一步,后续处置才是重头。结合不同表的性质,我通常按这个思路走:优先看这张表有没有时间字段,如果有,且业务允许清理历史数据,就按时间分批次DELETE,或者建议业务方做按时间分区,未来删除时直接DROP分区,速度极快;如果表上有大量未用的冗余索引,就先评估删除索引,降低索引空间和写入开销;如果数据必须长留,就要考虑归档到冷存储、拆分表,或者与业务方一起评估是否真的需要保留所有明细。
这些处置动作没有一个能在高峰期“顺手”完成,都得提前做方案、预约窗口。所以与其等到告警,不如平时就定期跑容量SQL,把大表都标记出来,心里有数。
说说我这几年实际操作下来的一些体会。首先,把核心SQL存成一份模板或脚本,每次排查直接从里面改库名,不要每次都手敲,能省下不少时间,也能降低写错的概率。其次,统计结果一定要标单位、标时间,否则过了一个月回看数据,很容易把MB和GB搞混。最后,别迷信某一个数字,SQL统计是辅助判断的,真正做扩容或迁移决策时,记得结合文件系统实际占用、历史增长趋势和业务高峰期的IO表现综合来看。如果你也经常处理数据库容量问题,希望这些SQL和踩过的坑能让你少走几步弯路。