1. 程式人生 > MYSQL進階教學 ><p>如何高效高效能的選擇使用 MySQL 索引?</p>

<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_namelast_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. 小結

本小節介紹了高效使用索引的多種方法:獨立的列、字首索引、合適的索引列順序、覆蓋索引、使用索引實現排序。應該使用哪個索引,以及評估選擇不同索引的效能影響,需要不斷地學習。