技術分享 | InnoDB Handler_read_* 變量解釋

做者:高鵬
文章末尾有他著做的《深刻理解 MySQL 主從原理 32 講》,深刻透徹理解 MySQL 主從,GTID 相關技術知識。
源碼版本:percona 5.7.14
本文爲學習記錄,可能有誤請諒解。

本文建議PC端觀看,效果更佳。html

1、Handler_read_* 值的實質

內部表示以下:mysql

{"Handler_read_first",       (char*) offsetof(STATUS_VAR, ha_read_first_count),     SHOW_LONGLONG_STATUS,    SHOW_SCOPE_ALL},
{"Handler_read_key",         (char*) offsetof(STATUS_VAR, ha_read_key_count),       SHOW_LONGLONG_STATUS,    SHOW_SCOPE_ALL},
{"Handler_read_last",        (char*) offsetof(STATUS_VAR, ha_read_last_count),      SHOW_LONGLONG_STATUS,    SHOW_SCOPE_ALL},
{"Handler_read_next",        (char*) offsetof(STATUS_VAR, ha_read_next_count),      SHOW_LONGLONG_STATUS,    SHOW_SCOPE_ALL},
{"Handler_read_prev",        (char*) offsetof(STATUS_VAR, ha_read_prev_count),      SHOW_LONGLONG_STATUS,    SHOW_SCOPE_ALL},
{"Handler_read_rnd",         (char*) offsetof(STATUS_VAR, ha_read_rnd_count),       SHOW_LONGLONG_STATUS,    SHOW_SCOPE_ALL},
{"Handler_read_rnd_next",    (char*) offsetof(STATUS_VAR, ha_read_rnd_next_count),  SHOW_LONGLONG_STATUS,    SHOW_SCOPE_ALL},

實際上這些變量都是 MySQL 層定義出來的,由於 MySQL 能夠包含多個存儲引擎。所以這些值如何增長鬚要在引擎層的接口中自行實現,也就是說各個引擎都有本身的實現,在 MySQL 層進行彙總,所以這些值不是某個引擎特有的,打個比方若是有 Innodb 和 MyISAM 引擎,那麼這些值是兩個引擎的總和。本文將以 Innodb 爲主要學習對象進行解釋。sql

2、各個值的解釋

1. Handler_read_key

  • 內部表示:ha_read_key_count
  • Innodb 更改接口:ha_innobase::index_read
  • 文檔解釋:The number of requests to read a row based on a key. If this value is high, it is a good indication that your tables are properly indexed for your queries.
  • 源碼函數解釋:Positions an index cursor to the index specified in the handle. Fetches the row if any.
  • 做者解釋:這個函數是訪問索引的時候定位到值所在的位置用到的函數,由於必需要知道讀取索引的開始位置才能向下訪問。

2. Handler_read_next

  • 內部表示:ha_read_next_count
  • Innodb 更改接口:ha_innobase::index_next_same ha_innobase::index_next
  • 文檔解釋:The number of requests to read the next row in key order. This value is incremented if you are

querying an index column with a range constraint or if you are doing an index scan.mvc

  • 源碼函數解釋:

index_next - Reads the next row from a cursor, which must have previously been positioned using index_read.
index_next_same - Reads the next row matching to the key value given as the parameter.函數

  • 做者解釋:訪問索引的下一條數據封裝的 ha_innobase::general_fetch 函數,index_next_same 和 index_next 不一樣在於訪問的方式不同,好比範圍 range 查詢須要用到和索引全掃描也會用到 index_next,而 ref 訪問方式會使用 index_next_same

3. Handler_read_first

  • 內部表示:ha_read_first_count
  • Innodb 更改接口:ha_innobase::index_first
  • 文檔解釋:The number of times the first entry in an index was read. If this value is high, it suggests that the

server is doing a lot of full index scans; for example, SELECT col1 FROM foo, assuming that col1
is indexed學習

  • 源碼函數解釋:Positions a cursor on the first record in an index and reads the corresponding row to buf.
  • 做者解釋:定位索引的第一條數據,實際上也是封裝的 ha_innobase::index_read 函數(如全表掃描/全索引掃描調用)

