外呼系统慢查询排查:两条 SQL 摸清数据库大表
约 1660 字大约 6 分钟
2026-09-14
最近承接了一个外呼系统的共建。这类系统的数据特点很鲜明:任务下发、批量外呼、通话记录落库、话单回流,链路上每一步都在往数据库里写数据,而且基本是只增不减的流水型写入——通话记录一天几十万行,话单一天几十万行,跑上一两年,随便一张表就是亿级体量。
项目刚接手没几天,运维那边甩过来几条慢 SQL,让分析一下瓶颈在哪。
拿到慢 SQL,很多人的第一反应是直接 EXPLAIN 跑一把看执行计划。我也想这么干,但手上这是共建系统,表是谁建的、数据长什么样、哪些表已经悄悄长成了巨无霸,我一概不知。同一句缺索引的 SQL,跑在几千行的表上是小事,跑在几千万行的表上就是事故——所以在碰执行计划之前,得先搞清楚:这个库里,到底哪些表是大表。
为什么第一步是找大表
因为慢 SQL 的病根,十有八九埋在大表里。
对外呼系统来说更是如此。一条 SQL 上个月还是 50ms,这个月变成 5s,多半不是 SQL 本身写烂了,而是它背后的表在持续膨胀,把原本将就着能用的执行计划彻底压垮了。先知道大表在哪,后续的 EXPLAIN、索引审计、归档方案才有主次——不然就是拿着放大镜满地图找病灶。
而这件事的成本低到不像话:MySQL 把每张表的行数和存储占用都记在 information_schema.TABLES 里,一条 SQL 全捞出来,几秒钟建立全局认知。几乎零成本、又能决定后续排查方向的事,没理由不第一个做。
第一条 SQL:全库摸底
共建系统的数据库实例里,除了自己的业务库,往往还住着别的业务。所以第一遍我习惯扫全库,把四个系统库挡在门外:
-- 全库扫描:各表的记录数和存储容量(排除系统库)
SELECT
table_schema AS '数据库名',
table_name AS '表名',
table_rows AS '记录数',
CONCAT(ROUND(data_length / 1024 / 1024, 2), ' MB') AS '数据大小',
CONCAT(ROUND(index_length / 1024 / 1024, 2), ' MB') AS '索引大小',
CONCAT(ROUND((data_length + index_length) / 1024 / 1024, 2), ' MB') AS '总大小'
FROM information_schema.TABLES
WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
ORDER BY table_schema, table_rows DESC; 高亮的两行是这条 SQL 的灵魂:WHERE 把 information_schema、mysql、performance_schema、sys 四个系统库排除掉,免得统计结果里混进一堆系统表的干扰;ORDER BY 让结果按库分组、按记录数降序,跑完之后每个库里谁是大表,前几行直接给出答案。
这次实际跑出来,排名靠前的清一色是通话记录表、外呼任务明细表、话单表,头部那张已经过亿行、总大小几十 GB。后续慢 SQL 的排查重点,基本就锁定在它们身上了。
查出来的字段都是什么意思
| 字段 | 说明 |
|---|---|
table_schema | 表所在的数据库 |
table_name | 表名 |
table_rows | 表的行数,估算值,后面单独说这个坑 |
data_length | 表数据占用的空间,单位字节 |
index_length | 表上索引占用的空间,单位字节 |
两个细节值得留意。data_length 和 index_length 的原始单位是字节,SQL 里除了两次 1024 才换算成 MB——顺手还能观察一件事:如果一张表的索引比数据还大,多半说明索引设计有冗余,共建系统里这是高发问题。另外两者加起来才是这张表对磁盘的真实压力,评估瓶颈时别只盯着行数看。
第二条 SQL:只盯自己的库
确认排查范围之后,日常监控就没必要每次全库扫了,圈定业务库即可:
-- 查看指定数据库中各表的记录数和存储容量
SELECT
table_name AS '表名',
table_rows AS '记录数',
CONCAT(ROUND(data_length / 1024 / 1024, 2), ' MB') AS '数据大小',
CONCAT(ROUND(index_length / 1024 / 1024, 2), ' MB') AS '索引大小',
CONCAT(ROUND((data_length + index_length) / 1024 / 1024, 2), ' MB') AS '总大小'
FROM information_schema.TABLES
WHERE table_schema = 'your_database_name'
ORDER BY table_rows DESC;把 your_database_name 换成实际库名就行,输出少了一列库名,更清爽,发给团队里其他人用也直观。
顺手生成精确 COUNT 语句
把输出列改成 CONCAT('SELECT COUNT(*) FROM ', table_name, ';'),查出来的就是一列可以直接执行的 COUNT 语句,下一轮核实行数时不用一张张手敲。
一个必须知道的坑:table_rows 是估算值 不精确
先记住这个结论
table_rows 来自 InnoDB 的统计信息,是采样估算出来的,误差可能达到百分之十几甚至几十。它是给查询优化器做代价估算用的,从来不是给人数数用的。
那这个数还能信吗?
分场景。定位大表的阶段,你要的从来不是精确到个位数的行数,而是数量级——一张表到底是 8000 万还是 1 亿,对排查方向没有任何影响;但到底是 80 万还是 8000 万,影响就大了。真正需要精确数字的时候(评估归档方案、做数据迁移),对重点表老老实实跑一次 COUNT(*)。
为什么 InnoDB 不维护精确行数
MyISAM 的表头里存着精确行数,因为它写入是表级锁、串行执行,数得出数。而 InnoDB 是 MVCC 多版本并发控制,不同事务在同一时刻看到的行数可能都不一样——同一个"现在",有的会话能看见那几条未提交的插入,有的看不见,所以它压根没法维护一个全局一致的实时计数,只定期采样统计信息给优化器参考。
拿到大表清单之后
到这里,"面"上的认知就有了。接下来才是把运维给的慢 SQL 逐条对号入座:慢 SQL 涉及的表 × 大表清单,交集出来的就是重点嫌疑对象,EXPLAIN、索引审计、冷热数据归档,都是从这份交集往下走的。
慢 SQL 的逐条分析和优化方案,等这轮做完再单独整理一篇。
接手别人的系统,别急着动手术,摸清家底永远是第一步。