索引是數據庫優化中最常用也是最重要的手段之一,通過索引通常可以解決大多數的SQL性能問題。
一、索引的存儲分類
索引是在MySQL的存儲引擎層中實現的,因此,每種存儲引擎的索引都不一定完全相同,也不是所有的存儲引擎都支持所有的索引類型。目前MySQL提供了以下4種索引:
- B-Tree索引:最常見的索引類型,大部分存儲引擎都支持B樹索引;
- HASH索引:只有Memory引擎支持,使用場景簡單;
- R-Tree索引(空間索引):是MyISAM的一個特殊索引類型,主要用於空間數據類型,使用較少;
- Full-text(全文索引):MyISA版本M的一個特殊索引類型,主要用於全文索引,InnoDB從MySQL5.6版本開始支持。
MySQL暫不支持函數索引,但是可以對列的前面某一部分進行索引,這稱爲前綴索引,這個特性可以大大縮小索引文件的大小,但是缺點是在排序ORDER BY 和分組GROUP BY 操作的時候無法使用。
這是一個創建前綴索引的例子
create index idx_title on film(title(10));
HASH索引相對較簡單,主要適用於Key-Value查詢,通常它要比B樹索引快;但是HASH索引不適用於範圍查詢,像不等符號、between等之類的。B-Tree索引相對較複雜一點,下面主要介紹的就是這種索引。
二、MySQL如何使用索引
B-Tree索引是最常見的索引,構造類似於二叉樹,能根據鍵值提供一行或者一個行集的快速訪問,通常只需要很少的讀操作就可以找到正確的行。可以利用B-Tree索引進行全關鍵字、關鍵字範圍和關鍵字前綴查詢。
爲避免混淆,將表rental上的索引rental_date重命名爲idx_rental_date
mysql> alter table rental drop index rental_date;
Query OK, 0 rows affected (0.02 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> alter table rental add index idx_rental_date (rental_date,inventory_id,customer_id);
Query OK, 0 rows affected (0.05 sec)
Records: 0 Duplicates: 0 Warnings: 0
2.1 MySQL種能夠使用索引的典型場景
1. 匹配全值
對索引中所有列都指定具體值,即對索引中的所有列都有等值匹配的條件。例如下面的例子指定表rental中三個字段的具體條件:
mysql> explain select * from rental where rental_date='2005-05-25 17:22:10' and inventory_id = 373 and customer_id = 343 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: rental
partitions: NULL
type: ref
possible_keys: idx_fk_inventory_id,idx_fk_customer_id,idx_rental_date
key: idx_rental_date
key_len: 10
ref: const,const,const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
可以看到,優化器選擇了複合索引idx_rental_date準確的進行了查找。
2. 匹配值的範圍查詢
對索引的值能夠進行範圍查找,例如下面的例子指定customer_id的範圍:
mysql> explain select * from rental where customer_id >= 373 and customer_id <= 400 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: rental
partitions: NULL
type: range
possible_keys: idx_fk_customer_id
key: idx_fk_customer_id
key_len: 2
ref: NULL
rows: 746
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
從上面的例子可以看出,range表示優化器選擇範圍查詢,idx_fk_customer_id表示優化器選擇該索引來加速訪問;
3. 最左匹配原則
使用索引查找時包含到最左邊的列,比如在col1、col2、col3三個列字段上有個聯合索引,當你想使用這個索引進行查找時你的等值查詢條件必須包含col1(最左邊的那一列),否則就無法用到該聯合索引,像col2、(col2+col3)就無法用到該索引。請看下面的例子:
給三列payment_date,amount,last_update創建一個聯合索引idx_payment_date
mysql> alter table payment add index idx_payment_date(payment_date,amount,last_update);
Query OK, 0 rows affected (0.09 sec)
Records: 0 Duplicates: 0 Warnings: 0
當等值條件包含最左列payment_date時可以用到索引idx_payment_date
mysql> explain select * from payment where payment_date = '2006-02-14 15:16:03' and last_update = '2006-02-15 22:12:32' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: payment
partitions: NULL
type: ref
possible_keys: idx_payment_date
key: idx_payment_date
key_len: 5
ref: const
rows: 182
filtered: 10.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
當等值條件不包含最左列payment_date時無法用到索引idx_payment_date
mysql> explain select * from payment where amount = 3.98 and last_update = '2006-02-15 22:12:32' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: payment
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 16125
filtered: 1.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
這個原則是MySQL中B-Tree索引使用的首要原則。
4. 僅僅對索引進行查詢
當查詢的列都包含在聯合索引的字段中時(且這時候要滿足最左原則能夠使用到該聯合索引),能夠直接根據索引獲取到所需的數據,而不需要通過索引回到表中再去找關聯數據。
mysql> explain select last_update from payment where payment_date = '2006-02-14 15:16:03' and amount=3.98 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: payment
partitions: NULL
type: ref
possible_keys: idx_payment_date
key: idx_payment_date
key_len: 8
ref: const,const
rows: 8
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
從例子中可以看到,last_update在聯合索引idx_payment_date的字段中,因此此處可以通過該索引直接獲得數據,Extra顯示Using index表明是覆蓋索引掃描,不需要回表,直接通過索引就能得到想要的數據,減少了不必要的訪問加快了速度。
5. 匹配列前綴
僅僅使用索引中的第一列,並且只包含索引第一列的開頭一部分進行查找。比如下面的例子
創建聯合前綴索引
mysql> create index idx_title_desc_part on film_text (title(10), description(20));
Query OK, 0 rows affected (0.04 sec)
Records: 0 Duplicates: 0 Warnings: 0
當要查找以AFRICAN開頭的電影時,使用到該索引
mysql> explain select title from film_text where title like 'AFRICAN%' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: film_text
partitions: NULL
type: range
possible_keys: idx_title_desc_part,idx_title_description
key: idx_title_desc_part
key_len: 32
ref: NULL
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
需要滿足一定的條件才能使用到該索引,Using where表示優化器需要通過索引回表查詢數據。
6. 索引匹配部分精確查找,部分範圍查找
例如下面的例子,查找條件一個精確一個範圍
mysql> explain select inventory_id from rental where rental_date = '2006-02-14 15:16:03' and customer_id >= 300 and customer_id <= 400 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: rental
partitions: NULL
type: ref
possible_keys: idx_fk_customer_id,idx_rental_date
key: idx_rental_date
key_len: 5
ref: const
rows: 182
filtered: 16.85
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
可以看到使用了聯合索引,使用了覆蓋索引掃描,而且還需要回表查詢數據。
7. 如果列名是索引,那麼使用“ 列名 is null ” 就會使用該索引
看下面的例子,rental_id這個列名就是索引
mysql> explain select * from payment where rental_id is null \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: payment
partitions: NULL
type: ref
possible_keys: fk_payment_rental
key: fk_payment_rental
key_len: 5
ref: const
rows: 5
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
8. Index Condition Pushdown 特性
ICP特性在MySQL5.6以後引入,pushdown表示操作下放,某些情況下的條件過濾操作下放到存儲引擎;這進一步優化了查詢。比如前面舉的例子中Extra字段的值爲Using index condition,就表明使用了ICP來進行優化查詢。
比如下面這條例子:
explain select inventory_id from rental where rental_date = '2006-02-14 15:16:03' and customer_id >= 300 and customer_id <= 400 \G
在之前版本中沒有使用ICP特性時,它的執行計劃是這樣的:優化器首先根據複合索引idx_rental_date的首字段rental_date過濾出符合條件rental_date = '2006-02-14 15:15:03' 的記錄,然後根據複合索引回表獲取記錄,最終再根據customer_id的條件過濾出最後的查詢結果(extra字段顯示爲using where)。
在5.6版本之後使用了ICP特性,它的執行計劃是這樣的:優化器首先根據複合索引idx_rental_date的首字段rental_date過濾出符合條件rental_date = '2006-02-14 15:15:03' 的記錄(key字段顯示爲idx_rental_date),然後把條件customer_id的過濾操作下推到存儲引擎來完成,在回表查詢記錄之前,不符合customer_id的記錄已經被過濾掉了,因此不必再訪問表中這些記錄,直接可以查詢到最終的結果(extra字段顯示爲using index condition)。
mysql> explain select inventory_id from rental where rental_date = '2006-02-14 15:16:03' and customer_id >= 300 and customer_id <= 400 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: rental
partitions: NULL
type: ref
possible_keys: idx_fk_customer_id,idx_rental_date
key: idx_rental_date
key_len: 5
ref: const
rows: 182
filtered: 16.85
Extra: Using where; Using index
1 row in set, 1 warning (0.00 sec)
2.2 存在索引但不能使用索引的場景
1. 使用%開頭的LIKE查詢不能夠使用B-Tree索引
mysql> explain select * from actor where last_name like '%NI%' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 200
filtered: 11.11
Extra: Using where
1 row in set, 1 warning (0.00 sec)
上面的關鍵字key爲NULL表示沒有使用索引,根據B樹索引的結構也能知道以%開頭的查詢自然沒法利用索引,一般推薦使用全文索引來解決類似全文索引問題;還有一種解決辦法就是通過索引的全掃描來過濾後再關聯到表中查詢,這通常是利用索引比表小的特性來減少大量的IO操作,比如下面的例子:InnoDB表上二級索引idx_last_name實際上存儲了字段lsat_name和actor_id,通過掃描二級索引idx_last_name獲得滿足條件lsat_name like ‘%NI%’ 的主鍵 actor_id列表,之後再根據主鍵回表去檢索記錄,這樣就避免了對全表的掃描。
mysql> explain select * from (select actor_id from actor where last_name like '%NI%' )a, actor b where a.actor_id = b.actor_id \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: index
possible_keys: PRIMARY
key: idx_actor_last_name
key_len: 137
ref: NULL
rows: 200
filtered: 11.11
Extra: Using where; Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: b
partitions: NULL
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 2
ref: sakila.actor.actor_id
rows: 1
filtered: 100.00
Extra: NULL
2 rows in set, 1 warning (0.00 sec)
2. 數據類型出現隱式轉換的時候也不會使用索引
特別是當列類型是字符串時這種情況更明顯,哪怕這一列有索引也不會使用,比如下面的例子;因此一定記住要用引號將字符常量括起來。
mysql> explain select * from actor where last_name = 1 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: ALL
possible_keys: idx_actor_last_name
key: NULL 沒有使用到索引
key_len: NULL
ref: NULL
rows: 200
filtered: 10.00
Extra: Using where
1 row in set, 3 warnings (0.00 sec)
mysql> explain select * from actor where last_name = '1' \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: actor
partitions: NULL
type: ref
possible_keys: idx_actor_last_name
key: idx_actor_last_name 使用到了索引
key_len: 137
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
3. 不滿足最左原則時不會使用索引
前面提到過,不滿足最左原則時不會使用到該索引。
4. 如果使用索引比全表掃描還慢就不會使用索引
比如查詢某一值時返回的記錄比值非常大,這時候全表掃描的效率反而比使用索引的效率更高,因此,mysql優化器就更傾向於使用全表掃描。也就是說在查詢時,篩選性越高越容易使用到索引,篩選性越低越不容易使用索引。
5. 用OR分開的條件中如果有任意一個沒法使用到索引那麼都不會使用索引
比如下面的例子中customer_id列有索引可用,而amount無法使用索引,因此amount必定要進行全表掃描,當進行全表掃描時就一併將customer_id列進行掃描了,因此沒必要再通過它的索引掃描增加IO訪問,一次全表掃描過濾條件就足夠。
mysql> explain select * from payment where customer_id = 203 or amount = 3.96 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: payment
partitions: NULL
type: ALL
possible_keys: idx_fk_customer_id
key: NULL
key_len: NULL
ref: NULL
rows: 16125
filtered: 10.15
Extra: Using where
1 row in set, 1 warning (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 | 15 |
+-----------------------+-------+
7 rows in set (0.00 sec)
如果索引正在工作,Handler_read_key的值將很高,這個值代表了一個行被索引值讀取的次數,很低的值表明增加索引得到的性能改善不高,因爲索引並不經常使用。
Handler_read_rnd_next 的值高則意味着查詢運行低效,應該建立索引補救。這個值的含義是在數據文件中讀下一行的請求數;如果正在進行大量的表掃描,那麼它的值就會非常高,這就說明索引不正確或是查詢沒有利用到索引。