在分析一个SQL的性能好坏时,除了执行计划,另外一个常看的指标是"Handler_read_*"相关变量。

  • Handler_read_key

  • Handler_read_first

  • Handler_read_last

  • Handler_read_next

  • Handler_read_prev

  • Handler_read_rnd

  • Handler_read_rnd_next

这七个变量,官方文档也有讲解,但很多人看完后,还是一头雾水。

下面结合具体的示例,来看看这七个变量的具体含义和区别。

Handler

首先说说什么是handler。

handler是一个类,里面按不同的功能模块定义了若干接口(具体可参考sql/handler.h)。其中,

DML操作相关的接口有:

  • write_row()

  • update_row()

  • delete_row()

  • delete_all_rows()

  • start_bulk_insert()

  • end_bulk_insert()

索引扫描相关的接口有:

  • index_read_map()

  • index_init()

  • index_end()

  • index_read_idx_map()

  • index_next()

  • index_prev()

  • index_first()

  • index_last()

  • index_next_same()

  • index_read_last_map()

  • read_range_first()

  • read_range_next()

其它相关接口可参考sql/handler.h,sql/handler.cc文件。

如此设计,有两点显而易见的好处:

1.  Server层与存储引擎层解耦。MySQL Server层在与存储引擎层交互时,无需关心存储引擎层的实现细节,直接调用handler对象的相关方法即可。

2.  降低了新引擎的引入门槛。如MyRocks。

测试数据

mysql> show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create Table: CREATE TABLE `t1` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `k` int(10) unsigned NOT NULL DEFAULT '0',
  `c` varchar(20) NOT NULL DEFAULT '',
  `pad` varchar(20) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `k` (`k`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8mb4
1 row in set (0.00 sec)

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|      100 |
+----------+
1 row in set (0.00 sec)

mysql> select * from t1 limit 6;
+----+---+--------+----------+
| id | k | c      | pad      |
+----+---+--------+----------+
|  1 | 1 | test_c | test_pad |
|  2 | 1 | test_c | test_pad |
|  3 | 1 | test_c | test_pad |
|  4 | 4 | test_c | test_pad |
|  5 | 5 | test_c | test_pad |
|  6 | 6 | test_c | test_pad |
+----+---+--------+----------+
6 rows in set (0.00 sec)

Handler_read_key

首先看看官档的解释

简而言之,即基于索引来定位记录,该值越大,代表基于索引的查询越多。

看看下面这个Demo。

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1 where id=1;
+----+---+--------+----------+
| id | k | c      | pad      |
+----+---+--------+----------+
|  1 | 1 | test_c | test_pad |
+----+---+--------+----------+
1 row in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.00 sec)

测试中有两点发现:

1.  无论是基于主键,还是二级索引进行等值查询,Handler_read_key都会加1。

2.  对于二级索引,如果返回了N条记录,Handler_read_next会相应加N。

Handler_read_first

首先看看官档的解释

读取索引的第一个值,该值越大,代表涉及索引全扫描的查询越多。

但是,这并不意味着查询利用到了索引,还需要结合其它的Handler_read_xxx来分析。

看看下面这个Demo

mysql> flush status;
Query OK, 0 rows affected (0.02 sec)

mysql> select * from t1 where c='0';
Empty set (0.10 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 101   |
+-----------------------+-------+
7 rows in set (0.01 sec)

 基于c来查询,c不是索引,故走的是全表扫描(通过Handler_read_rnd_next的值和表的总行数也可判断出来),但Handler_read_first和 Handler_read_key同样也增加了。

下面再看看另外一个Demo

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t2 where c='0';
Empty set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 0     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 101   |
+-----------------------+-------+
7 rows in set (0.00 sec)

t2和t1基本一样,只不过t2是MyISAM表,此时只增加了Handler_read_rnd_next。

之所以会这样,是因为t1是Innodb表,而Innodb是索引组织表,全表扫描实际上是基于主键来做的,所以Handler_read_first和Handler_read_key都会相应加1。

而t2是MyISAM表,MyISAM是堆表。

所以,单凭Handler_read_first很难评估查询的优劣。

Handler_read_last

首先看看官档的解释

和Handler_read_first相反,是读取索引的最后一个值。

该值增加基本上可以判定查询中使用了基于索引的order by desc子句。

看看下面两个Demo

1. 基于主键的正向排序

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1 order by id limit 10;
...
10 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 9     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.00 sec)

可以看到,增加的还是Handler_read_first和Handler_read_nex t。

2. 基于主键的反向排序

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1 order by id desc limit 10;
...
10 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 1     |
| Handler_read_last     | 1     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 9     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.00 sec)

此时增加的是Handler_read_last和Handler_read_ prev。

Handler_read_next

首先看看官档的解释

根据索引的顺序来读取下一行的值,常用于基于索引的范围扫描和order by limit子句中。

看看下面两个Demo

1. 基于索引的范围查询

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1 where k < 2;
+----+---+--------+----------+
| id | k | c      | pad      |
+----+---+--------+----------+
|  1 | 1 | test_c | test_pad |
|  2 | 1 | test_c | test_pad |
|  3 | 1 | test_c | test_pad |
+----+---+--------+----------+
3 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 3     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.00 sec)

2. 基于索引的order by子句

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1 force index(k) order by k limit 10;
...
10 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 9     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.01 sec)

注意:该查询使用了hint,强制索引,如果没用的话,会走全表扫描。

Handler_read_prev

首先看看官档的解释

根据索引的顺序来读取上一行的值。一般用于基于索引的order by desc子句中。