4. Handler_read_rnd_next

  • 內部表示:ha_read_rnd_next_count
  • Innodb 更改接口:ha_innobase::rnd_next
  • 文檔解釋:The number of requests to read the next row in the data file. This value is high if you are doing a lot of table scans. Generally this suggests that your tables are not properly indexed or that your queries

are not written to take advantage of the indexes you have.測試

  • 源碼函數解釋:Reads the next row in a table scan (also used to read the FIRST row in a table scan).
  • 做者解釋:全表掃描訪問下一條數據,實際上也是封裝的 ha_innobase::general_fetch,在訪問以前會調用 ha_innobase::index_first

5. Handler_read_rnd

  • 內部表示:ha_read_rnd_count
  • Innodb 更改接口:ha_innobase::rnd_pos
  • Memory 更改接口:ha_heap::rnd_pos
  • 文檔解釋:The number of requests to read a row based on a fixed position. This value is high if you are doing a lot of queries that require sorting of the result. You probably have a lot of queries that require MySQL to scan entire tables or you have joins that do not use keys properly.
  • 做者解釋:這個狀態值在我測試期間只發現對臨時表作排序的時候會用到,並且是 Memory 引擎的,具體只能按照文檔理解了。

6. 其餘

最後 2 個簡單說一下fetch

  • Handler_read_prev

Innodb 接口爲 ha_innobase::index_prev 訪問索引的上一條數據,實際上也是封裝的 ha_innobase::general_fetch 函數,用於 ORDER BY DESC 索引掃描避免排序,內部狀態值 ha_read_prev_count 增長。ui

  • Handler_read_last

Innodb 接口爲 ha_innobase::index_last 訪問索引的最後一條數據做爲定位,實際上也是封裝的 ha_innobase::index_read 函數,用於 ORDER BY DESC 索引掃描避免排序,內部狀態值 ha_read_last_count 增長。this

3、經常使用查詢測試

1. 測試用例

