MySQL InnoDB的二級索引(Secondary Index)會自動補齊主鍵,將主鍵列追加到二級索引列后面。詳細一點來說,InnoDB的二級索引(Secondary Index)除了存儲索引列key值,還存儲着主鍵的值(而不是指向主鍵的指針)。為什么這樣做呢?因為InnoDB是以聚集索引方式組織數據的存儲,即主鍵值相鄰的數據行緊湊的存儲在一起(索引組織表)。當數據行移動或者發生頁分裂的時候,可以減少大量的二級索引維護工作。InnoDB移動行時,無需更新二級索引。我們以官方文檔的例子來測試:
CREATE TABLE t1 (
i1 INT NOT NULL DEFAULT 0,
i2 INT NOT NULL DEFAULT 0,
d DATE DEFAULT NULL,
PRIMARY KEY (i1, i2),
INDEX k_d (d)
) ENGINE = InnoDB;
如上所示,這個t1表包含主鍵和二級索引k_d,二級索引k_d(d)的元組在InnoDB內部實際被擴展成(d,i1,i2),即包含主鍵值。因此在設計主鍵的時候,常見的一條設計原則是要求主鍵字段盡量簡短,以避免二級索引過大(因為二級索引會自動補齊主鍵字段)。
優化器會考慮擴展二級索引的主鍵列,確定什么時候使用以及如何使用該索引。 這樣可以產生更高效的執行計划和達到更好的性能。有不少博客介紹索引擴展是從MySQL5.6.9開始引入的。不過個人還沒有在官方文檔看到相關資料。
優化器可以用擴展的二級索引來進行ref,range,index_merge等類型索引訪問(index access),松散的索引掃描(index sacns),連接和排序優化,以及min()/max()優化。
我們先來插入測試數據(腳本來自官方文檔):
INSERT INTO t1 VALUES
(1, 1, '1998-01-01'), (1, 2, '1999-01-01'),
(1, 3, '2000-01-01'), (1, 4, '2001-01-01'),
(1, 5, '2002-01-01'), (2, 1, '1998-01-01'),
(2, 2, '1999-01-01'), (2, 3, '2000-01-01'),
(2, 4, '2001-01-01'), (2, 5, '2002-01-01'),
(3, 1, '1998-01-01'), (3, 2, '1999-01-01'),
(3, 3, '2000-01-01'), (3, 4, '2001-01-01'),
(3, 5, '2002-01-01'), (4, 1, '1998-01-01'),
(4, 2, '1999-01-01'), (4, 3, '2000-01-01'),
(4, 4, '2001-01-01'), (4, 5, '2002-01-01'),
(5, 1, '1998-01-01'), (5, 2, '1999-01-01'),
(5, 3, '2000-01-01'), (5, 4, '2001-01-01'),
(5, 5, '2002-01-01');
#默認情況下,索引擴展(use_index_extensions)選項是開啟的。可以在當前會話通過修改優化器開關optimizer_switch開啟、關閉此選項。
mysql> show variables like '%optimizer_switch%';
mysql> SET optimizer_switch = 'use_index_extensions=off';
Query OK, 0 rows affected (0.00 sec)
mysql> EXPLAIN
-> SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01';
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
| 1 | SIMPLE | t1 | ref | PRIMARY,k_d | k_d | 4 | const | 5 | Using where; Using index |
+----+-------------+-------+------+---------------+------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)
這種情況下,優化器不會使用主鍵,因為主鍵由字段(i1,i2)組成,但是該查詢中沒有引用t2字段;優化器會選擇二級索引 k_d(d) 。
我們將use_index_extensions選項在當前會話開啟,那么SQL語句的執行計划會怎樣變化呢?
mysql> SET optimizer_switch = 'use_index_extensions=on';
Query OK, 0 rows affected (0.00 sec)
mysql> EXPLAIN
-> SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01';
+----+-------------+-------+------+---------------+------+---------+-------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+-------------+------+-------------+
| 1 | SIMPLE | t1 | ref | PRIMARY,k_d | k_d | 8 | const,const | 1 | Using index |
+----+-------------+-------+------+---------------+------+---------+-------------+------+-------------+
1 row in set (0.00 sec)
mysql>
當use_index_extensions=off的時候,僅使用索引k_d中d列的數據,忽略了擴展的主鍵列的數據。而use_index_extensions=on時,使用了k_d索引中(i1,i2,d)三列的數據。可以從上面兩種情況下的explain輸出結果中信息得以驗證。
key_len:由4變到8,說明不僅僅使用了d列上的索引,而且使用了擴展的主鍵i1列的數據
ref:由const變為”const,const”, 使用了索引的兩部分。
rows:從5變為1,表明InnoDB只需要檢查更少的數據行就可以產生結果集。
Extra:”Using index,Using where” 變為”Using index”。通過索引覆蓋就完成數據查詢,而不需要讀取任何的數據行。官方文檔的介紹如下:
The Extra value changes from Using where; Using index to Using index. This means that rows can be read using only the index, without consulting columns in the data row.
其實關於這兩者的區別,查了很多資料都沒有徹底搞清楚”Using index,Using where”與”Using index”的區別。此處不做展開。
另外,從status信息中“Handler_read_%”相關狀態值可以觀察實際執行過程中索引和數據行的訪問統計。
flush table 關閉已打開的數據表,並清除緩存(表緩存和查詢緩存)。
flush status 把status計數器清零。
Handler_read_key: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.
Handler_read_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.(此選項表明在進行索引掃描時,按照索引從數據文件里取數據的次數。)
關閉use_index_extensions情況下,status的統計信息
mysql> SET optimizer_switch = 'use_index_extensions=off';
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH TABLE t1;
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH STATUS;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01';
+----------+
| COUNT(*) |
+----------+
| 1 |
+----------+
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 | 5 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
7 rows in set (0.00 sec)
開啟use_index_extensions情況下,status的統計信息
mysql> SET optimizer_switch = 'use_index_extensions=on';
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH TABLE t1;
Query OK, 0 rows affected (0.00 sec)
mysql> FLUSH STATUS;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT COUNT(*) FROM t1 WHERE i1 = 3 AND d = '2000-01-01';
+----------+
| COUNT(*) |
+----------+
| 1 |
+----------+
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 | 1 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+
7 rows in set (0.00 sec)
mysql>
對比兩個執行計划,發現Handler_read_next的值從5變為1,表明索引的訪問效率更高了,減少了數據行的讀取次數。
本文結合官方文檔Use of Index Extensions和自己理解整理。
參考資料:
https://docs.oracle.com/cd/E17952_01/mysql-5.6-en/index-extensions.html
https://dev.mysql.com/doc/refman/5.6/en/index-extensions.html
http://blog.51cto.com/huanghualiang/1557306
http://reckey.iteye.com/blog/2258450