<p>如何高效高效能的選擇使用 MySQL 索引?</p>
想要實現高效能的查詢,正確的使用索引是基礎。本小節通過多個實際應用場景,幫助大家理解如何高效地選擇和使用索引。
1. 獨立的列
獨立的列,是指索引列不能是表示式的一部分,也不能是函式的引數。如果 SQL 查詢中的列不是獨立的,MySQL 不能使用該索引。
下面兩個查詢,MySQL 無法使用 id 列和 birth_date 列的索引。開發人員應該養成編寫 SQL 的好習慣,始終要將索引列單獨放在比較符號的左側。
mysql> select * from customer where id + 1 = 2;
mysql> select * from customer where to_days( birth_date) - to_days('2020-06-07') <= 10;
2. 字首索引
有時候需要對很長的字元列建立索引,這會使得索引變得很佔空間,效率也很低下。碰到這種情況,一般可以索引開始的部分字元,這樣可以節省索引產生的空間,但同時也會降低索引的選擇性。
那我們就要選擇足夠長的字首來保證較高的選擇性,但是為了節省空間,字首又不能太長,只要字首的基數,接近於完整列的基數即可。
Tips:索引的選擇性指,不重複的索引值(也叫基數,cardinality)和資料表的記錄總數的比值,索引的選擇性越高表示查詢效率越高。
完整列的選擇性:
mysql> select count (distinct last_name)/count(*) from customer;
+------------------------------------+
| count(distinct last_name)/count(*) |
+------------------------------------+
| 0.053 |
+------------------------------------+
不同字首長度的選擇性:
mysql> select count(distinct left(last_name,3))/count (*) left_3, count(distinct left(last_name,4))/count(*) left_4, count(distinct left(last_name,5))/count(*) left_5, count(distinct left(last_name,6))/count(*) left_6 from customer;
+--------+--------+--------+--------+
| left_3 | left_4 | left_5 | left_6 |
+--------+--------+--------+--------+
| 0.043| 0.046| 0.050| 0.051|
+--------+--------+--------+--------+
從上面的查詢可以看出,當前綴長度為 6 時,字首的選擇性接近於完整列的選擇性 0.053,再增加字首長度,能夠提升選擇性的幅度也很小了。
建立字首長度為6的索引:
mysql> alter table customer add index idx_last_name(last_name(6));
字首索引可以使索引更小更快,但同時也有缺點:無法使用字首索引做 order by 和 group by,也無法使用字首索引做覆蓋掃描。
3. 合適的索引列順序
在一個多列 B-Tree 索引中,索引列的順序表示索引首先要按照最左列進行排序,然後是第二列、第三列等。索引可以按照升序或降序進行掃描,以滿足精確符合列順序的 order by、group by 和 distinct 等的查詢需求。
索引的列順序非常重要,在不考慮排序和分組的情況下,通常我們會將選擇性最高的列放到索引最前面。
以下查詢,是應該建立一個 (last_name,first_name)
的索引,還是應該建立一個(first_name,last_name)
的索引?
mysql> select * from customer where last_name = 'Allen' and first_name = 'Cuba'
我們首先來計算下這兩個列的選擇性,看哪個列更高。
mysql> select count(distinct last_name)/count(*) last_name_selectivity, count(distinct first_name)/count(*) first_name_selectivity from customer;
+-----------------------+------------------------+
| last_name_selectivity | first_name_selectivity |
+-----------------------+------------------------+
| 0.053 | 0.372 |
+-----------------------+------------------------+
很明顯,列 first_name 的選擇性更高,所以選擇 first_name 作為索引列的第一列:
mysql> alter table customer add index idx1_customer(first_name,last_name);
4. 覆蓋索引
如果一個索引包含所有需要查詢的欄位,稱之為覆蓋索引。由於覆蓋索引無須回表,通過掃描索引即可拿到所有的值,它能極大地提高查詢效率:索引條目一般比資料行小的多,只通過掃描索引即可滿足查詢需求,MySQL 可以極大地減少資料的訪問量。
表 customer 有一個多列索引 (first_name,last_name)
,以下查詢只需要訪問 first_name
和last_name
,這時就可以通過這個索引來實現覆蓋索引。
mysql> explain select last_name, first_name from customer\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer
partitions: NULL
type: index
possible_keys: NULL
key: idx1_customer
key_len: 186
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
當查詢為覆蓋索引查詢時,在 explain 的 extra 列可以看到 Using index。
5. 使用索引實現排序
MySQL 可以通過排序操作,或者按照索引順序掃描來生成有序的結果。如果 explain 的 type 列的值為index,說明該查詢使用了索引掃描來做排序。
order by 和查詢的限制是一樣的,需要滿足索引的最左字首要求,否則無法使用索引進行排序。只有當索引的列順序和 order by 子句的順序完全一致,並且所有列的排序方向(正序或倒序)都一致,MySQL才能使用索引來做排序。如果查詢是多表關聯,只有當 order by 子句引用的欄位全部為第一個表時,才能使用索引來做排序。
以表 customer 為例,我們來看看哪些查詢可以通過索引進行排序。
mysql> create table customer(
id int,
last_name varchar(30),
first_name varchar(30),
birth_date date,
gender char(1),
key idx_customer(last_name,first_name,birth_date)
);
5.1 可以通過索引進行排序的查詢
索引的列順序和 order by 子句的順序完全一致:
mysql> explain select last_name,first_name from customer order by last_name, first_name, birth_date\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer
partitions: NULL
type: index
possible_keys: NULL
key: idx_customer
key_len: 190
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index
1 row in set, 1 warning (0.00 sec)
索引的第一列指定為常量:
從 explain 可以看到沒有出現排序操作(filesort):
mysql> explain select * from customer where last_name = 'Allen' order by first_name, birth_date\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer
partitions: NULL
type: ref
possible_keys: idx_customer
key: idx_customer
key_len: 93
ref: const
rows: 1
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
索引的第一列指定為常量,使用第二列排序:
mysql> explain select * from customer where last_name = 'Allen' order by first_name desc\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer
partitions: NULL
type: ref
possible_keys: idx_customer
key: idx_customer
key_len: 93
ref: const
rows: 1
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
索引的第一列為範圍查詢,order by 使用的兩列為索引的最左字首:
mysql> explain select * from customer where last_name between 'Allen' and 'Bush' order by last_name,first_name\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer
partitions: NULL
type: range
possible_keys: idx_customer
key: idx_customer
key_len: 93
ref: NULL
rows: 1
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.00 sec)
5.2 不能通過索引進行排序的查詢
使用兩種不同的排序方向:
mysql> explain select * from customer where last_name = 'Allen' order by first_name desc, birth_date asc\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer
partitions: NULL
type: ref
possible_keys: idx_customer
key: idx_customer
key_len: 93
ref: const
rows: 1
filtered: 100.00
Extra: Using index condition; Using filesort
1 row in set, 1 warning (0.00 sec)
order by 子句引用了一個不在索引的列:
mysql> explain select * from customer where last_name = 'Allen' order by first_name, gender\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: customer
partitions: NULL
type: ref
possible_keys: idx_customer
key: idx_customer
key_len: 93
ref: const
rows: 1
filtered: 100.00
Extra: Using index condition; Using filesort
1 row in set, 1 warning (0.00 sec)
where 條件和 order by 的列無法組成索引的最左字首:
mysql> explain select * from customer where last_name = 'Allen' order by birth_date\G
第一列是範圍查詢,where 條件和 order by 的列無法組成索引的最左字首:
mysql> explain select * from customer where last_name between 'Allen' and 'Bush' order by first_name\G
第一列是常量,第二列是範圍查詢(多個等於也是範圍查詢):
mysql> explain select * from customer where last_name = 'Allen' and first_name in ('Cuba','Kim') order by birth_date\G
6. 小結
本小節介紹了高效使用索引的多種方法:獨立的列、字首索引、合適的索引列順序、覆蓋索引、使用索引實現排序。應該使用哪個索引,以及評估選擇不同索引的效能影響,需要不斷地學習。