mysql> show create table z1;
+-------+-------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                              |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------+
| z1    | CREATE TABLE `z1` (
  `a` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL,
  KEY `a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+-------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> show create table z10;
+-------+------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table                                                                                                                                   |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------+
| z10   | CREATE TABLE `z10` (
  `a` int(11) DEFAULT NULL,
  `name` varchar(20) DEFAULT NULL,
  KEY `a_idx` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1 |
+-------+------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)
mysql> select count(*) from z1;
+----------+
| count(*) |
+----------+
|    56415 |
+----------+
1 row in set (5.27 sec)

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

2. 全表掃描

mysql> desc select * from z1;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows  | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
|  1 | SIMPLE      | z1    | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 56650 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql> pager cat >>/dev/null
PAGER set to 'cat >>/dev/null'
mysql> flush status;
Query OK, 0 rows affected (0.10 sec)
mysql> select * from z1;
56415 rows in set (4.05 sec)
mysql> pager;
Default pager wasn't set, using stdout.
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 | 56416 |
+-----------------------+-------+
7 rows in set (0.01 sec)

Handler_read_first 增長 1 次用於初次定位,Handler_read_key 增長 1 次,Handler_read_rnd_next 增長掃描行數。咱們前面說過由於 ha_innobase::index_first 也是封裝的 ha_innobase::index_read 所以都須要 +1。

3. 全索引掃描

mysql> desc select a from z1;
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows  | filtered | Extra       |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-------------+
|  1 | SIMPLE      | z1    | NULL       | index | NULL          | a    | 5       | NULL | 56650 |   100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

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

mysql> pager cat >>/dev/null
PAGER set to 'cat >>/dev/null'
mysql> select a from z1;
56415 rows in set (4.57 sec)

mysql> pager
Default pager wasn't set, using stdout.
mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 56415 |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.01 sec)

Handler_read_first 增長1次用於初次定位,Handler_read_key 增長 1 次,Handler_read_next 增長掃描行數用於連續訪問接下來的行。咱們前面說過由於 ha_innobase::index_first 也是封裝的 ha_innobase::index_read 所以都須要 +1。

4. 索引 ref 訪問

我這裏由於是測試索引全是等於 10 的加上了 force index

mysql>  desc select  * from z1 force index(a) where a=10;
+----+-------------+-------+------------+------+---------------+------+---------+-------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref   | rows  | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+-------+-------+----------+-------+
|  1 | SIMPLE      | z1    | NULL       | ref  | a             | a    | 5       | const | 28325 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+-------+-------+----------+-------+
1 row in set, 1 warning (0.01 sec)

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

mysql> pager cat >>/dev/null
PAGER set to 'cat >>/dev/null'
mysql> select  * from z1 force index(a) where a=10;
56414 rows in set (32.39 sec)
mysql> pager
Default pager wasn't set, using stdout.
mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 56414 |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.06 sec)

Handler_read_key 增長 1 次這是用於初次定位,Handler_read_next 增長掃描行數次數用於接下來的數據訪問。

5. 索引 range 訪問

mysql> desc select  * from z1 force index(a) where a>9 and a<12;
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows  | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-----------------------+
|  1 | SIMPLE      | z1    | NULL       | range | a             | a    | 5       | NULL | 28325 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

mysql>  pager cat >>/dev/null
PAGER set to 'cat >>/dev/null'
mysql> select  * from z1 force index(a) where a>9 and a<12;
56414 rows in set (47.54 sec)
mysql> show status like 'Handler_read%';
7 rows in set (0.03 sec)
mysql>  pager
Default pager wasn't set, using stdout.
mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 0     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 56414 |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.02 sec)

Handler_read_key 增長 1 次這是用於初次定位,Handler_read_next 增長掃描行數次數用於接下來的數據訪問。

6. 被驅動錶帶索引訪問

mysql> desc select * from z1 STRAIGHT_JOIN z10 force index(a_idx) on z1.a=z10.a;
+----+-------------+-------+------------+------+---------------+-------+---------+-----------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref       | rows  | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-----------+-------+----------+-------------+
|  1 | SIMPLE      | z1    | NULL       | ALL  | a             | NULL  | NULL    | NULL      | 56650 |   100.00 | Using where |
|  1 | SIMPLE      | z10   | NULL       | ref  | a_idx         | a_idx | 5       | test.z1.a |    10 |   100.00 | NULL        |
+----+-------------+-------+------------+------+---------------+-------+---------+-----------+-------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)

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

mysql> pager cat >> /dev/null
PAGER set to 'cat >> /dev/null'
mysql>  select * from z1 STRAIGHT_JOIN z10 force index(a_idx) on z1.a=z10.a;
112828 rows in set (1 min 21.21 sec)

mysql> pager
Default pager wasn't set, using stdout.
mysql>  show status like 'Handler_read%';
+-----------------------+--------+
| Variable_name         | Value  |
+-----------------------+--------+
| Handler_read_first    | 1      |
| Handler_read_key      | 56416  |
| Handler_read_last     | 0      |
| Handler_read_next     | 112828 |
| Handler_read_prev     | 0      |
| Handler_read_rnd      | 0      |
| Handler_read_rnd_next | 56416  |
+-----------------------+--------+
7 rows in set (0.00 sec)

Handler_read_first 增長一次做爲驅動表 z1 全表掃描定位的開始,接下來 Handler_read_rnd_next 掃描所有記錄,每次掃描一次在 z10 表經過索引 a_idx 定位一次 Handler_read_key 增長 1 次,而後接下來進行索引 a_idx 進行數據查找 Handler_read_next 增長爲掃描的行數。

7. 索引避免排序正向和反向

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

mysql> pager cat >> /dev/null
PAGER set to 'cat >> /dev/null'
mysql> select * from z1 force index(a) order by a;
56415 rows in set (27.39 sec)

mysql> pager
Default pager wasn't set, using stdout.
mysql> show status like 'Handler_read%';
+-----------------------+-------+
| Variable_name         | Value |
+-----------------------+-------+
| Handler_read_first    | 1     |
| Handler_read_key      | 1     |
| Handler_read_last     | 0     |
| Handler_read_next     | 56415 |
| Handler_read_prev     | 0     |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.01 sec)

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

mysql> desc  select * from z1 force index(a) order by a desc;
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-------+
| id | select_type | table | partitions | type  | possible_keys | key  | key_len | ref  | rows  | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-------+
|  1 | SIMPLE      | z1    | NULL       | index | NULL          | a    | 5       | NULL | 56650 |   100.00 | NULL  |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql> pager cat >> /dev/null
PAGER set to 'cat >> /dev/null'
mysql>  select * from z1 force index(a) order by a desc;
56415 rows in set (24.94 sec)

mysql> pager
Default pager wasn't set, using stdout.
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     | 56415 |
| Handler_read_rnd      | 0     |
| Handler_read_rnd_next | 0     |
+-----------------------+-------+
7 rows in set (0.01 sec)

不用過多解釋,能夠看到 Handler_read_last 和 Handler_read_prev 的用途。

4、總結

  • Handler_read_rnd_next 一般表明着全表掃描。
  • Handler_read_first 一般表明着全表或者全索引掃描。
  • Handler_read_next 一般表明着合理的使用了索引或者全索引掃描。
  • Handler_read_key 無論全表全索引或者正確使用的索引實際上都會增長,只是一次索引定位而已。
  • Innodb 中全表掃描也是主鍵的全索引掃描。

順序訪問的一條記錄實際上都是調用 ha_innobase::general_fetch 函數,另一個功能 innodb_thread_concurrency 參數的功能就在裏面實現,下次再說。

5、參考棧幀

全表掃描

mysql> desc select * from z1 ;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
| 1 | SIMPLE | z1 | NULL | ALL | NULL | NULL | NULL | NULL | 56650 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------+
1 row in set, 1 warning (0.00 sec)

第一次:
#0 row_search_mvcc (buf=0x7fff2ccc9380 "\377", mode=PAGE_CUR_G, prebuilt=0x7fff2cd4bb40, match_mode=0, direction=0)
at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/row/row0sel.cc:4479
#1 0x00000000019b3051 in ha_innobase::index_read (this=0x7fff2cd32480, buf=0x7fff2ccc9380 "\377", key_ptr=0x0, key_len=0, find_flag=HA_READ_AFTER_KEY)
at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/handler/ha_innodb.cc:9104
#2 0x00000000019b4374 in ha_innobase::index_first (this=0x7fff2cd32480, buf=0x7fff2ccc9380 "\377")
at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/handler/ha_innodb.cc:9551
#3 0x00000000019b462c in ha_innobase::rnd_next (this=0x7fff2cd32480, buf=0x7fff2ccc9380 "\377")
at /root/mysql5.7.14/percona-server-5.7.14-7/storage/innobase/handler/ha_innodb.cc:9656
#4 0x0000000000f66fa2 in handler::ha_rnd_next (this=0x7fff2cd32480, buf=0x7fff2ccc9380 "\377") at /root/mysql5.7.14/percona-server-5.7.14-7/sql/handler.cc:3099
#5 0x00000000014c61b6 in rr_sequential (info=0x7fff2c0026a0) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/records.cc:520
#6 0x000000000155f2a4 in join_init_read_record (tab=0x7fff2c002650) at /root/mysql5.7.14/percona-server-5.7.14-7/sql/sql_executor.cc:2481
#7 0x000000000155c381 in sub_select (join=0x7fff2c001f70, qep_tab=0x7fff2c002650, end_of_records=false)
at /root/mysql5.7.14/percona-server-5.7.14-7/sql/sql_executor.cc:1271
#8 0x000000000155bd

最後推薦高鵬的專欄《深刻理解MySQL主從原理 32講》,想要透徹瞭解學習MySQL 主從原理的朋友不容錯過。

相關文章
相關標籤/搜索