SHOW SESSION STATUS 与 Handler 变量
会话级内存计数变量SESSION STATUS 中,'Handler%'开头的这一组变量、反映了存储引擎取数的一些细节,可以用来分析SQL性能。
FLUSH STATUS;
select count(*) from order_items oi ;
SHOW SESSION STATUS LIKE 'Handler%';
查询结果:
Variable_name |Value |
--------------------------|-------|
Handler_commit |1 |
Handler_delete |0 |
Handler_discover |0 |
Handler_external_lock |2 |
Handler_mrr_init |0 |
Handler_prepare |0 |
Handler_read_first |1 |
Handler_read_key |1 |
Handler_read_last |0 |
Handler_read_next |3000000|
Handler_read_prev |0 |
Handler_read_rnd |0 |
Handler_read_rnd_next |0 |
Handler_rollback |0 |
Handler_savepoint |0 |
Handler_savepoint_rollback|0 |
Handler_update |0 |
Handler_write |0 |
- FLUSH STATUS 清的是什么
MySQL 维护着一批会话级状态计数器(就是一堆内存变量),记录当前这个连接执行过多少查询、扫过多少行、发生过多少次排序等等。FLUSH STATUS 做了两件事:
① 把当前会话的计数"加总"到全局计数器(累加,不是清空全局)
② 把当前会话的计数器全部清零
所以它不写任何系统表、不落盘,纯内存操作。目的就是:清掉历史干扰,让接下来的计数只反映你接下来那条 SQL 的行为。 这也是为什么这个套路是三连:FLUSH → 执行 SQL → SHOW。
- SHOW SESSION STATUS 是啥意思
读当前会话的计数器。SESSION 是作用域,另一个选项是 GLOBAL(全实例累计):
SHOW SESSION STATUS LIKE 'Handler%'; -- 当前这个连接的计数
SHOW GLOBAL STATUS LIKE 'Handler%'; -- 整个 MySQL 实例的累计计数
LIKE 'Handler%' 是过滤——Handler 前缀的变量记录的是存储引擎"怎么取行"的底层动作。
- 这组数据是教科书级别的
Handler_read_key = 1 ← 1 次 B+Tree 下潜:定位到 idx_created_at 第一个叶子
Handler_read_first = 1 ← 读了第一个索引条目
Handler_read_next = 3,000,000 ← 沿叶子链表读了 300 万次"下一个"
Handler_read_rnd_next = 0 ← 没有全表扫描
完美还原了刚才那 2 秒在干什么:从索引最左叶子出发,沿双向链表一路走到最右,数了 300 万条。注意 read_next 精确是 3,000,000 而 EXPLAIN 估的 rows 是 2,989,004——这就是"Handler 变量是真实值、EXPLAIN rows 是估算值"的直观对照。
判读口诀(以后自己排查用)
Handler_read_next 大 → 索引范围扫描/全索引扫描(B+Tree 顺着链表走)
Handler_read_rnd_next 大 → 全表扫描或回表无序读(按页随机跳)
Handler_read_key 大 → 点查/等值匹配多
执行EXPLAIN,
id|select_type|table|...|type |possible_keys|key |key_len|...
1|SIMPLE |oi |...|index| |idx_created_at|5 |...
↑
实际使用的索引
用了idx_created_at,所以这个count(*)是做了一次全索引扫描:
① 全表扫描 (Full Table Scan)
扫的是聚集索引 —— 表数据本身(所有列都在里面)
EXPLAIN: type=ALL
Handler 计数器: Handler_read_rnd_next 逐行累加
② 全索引扫描 (Full Index Scan)
扫的是某个二级索引 —— 只有索引列+主键
EXPLAIN: type=index
Handler 计数器: Handler_read_first 定位 + Handler_read_next 沿链表走
COUNT() 是*②全索引扫描:扫的是 idx_created_at 这棵二级索引树(77MB),不是表数据(几百 MB 含所有列)。所以 read_next = 3,000,000 而 read_rnd_next = 0。
计数器名字背后的含义
read_rnd_next = "随机地读下一行" → 按聚集索引页的物理顺序一行行读
(叫 rnd 是因为从执行器视角看,它没说"按哪个键序",就是表里的下一行)
read_next = "按索引键序读下一个" → 顺着某棵 B+Tree 的叶子链表走
这个区分为什么重要
两种都是 O(N),但成本差不少:
全索引扫描:只读 77MB 的二级索引,每页能装更多条目
全表扫描: 要读几百 MB 的完整表数据(TEXT 列、所有字段)
这正是 COUNT(*) 挑最小二级索引的原因
浙公网安备 33010602011771号