具体示例可参考Handler_read_last。

Handler_read_rnd

首先看看官档的解释

基于固定位置来读取记录。

关于固定位置的定义,不同的存储引擎有不同的说法

下面看看Handler_read_rnd的使用场景

大意是对记录基于某种标准进行排序,然后再根据它们的位置信息来遍历排序后的结果,这往往会导致表的随机读。

看看下面这个Demo

mysql> flush status;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from t1 order by rand() limit 10;
...
10 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 10    |
| Handler_read_rnd_next | 202   |
+-----------------------+-------+
7 rows in set (0.00 sec)

这里使用了order by rand()来生成随机记录。虽然只生成了10条记录,但Handler_read_rnd_next却调用了202次,比全表扫描还多,所以线上不建议使用order by rand()来生成随机记录。

Handler_read_rnd_next

首先看看官档的解释

读取下一行记录的次数,常用于全表扫描中。 

实现原理

看看下面两个Demo

1.  全表扫描,带有limit条件

mysql> flush status;
Query OK, 0 rows affected (0.03 sec)

mysql> select * from t1 limit 50;
...
50 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 50    |
+-----------------------+-------+
7 rows in set (0.01 sec)

2.  全表扫描

mysql> flush status;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t1;
...
100 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 0     |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 101   |
+-----------------------+-------+
7 rows in set (0.00 sec)

细心的童鞋可能会发现,limit 50时Handler_read_rnd_next为50,而不带limit条件时,Handler_read_rnd_next却为101,不是只有100行数据么?

实际上,在做全表扫描时,MySQL也并不知道表有多少行,它会不断调用handler::rnd_next()函数,直至记录返回完毕。

所以最后一次调用虽然为空,但毕竟调用了这个函数,故Handler_read_rnd_next需在表的总行数的基础上加1。

综合案例

最后,来个综合一点的案例,看看两表关联查询,各状态值又是怎样的呢?

在这里,会涉及到MySQL的Nest Loop算法。

mysql> flush status;
Query OK, 0 rows affected (0.01 sec)

mysql> select * from t1 t_1,t1 t_2 where t_1.k=t_2.k;
+-----+-----+--------+----------+-----+-----+--------+----------+
| id  | k   | c      | pad      | id  | k   | c      | pad      |
+-----+-----+--------+----------+-----+-----+--------+----------+
|   1 |   1 | test_c | test_pad |   1 |   1 | test_c | test_pad |
|   1 |   1 | test_c | test_pad |   2 |   1 | test_c | test_pad |
|   1 |   1 | test_c | test_pad |   3 |   1 | test_c | test_pad |
|   2 |   1 | test_c | test_pad |   1 |   1 | test_c | test_pad |
|   2 |   1 | test_c | test_pad |   2 |   1 | test_c | test_pad |
|   2 |   1 | test_c | test_pad |   3 |   1 | test_c | test_pad |
|   3 |   1 | test_c | test_pad |   1 |   1 | test_c | test_pad |
|   3 |   1 | test_c | test_pad |   2 |   1 | test_c | test_pad |
|   3 |   1 | test_c | test_pad |   3 |   1 | test_c | test_pad |
|   4 |   4 | test_c | test_pad |   4 |   4 | test_c | test_pad |
|   5 |   5 | test_c | test_pad |   5 |   5 | test_c | test_pad |
|   6 |   6 | test_c | test_pad |   6 |   6 | test_c | test_pad |
...
106 rows in set (0.01 sec)

mysql> desc select * from t1 t_1,t1 t_2 where t_1.k=t_2.k;
+----+-------------+-------+------+---------------+------+---------+----------------+------+-------+
| id | select_type | table | type | possible_keys | key  | key_len | ref            | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+----------------+------+-------+
|  1 | SIMPLE      | t_1   | ALL  | k             | NULL | NULL    | NULL           |  100 | NULL  |
|  1 | SIMPLE      | t_2   | ref  | k             | k    | 4       | slowtech.t_1.k |    1 | NULL  |
+----+-------------+-------+------+---------------+------+---------+----------------+------+-------+
2 rows in set (0.00 sec)

mysql> show status like '%Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 101   |
| Handler_read_last     | 0     |
| Handler_read_next     | 106   |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 101   |
+-----------------------+-------+
7 rows in set (0.00 sec)

通过执行计划可以看出,该查询的处理流程大致如下:

for each row in t_1  {
  for each row in t_2 where t_2.k = each_row.k {
         send to client
  }
}

接着,来分析下输出结果

1.  对t_1表进行全表扫描,全表扫描对应的状态值是Handler_read_first = 1,Handler_read_key = 1,Handle r_read_rnd_next = 101。

2.  因为t_1表有100行,所以会对t_2基于k值进行100次查询,对应的,Handler_read_key = 100。

3.  观察t1表k值的分布,当id=1,2,3时,k的值均为1,其它id的k值不相同。所以一共会返回106条记录,对应的,Handler_read_next = 106。

总结

1. Handler_read_key的值越大越好,代表基于索引的查询较多。

2. Handler_read_first,Handler_read_last,Handler_read_next,Handler_read_prev都会利用索引。但查询是否高效还需要结合其它Handler_read值来判断。

3. Handler_read_rnd不宜过大。

4. Handler_read_rnd_next不宜过大,过大的话,代表全表扫描过多,要引起足够的警惕。

参考资料

https://www.percona.com/blog/2010/06/15/what-does-handler_read_rnd-mean/

https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Handler_read_first

06-13 02:52