一、现象或背景:一个"数数"接口把从库拖到 9 秒
先说结论:在 InnoDB 里,SELECT COUNT(*) FROM t 不是"数个数",而是全表扫描——它要顺着聚簇索引把整张表的行一条条走一遍才能得出一个数。
我们内部有个"资产总览"后台,首页要展示 inventory 表的总行数。这张表 3800 万行,单行约 200 字节,整张表约 7.6GB。以前用 SELECT COUNT(*),日常 800ms 左右大家没当回事;直到某天主从切换后,从库上这条 SQL 的 P99 直接拉到 9.3 秒,把其它读请求全堵住,值班同学以为是从库挂了,其实是这一个"数数"接口把 IO 打满了。
打比方:COUNT(*) 在 InnoDB 里就像让你数一个鞋柜里有多少双鞋——它不会翻标签,而是把每一双鞋都掏出来看一眼,再看有没有。鞋柜越大,你数得越久。
二、原理或根因分析:为什么 InnoDB 的 COUNT(*) 这么贵
- InnoDB 没有维护一个"行数计数器"。这和 MyISAM 不同——MyISAM 表头存了行数,
COUNT(*)是 O(1);InnoDB 因为 MVCC 多版本,同一时刻不同事务看到的行数不一样,所以没法存一个固定数字,只能现数。 - 关键机制:
COUNT(*)走二级索引最小宽度索引(MIN_LENGTH),但仍要把索引叶子节点全部遍历一遍,逐行判断可见性(MVCC 下还要看 undo 版本)。所以它的耗时和表行数成正比,是 O(N)。 - 更坑的是:即便表上没有任何 WHERE,优化器也用不上覆盖索引"免回表"的红利——它本来就不回表,它只是把整棵 B+ 树叶子扫一遍。表越大、undo 越多,越慢。
- 主从切换后从库上这条 SQL 变慢,是因为从库通常 buffer pool 更小、且要承担回放日志的 IO,全表扫描这种"大吞吐低命中"的查询在从库上尤其致命。
三、具体做法:三种替代(可复现)
方案 A:用最小二级索引近似计数,避开大聚簇索引。 如果表有二级索引且行较宽,让它走最窄的那个索引叶子,扫描量更小:
-- 强制走最窄二级索引(假设 idx_biz_no 是最窄的)
SELECT COUNT(*) FROM inventory FORCE INDEX (idx_biz_no);
实测同表 FORCE INDEX (idx_biz_no) 后从 9.3s 降到 1.1s(索引叶子比聚簇叶子小约 40%)。
方案 B:不要"每次都数",改成"异步缓存计数"。 后台首页的总数,本质是个"概览数",不需要 100% 精确。用一个物化视图/汇总表,定时任务每 5
0赞
0收藏
0评论
2浏览