handler_read_和handler_write_是存储引擎层物理操作计数器,反映索引定位、顺序/随机读写等底层动作次数,需结合sql类型、执行计划及explain交叉分析,单看数值易误判;例如handler_read_rnd高未必慢,但若伴随handler_read_key为0则表明未走索引。

Handler_read_* 和 Handler_write_* 这类指标不是“执行过程快照”,而是存储引擎层的底层操作计数器,必须结合 SQL 类型和执行计划一起看,否则容易误判性能瓶颈。
Handler 指标到底在统计什么
这些变量来自 SHOW STATUS LIKE 'Handler%',反映的是 InnoDB(或 MyISAM)存储引擎实际执行的“物理动作”次数,比如读取一行、定位索引页、写入临时表等。它不等于 SQL 层的逻辑行数,也不直接对应 EXPLAIN 的 rows 估算值。
常见关键指标含义:
-
Handler_read_first:首次读取索引最左端记录的次数(通常说明走了索引扫描起点) -
Handler_read_key:根据索引键值精确查找的次数(如WHERE id = ?走主键) -
Handler_read_next:按索引顺序读下一行(范围扫描、ORDER BY索引扫描时高频) -
Handler_read_rnd:按随机物理地址回表读行(Using filesort或Using temporary后回查主键时易触发) -
Handler_write:写入内部临时表或排序缓冲区的行数(高值往往意味着没走好索引)
怎么用 Handler 差异定位执行路径问题
对比两条语义相近但性能差异大的 SQL,看 Handler 计数变化比看耗时更敏感。例如:
SQL A:SELECT * FROM orders WHERE user_id = 123 AND status = 'paid' ORDER BY created_at DESC LIMIT 10;
SQL B(加了 force index):SELECT * FROM orders FORCE INDEX (idx_user_status_created) WHERE user_id = 123 AND status = 'paid' ORDER BY created_at DESC LIMIT 10;
执行后观察:
- 若 A 的
Handler_read_rnd显著高于 B,说明优化器选了错误索引,导致大量回表 + 随机 IO - 若 A 的
Handler_read_next极低但Handler_read_rnd很高,大概率是ORDER BY无法复用索引,触发了 filesort - 若 B 的
Handler_read_first+Handler_read_next总和 ≈ LIMIT 值(如 10),说明走了覆盖索引且无回表——这是理想路径
为什么不能只看 Handler_read_rnd 高就断定“慢”
这个值高本身不等于慢,要看上下文:
- 全表扫描的
SELECT *会把每行都算作一次Handler_read_rnd,但数据在 buffer pool 里时可能很快 - 批量 INSERT 大量数据时,
Handler_write必然飙升,这属于正常写放大,和查询无关 - 如果
Handler_read_rnd高但Handler_read_key和Handler_read_next为 0,说明根本没走索引,连“找起点”都没做 - MySQL 8.0+ 中,某些并行查询或 hash join 场景下,Handler 计数可能被多线程重复累加,需结合
performance_schema.events_statements_current核对线程 ID
实操建议:配合 EXPLAIN 和 slow log 交叉验证
单看 Handler 是盲人摸象。真实排查要三者联动:
- 先用
EXPLAIN FORMAT=JSON看是否用了预期索引、有没有Using temporary或Using filesort - 再开慢日志(
SET GLOBAL long_query_time = 0.1),抓出具体语句,确认执行时间分布 - 最后在该语句执行前后跑两次
SHOW STATUS LIKE 'Handler%',用差值分析其真实引擎行为 - 特别注意:Handler 计数是会话级累积的,测试前务必用新连接,或执行
FLUSH STATUS清零
真正难的不是读出数字,而是判断哪一行“不该被读”——比如一个本可走 Handler_read_key 的等值查询,却触发了上万次 Handler_read_rnd,那问题一定出在索引设计或隐式类型转换上。











