MySQL優化系列6-索引優化

備注:測試數據庫版本為MySQL 8.0

一.索引介紹

要理解MySQL中索引是如何工作的,最簡單的方法就是去看看一本書的“索引”部分:如果想在一本書中找到某個特定主題,一般會先看書的“索引”,找到對應的頁碼。

考慮現在MySQL的存儲引擎都是InnoDB,其它引擎很少使用,下面的討論都是圍繞InnoDB存儲引擎展開。

在MySQL中,存儲引擎用類似的方法使用索引,其先在索引中找到對應值,然后根據匹配的索引記錄找到對應的數據行。。假如要運行下面的查詢:

mysql> SELECT first_name FROM sakila.actor WHERE actor_id=5;

如果在actor_id列上建有索引,則MySQL將使用該索引找到actor_id為5的行,也就是說,MySQL先在索引上按值進行查找,然后返回所有包含該值的數據行。

索引可以包含一個或多個列的值。如果索引包含多個列,那么列的順序也十分重要,因為MySQL只能高效地使用索引的最左前綴列。

索引的優點:

  1. 索引大大減少了服務器需要掃描的數據量。
  2. 索引可以幫助服務器避免排序和臨時表。
  3. 索引可以將隨機I/O變為順序I/O。

如果表的數量特別多,可以建立一個元數據信息表,用來查詢需要用到的某些特性。例如執行那些需要聚合多個應用分布在多個表的數據的查詢,則需要記錄“哪個用戶的信息存儲在哪個表中”的元數據,這樣在查詢時就可以直接忽略那些不包含指定用戶信息的表。對于大型系統,這是一個常用的技巧。事實上,Infobright就是使用類似的實現。對于TB級別的數據,定位單條記錄的意義不大,所以經常會使用塊級別元數據技術來替代索引。

1.1 索引的類型

1.1.1 B-Tree索引

B樹(B-tree)是一種樹狀數據結構,它能夠存儲數據、對其進行排序并允許以O(log n)的時間復雜度運行進行查找、順序讀取、插入和刪除的數據結構。B樹,概括來說是一個節點可以擁有多于2個子節點的二叉查找樹。與自平衡二叉查找樹不同,B-樹為系統最優化大塊數據的讀和寫操作。B-tree算法減少定位記錄時所經歷的中間過程,從而加快存取速度。普遍運用在數據庫和文件系統。”

定義:

  1. 根節點至少有兩個子節點
  2. 每個節點有M-1個key,并且以升序排列
  3. 位于M-1和M key的子節點的值位于M-1 和M key對應的Value之間
  4. 其它節點至少有M/2個子節點

下面是一個M=3的B樹:


image.png

1.1.2 B+Tree索引

B+樹是對B樹的一種變形樹,它與B樹的差異在于:

  1. 有k個子結點的結點必然有k個關鍵碼。
  2. 非葉結點僅具有索引作用,跟記錄有關的信息均存放在葉結點中。
  3. 樹的所有葉結點構成一個有序鏈表,可以按照關鍵碼排序的次序遍歷全部記錄。
image.png

B+樹和B樹的區別
B+樹的非葉子結點只包含導航信息,不包含實際的值,所有的葉子結點和相連的節點使用鏈表相連,便于區間查找和遍歷。

  1. IO次數更少:由于B+樹在內部節點上不包含數據信息,因此在內存頁中能夠存放更多的key。 數據存放的更加緊密,具有更好的空間局部性。因此訪問葉子節點上關聯的數據也具有更好的緩存命中率。
  2. 遍歷更加方便:B+樹的葉子結點都是相鏈的,因此對整棵樹的遍歷只需要一次線性遍歷葉子結點即可。而且由于數據順序排列并且相連,所以便于區間查找和搜索。而B樹則需要進行每一層的遞歸遍歷。相鄰的元素可能在內存中不相鄰,所以緩存命中性沒有B+樹好。

MySQL的InnoDB存儲引擎采用的就是B+Tree索引。

1.2.3 B*Tree索引

B*樹是B+樹的變體,在B+樹的非根和非葉子結點再增加指向兄弟的指針;


image.png

Oracle采用的的是BTree索引*

1.1.4 哈希索引

哈希索引(hash index)基于哈希表實現,只有精確匹配索引所有列的查詢才有效(4)。對于每一行數據,存儲引擎都會對所有的索引列計算一個哈希碼(hash code),哈希碼是一個較小的值,并且不同鍵值的行計算出來的哈希碼也不一樣。哈希索引將所有的哈希碼存儲在索引中,同時在哈希表中保存指向每個數據行的指針。

在MySQL中,只有Memory引擎顯式支持哈希索引。這也是Memory引擎表的默認
索引類型,Memory引擎同時也支持B-Tree索引。

1.1.5 空間數據索引

空間索引使用場景不多,此處略過。

1.1.6 全文索引

全文索引是一種特殊類型的索引,它查找的是文本中的關鍵詞,而不是直接比較索引中的值。全文搜索和其他幾類索引的匹配方式完全不一樣。它有許多需要注意的細節,如停用詞、詞干和復數、布爾搜索等。全文索引更類似于搜索引擎做的事情,而不是簡單的WHERE條件匹配。

在相同的列上同時創建全文索引和基于值的B-Tree索引不會有沖突,全文索引適用于MATCH AGAINST操作,而不是普通的WHERE條件操作。

二.如何創建高性能的索引

正確地創建和使用索引是實現高性能查詢的基礎。

高效地選擇和使用索引有很多種方式,其中有些是針對特殊案例的優化方法,有些則是針對特定行為的優化。使用哪個索引,以及如何評估選擇不同索引的性能影響的技巧,則需要持續不斷地學習。接下來的幾個小節將幫助讀者理解如何高效地使用索引。

2.1 獨立的列

對列進行了運算是無法使用到索引的,例如:

mysql> SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;

mysql> SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_DAYS(date_col) <= 10;

需要修正為:

mysql> SELECT actor_id FROM sakila.actor WHERE actor_id  = 5 - 1;

mysql> SELECT ... WHERE TO_DAYS(date_col) >= TO_DAYS(CURRENT_DATE) - 10;

2.2 前綴索引和索引選擇性

有時候需要索引很長的字符列,這會讓索引變得大且慢。一個策略是前面提到過的模擬哈希索引。但有時候這樣做還不夠,還可以做些什么呢?

通常可以索引開始的部分字符,這樣可以大大節約索引空間,從而提高索引效率。但這樣也會降低索引的選擇性。索引的選擇性是指,不重復的索引值(也稱為基數,cardinality)和數據表的記錄總數(#T)的比值,范圍從1/#T到1之間。索引的選擇性越高則查詢效率越高,因為選擇性高的索引可以讓MySQL在查找時過濾掉更多的行。唯一索引的選擇性是1,這是最好的索引選擇性,性能也是最好的。

一般情況下某個列前綴的選擇性也是足夠高的,足以滿足查詢性能。對于BLOB、TEXT或者很長的VARCHAR類型的列,必須使用前綴索引,因為MySQL不允許索引這些列的完整長度。

在進行前綴索引的時候,要計算cardinality,來選擇合適的前綴長度,例如對比count(distinct str1(10))/count(*) count(distinct str1(20))/count(*) count(distinct str1(30))/count(*) 等的前綴索引的cardinality,選擇一個最合適的。

下面是一個給varchar、text、blob列創建前綴索引的例子:

mysql> create table t2 (id int,str1 varchar(4000),str2 text,str3 blob);
Query OK, 0 rows affected (0.02 sec)

mysql> 
mysql> insert into t2 select 1,repeat('abc',100),repeat('wxy',100),repeat('dxx',100);
Query OK, 1 row affected (0.00 sec)
Records: 1  Duplicates: 0  Warnings: 0

mysql> create index i_001 on t2(str1(10));
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> create index i_002 on t2(str2(20));
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> create index i_003 on t2(str3(30));
Query OK, 0 rows affected (0.01 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> explain select * from t2 where str1 = 'abcabc%';
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t2    | NULL       | ref  | i_001         | i_001 | 33      | const |    1 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> explain select * from t2 where str1 = 'abcabcabcabc%';
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t2    | NULL       | ref  | i_001         | i_001 | 33      | const |    1 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> 
mysql> explain select * from t2 where str2 = 'wxywxy%';
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t2    | NULL       | ref  | i_002         | i_002 | 63      | const |    1 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> explain select * from t2 where str2 = 'wxywxywxywxywxy%';
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t2    | NULL       | ref  | i_002         | i_002 | 63      | const |    1 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> 
mysql> 
mysql> explain select * from t2 where str3 = 'dxxdxx%';
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t2    | NULL       | ref  | i_003         | i_003 | 33      | const |    1 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> explain select * from t2 where str3 = 'dxxdxxdxxdxxdxx%';
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key   | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | t2    | NULL       | ref  | i_003         | i_003 | 33      | const |    1 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+-------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

總結

  1. 直接創建完成索引,但是會比較占空間
  2. 創建前綴索引,節省空間,但是會增加查詢掃描的時間,并且不能使用覆蓋索引
  3. 倒敘存儲,再創建前綴索引,用于繞過字符串本身前綴的區分度不高的情況
  4. 創建hash索引,有額外的計算開銷,只支持等值查詢

2.3 多列索引

多列索引也叫組合索引,組合索引可以這樣理解,比如(a,b,c),abc都是排好序的,在任意一段a的下面b都是排好序的,任何一段b下面c都是排好序的;

組合索引的生效原則是 從前往后依次使用生效,如果中間某個索引沒有使用,那么斷點前面的索引部分起作用,斷點后面的索引沒有起作用;

where a=3 and b=45 and c=5 .... 這種三個索引順序使用中間沒有斷點,全部發揮作用;
where a=3 and c=5... 這種情況下b就是斷點,a發揮了效果,c沒有效果
where b=3 and c=4... 這種情況下a就是斷點,在a后面的索引都沒有發揮作用,這種寫法聯合索引沒有發揮任何效果;
where b=45 and a=3 and c=5 .... 這個跟第一個一樣,全部發揮作用,abc只要用上了就行,跟寫的順序無關

2.4 選擇合適的索引列順序

在一個多列B-Tree索引中,索引列的順序意味著索引首先按照最左列進行排序,其次是第二列,等等。所以,索引可以按照升序或者降序進行掃描,以滿足精確符合列順序的ORDER BY、GROUP BY和DISTINCT等子句的查詢需求。

對于如何選擇索引的列順序有一個經驗法則:將選擇性最高的列放到索引最前列。這個建議有用嗎?在某些場景可能有幫助,但通常不如避免隨機IO和排序那么重要,考慮問題需要更全面(場景不同則選擇不同,沒有一個放之四海皆準的法則。這里只是說明,這個經驗法則可能沒有你想象的重要)。

當不需要考慮排序和分組時,將選擇性最高的列放在前面通常是很好的。這時候索引的作用只是用于優化WHERE條件的查找。在這種情況下,這樣設計的索引確實能夠最快地過濾出需要的行,對于在WHERE子句中只使用了索引部分前綴列的查詢來說選擇性也更高。然而,性能不只是依賴于所有索引列的選擇性(整體基數),也和查詢條件的具體值有關,也就是和值的分布有關。這和前面介紹的選擇前綴的長度需要考慮的地方一樣。可能需要根據那些運行頻率最高的查詢來調整索引列的順序,讓這種情況下索引的選擇性最高。

mysql> SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
> COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
> COUNT(*)
> FROM payment\G
*************************** 1. row ***************************
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16049

customer_id的選擇性更高,所以答案是將其作為索引列的第一列:

mysql> ALTER TABLE payment ADD KEY(customer_id, staff_id);

2.5 聚簇索引

聚簇索引并不是一種單獨的索引類型,而是一種數據存儲方式。。具體的細節依賴于其實現方式,但InnoDB的聚簇索引實際上在同一個結構中保存了B-Tree索引和數據行。當表有聚簇索引時,它的數據行實際上存放在索引的葉子頁(leaf page)中。術語“聚簇”表示數據行和相鄰的鍵值緊湊地存儲在一起。因為無法同時把數據行存放在兩個不同的地方,所以一個表只能有一個聚簇索引.

聚簇索引中的記錄是如何存放:


image.png

InnoDB將通過主鍵聚集數據,這也就是說上圖的“被索引的列”就是主鍵列。

如果沒有定義主鍵,InnoDB會選擇一個唯一的非空索引代替。如果沒有這樣的索引,InnoDB會隱式定義一個主鍵來作為聚簇索引。InnoDB只聚集在同一個頁面中的記錄。包含相鄰鍵值的頁面可能會相距甚遠。

聚簇主鍵可能對性能有幫助,但也可能導致嚴重的性能問題。所以需要仔細地考慮聚簇索引,尤其是將表的存儲引擎從InnoDB改成其他引擎的時候(反過來也一樣)。

聚集的數據有一些重要的優點:

  1. 可以把相關數據保存在一起。例如實現電子郵箱時,可以根據用戶ID來聚集數據,這樣只需要從磁盤讀取少數的數據頁就能獲取某個用戶的全部郵件。如果沒有使用聚簇索引,則每封郵件都可能導致一次磁盤I/O。
  2. 數據訪問更快。聚簇索引將索引和數據保存在同一個B-Tree中,因此從聚簇索引中獲取數據通常比在非聚簇索引中查找要快。
  3. 使用覆蓋索引掃描的查詢可以直接使用頁節點中的主鍵值。

如果在設計表和查詢時能充分利用上面的優點,那就能極大地提升性能。同時,聚簇索引也有一些缺點:

  1. 聚簇數據最大限度地提高了I/O密集型應用的性能,但如果數據全部都放在內存中,則訪問的順序就沒那么重要了,聚簇索引也就沒什么優勢了。
  2. 插入速度嚴重依賴于插入順序。按照主鍵的順序插入是加載數據到InnoDB表中速度最快的方式。但如果不是按照主鍵順序加載數據,那么在加載完成后最好使用OPTIMIZE TABLE命令重新組織一下表。
  3. 更新聚簇索引列的代價很高,因為會強制InnoDB將每個被更新的行移動到新的位置。
  4. 基于聚簇索引的表在插入新行,或者主鍵被更新導致需要移動行的時候,可能面臨“頁分裂(page split)”的問題。當行的主鍵值要求必須將這一行插入到某個已滿的頁中時,存儲引擎會將該頁分裂成兩個頁面來容納該行,這就是一次頁分裂操作。頁分裂會導致表占用更多的磁盤空間。
  5. 聚簇索引可能導致全表掃描變慢,尤其是行比較稀疏,或者由于頁分裂導致數據存儲不連續的時候。
  6. 二級索引(非聚簇索引)可能比想象的要更大,因為在二級索引的葉子節點包含了引用行的主鍵列。
  7. 二級索引訪問需要兩次索引查找,而不是一次。

最后一點可能讓人有些疑惑,為什么二級索引需要兩次索引查找?答案在于二級索引中保存的“行指針”的實質。要記住,二級索引葉子節點保存的不是指向行的物理位置的指針,而是行的主鍵值。

這意味著通過二級索引查找行,存儲引擎需要找到二級索引的葉子節點獲得對應的主鍵值,然后根據這個值去聚簇索引中查找到對應的行。這里做了重復的工作:兩次B-Tree查找而不是一次。對于InnoDB,自適應哈希索引能夠減少這樣的重復工作。

2.6 覆蓋索引

通常大家都會根據查詢的WHERE條件來創建合適的索引,不過這只是索引優化的一個方面。設計優秀的索引應該考慮到整個查詢,而不單單是WHERE條件部分。索引確實是一種查找數據的高效方式,但是MySQL也可以使用索引來直接獲取列的數據,這樣就不再需要讀取數據行。如果索引的葉子節點中已經包含要查詢的數據,那么還有什么必要再回表查詢呢?如果一個索引包含(或者說覆蓋)所有需要查詢的字段的值,我們就稱之為“覆蓋索引”。

覆蓋索引是非常有用的工具,能夠極大地提高性能。考慮一下如果查詢只需要掃描索引而無須回表,會帶來多少好處:

  1. 索引條目通常遠小于數據行大小,所以如果只需要讀取索引,那MySQL就會極大地減少數據訪問量。這對緩存的負載非常重要,因為這種情況下響應時間大部分花費在數據拷貝上。覆蓋索引對于I/O密集型的應用也有幫助,因為索引比數據更小,更容易全部放入內存中(這對于MyISAM尤其正確,因為MyISAM能壓縮索引以變得更小)。
  2. 因為索引是按照列值順序存儲的(至少在單個頁內是如此),所以對于I/O密集型的范圍查詢會比隨機從磁盤讀取每一行數據的I/O要少得多。對于某些存儲引擎,例如MyISAM和Percona XtraDB,甚至可以通過OPTIMIZE命令使得索引完全順序排列,這讓簡單的范圍查詢能使用完全順序的索引訪問。
  3. 一些存儲引擎如MyISAM在內存中只緩存索引,數據則依賴于操作系統來緩存,因此要訪問數據需要一次系統調用。這可能會導致嚴重的性能問題,尤其是那些系統調用占了數據訪問中的最大開銷的場景。
  4. 由于InnoDB的聚簇索引,覆蓋索引對InnoDB表特別有用。InnoDB的二級索引在葉子節點中保存了行的主鍵值,所以如果二級主鍵能夠覆蓋查詢,則可以避免對主鍵索引的二次查詢。

在所有這些場景中,在索引中滿足查詢的成本一般比查詢行要小得多。

MySQL只能使用BTree索引做覆蓋索引。

表sakila.inventory有一個多列索引(store_id,flm_id)。MySQL如果只需訪問這兩列,就可以使用這個索引做覆蓋索引,如下所示:

mysql> EXPLAIN SELECT store_id, film_id FROM sakila.inventory\G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: inventory
type: index
possible_keys: NULL
key: idx_store_id_film_id
key_len: 3
ref: NULL
rows: 4673
Extra: Using index

2.7 使用索引掃描來做排序

MySQL有兩種方式可以生成有序的結果:通過排序操作;或者按索引順序掃描;如果EXPLAIN出來的type列的值為“index”,則說明MySQL使用了索引掃描來做排序(不要和Extra列的“Using index”搞混淆了)。

掃描索引本身是很快的,因為只需要從一條索引記錄移動到緊接著的下一條記錄。但如果索引不能覆蓋查詢所需的全部列,那就不得不每掃描一條索引記錄就都回表查詢一次對應的行。這基本上都是隨機I/O,因此按索引順序讀取數據的速度通常要比順序地全表掃描慢,尤其是在I/O密集型的工作負載時。

MySQL可以使用同一個索引既滿足排序,又用于查找行。因此,如果可能,設計索引時應該盡可能地同時滿足這兩種任務,這樣是最好的。

只有當索引的列順序和ORDER BY子句的順序完全一致,并且所有列的排序方向(倒序或正序)都一樣時,MySQL才能夠使用索引來對結果做排序。如果查詢需要關聯多張表,則只有當ORDER BY子句引用的字段全部為第一個表時,才能使用索引做排序。ORDER BY子句和查找型查詢的限制是一樣的:需要滿足索引的最左前綴的要求;否則,MySQL都需要執行排序操作,而無法利用索引排序。

2.8 壓縮(前綴壓縮)索引

MyISAM使用前綴壓縮來減少索引的大小,從而讓更多的索引可以放入內存中,這在某些情況下能極大地提高性能。默認只壓縮字符串,但通過參數設置也可以對整數做壓縮。MyISAM壓縮每個索引塊的方法是,先完全保存索引塊中的第一個值,然后將其他值和第一個值進行比較得到相同前綴的字節數和剩余的不同后綴部分,把這部分存儲起來即可。例如,索引塊中的第一個是“perform”,第二個值是“performance”,那么第二個值的前綴壓縮后存儲的是類似“7,ance”這樣的形式。MyISAM對行指針也采用類似的前綴壓縮方式。

壓縮塊使用更少的空間,代價是某些操作可能更慢。因為每個值的壓縮前綴都依賴前面的值,所以MyISAM查找時無法在索引塊使用二分查找而只能從頭開始掃描。正序的掃描速度還不錯,但是如果是倒序掃描——例如ORDER BY DESC——就不是很好了。所有在塊中查找某一行的操作平均都需要掃描半個索引塊。

測試表明,對于CPU密集型應用,因為掃描需要隨機查找,壓縮索引使得MyISAM在索引查找上要慢好幾倍。壓縮索引的倒序掃描就更慢了。壓縮索引需要在CPU內存資源與磁盤之間做權衡。壓縮索引可能只需要十分之一大小的磁盤空間,如果是I/O密集型應用,對某些查詢帶來的好處會比成本多很多。

可以在CREATE TABLE語句中指定PACK_KEYS參數來控制索引壓縮的方式。

2.9 冗余和重復索引

MySQL允許在相同列上創建多個索引,無論是有意的還是無意的。MySQL需要單獨維護重復的索引,并且優化器在優化查詢的時候也需要逐個地進行考慮,這會影響性能。

重復索引是指在相同的列上按照相同的順序創建的相同類型的索引。應該避免這樣創建重復索引,發現以后也應該立即移除。

CREATE TABLE test (
ID INT NOT NULL PRIMARY KEY,
A INT NOT NULL,
B INT NOT NULL,
UNIQUE(ID),
INDEX(ID)
) ENGINE=InnoDB;

一個經驗不足的用戶可能是想創建一個主鍵,先加上唯一限制,然后再加上索引以供查詢使用。事實上,MySQL的唯一限制和主鍵限制都是通過索引實現的,因此,上面的寫法實際上在相同的列上創建了三個重復的索引。通常并沒有理由這樣做,除非是在同一列上創建不同類型的索引來滿足不同的查詢需求。

冗余索引和重復索引有一些不同。如果創建了索引(A,B),再創建索引(A)就是冗余索引,因為這只是前一個索引的前綴索引。因此索引(A,B)也可以當作索引(A)來使用(這種冗余只是對B-Tree索引來說的)。但是如果再創建索引(B,A),則不是冗余索引,索引(B)也不是,因為B不是索引(A,B)的最左前綴列。另外,其他不同類型的索引(例如哈希索引或者全文索引)也不會是B-Tree索引的冗余索引,而無論覆蓋的索引列是什么。

冗余索引通常發生在為表添加新索引的時候。例如,有人可能會增加一個新的索引(A,B)而不是擴展已有的索引(A)。還有一種情況是將一個索引擴展為(A,ID),其中ID是主鍵,對于InnoDB來說主鍵列已經包含在二級索引中了,所以這也是冗余的。

大多數情況下都不需要冗余索引,應該盡量擴展已有的索引而不是創建新索引。但也有時候出于性能方面的考慮需要冗余索引,因為擴展已有的索引會導致其變得太大,從而影響其他使用該索引的查詢的性能。

2.10 未使用的索引

除了冗余索引和重復索引,可能還會有一些服務器永遠不用的索引。這樣的索引完全是累贅,建議考慮刪除。有兩個工具可以幫助定位未使用的索引。最簡單有效的辦法是在Percona Server或者MariaDB中先打開userstates服務器變量(默認是關閉的),然后讓服務器正常運行一段時間,再通過查詢INFORMATION_SCHEMA.INDEX_STATISTICS就能查到每個索引的使用頻率。

另外,還可以使用Percona Toolkit中的pt-index-usage,該工具可以讀取查詢日志,并對日志中的每條查詢進行EXPLAIN操作,然后打印出關于索引和查詢的報告。這個工具不僅可以找出哪些索引是未使用的,還可以了解查詢的執行計劃——例如在某些情況有些類似的查詢的執行方式不一樣,這可以幫助你定位到那些偶爾服務質量差的查詢,優化它們以得到一致的性能表現。該工具也可以將結果寫入到MySQL的表中,方便查詢結果。

2.11 索引和鎖

索引可以讓查詢鎖定更少的行。如果你的查詢從不訪問那些不需要的行,那么就會鎖定更少的行,從兩個方面來看這對性能都有好處。首先,雖然InnoDB的行鎖效率很高,內存使用也很少,但是鎖定行的時候仍然會帶來額外開銷;其次,鎖定超過需要的行會增加鎖爭用并減少并發性。

InnoDB只有在訪問行的時候才會對其加鎖,而索引能夠減少InnoDB訪問的行數,從而減少鎖的數量。但這只有當InnoDB在存儲引擎層能夠過濾掉所有不需要的行時才有效。如果索引無法過濾掉無效的行,那么在InnoDB檢索到數據并返回給服務器層以后,MySQL服務器才能應用WHERE子句。這時已經無法避免鎖定行了:InnoDB已經鎖住了這些行,到適當的時候才釋放。在MySQL 5.1和更新的版本中,InnoDB可以在服務器端過濾掉行后就釋放鎖,但是在早期的MySQL版本中,InnoDB只有在事務提交后才能釋放鎖。

關于InnoDB、索引和鎖有一些很少有人知道的細節:InnoDB在二級索引上使用共享(讀)鎖,但訪問主鍵索引需要排他(寫)鎖。這消除了使用覆蓋索引的可能性,并且使得SELECT FOR UPDATE比LOCK IN SHARE MODE或非鎖定查詢要慢很多。

2.12 索引下推

索引下推(index condition pushdown )簡稱ICP,在Mysql5.6的版本上推出,用于優化查詢。

在不使用ICP的情況下,在使用非主鍵索引(又叫普通索引或者二級索引)進行查詢時,存儲引擎通過索引檢索到數據,然后返回給MySQL服務器,服務器然后判斷數據是否符合條件 。

在使用ICP的情況下,如果存在某些被索引的列的判斷條件時,MySQL服務器將這一部分判斷條件傳遞給存儲引擎,然后由存儲引擎通過判斷索引是否符合MySQL服務器傳遞的條件,只有當索引符合條件時才會將數據檢索出來返回給MySQL服務器 。

索引條件下推優化可以減少存儲引擎查詢基礎表的次數,也可以減少MySQL服務器從存儲引擎接收數據的次數。

測試開始:
在開始之前先先準備一張用戶表(user),其中主要幾個字段有:id、name、age、address。建立聯合索引(name,age)。
假設有一個需求,要求匹配姓名第一個為陳的所有用戶,sql語句如下:

SELECT * from user where  name like '陳%'

根據 "最佳左前綴" 的原則,這里使用了聯合索引(name,age)進行了查詢,性能要比全表掃描肯定要高。

問題來了,如果有其他的條件呢?假設又有一個需求,要求匹配姓名第一個字為陳,年齡為20歲的用戶,此時的sql語句如下:

SELECT * from user where  name like '陳%' and age=20

這條sql語句應該如何執行呢?下面對Mysql5.6之前版本和之后版本進行分析。

Mysql5.6之前的版本:

  1. 5.6之前的版本是沒有索引下推這個優化的,因此執行的過程如下圖:


    image.png
  2. 會忽略age這個字段,直接通過name進行查詢,在(name,age)這課樹上查找到了兩個結果,id分別為2,1,然后拿著取到的id值一次次的回表查詢,因此這個過程需要回表兩次。

Mysql5.6及之后版本:

  1. 5.6版本添加了索引下推這個優化,執行的過程如下圖:


    image.png

InnoDB并沒有忽略age這個字段,而是在索引內部就判斷了age是否等于20,對于不等于20的記錄直接跳過,因此在(name,age)這棵索引樹中只匹配到了一個記錄,此時拿著這個id去主鍵索引樹中回表查詢全部數據,這個過程只需要回表一次。

實踐

  1. 當然上述的分析只是原理上的,我們可以實戰分析一下,因此陳某裝了Mysql5.6版本的Mysql,解析了上述的語句,如下圖:


    image.png
  2. 根據explain解析結果可以看出Extra的值為Using index condition,表示已經使用了索引下推。

2.13 不可見索引

MySQL支持不可見索引;也就是說,優化器不使用的索引。該特性適用于主鍵以外的索引(顯式或隱式)。

大表的索引的維護是非常耗時的,如果我們想判斷一個索引對查詢的影響,而又不想刪除索引,因為重建耗時太久,此時,我們可以將索引設置為不可見,這樣優化器就用不到索引了。

CREATE TABLE t1 (
  i INT,
  j INT,
  k INT,
  INDEX i_idx (i) INVISIBLE
) ENGINE = InnoDB;
CREATE INDEX j_idx ON t1 (j) INVISIBLE;
ALTER TABLE t1 ADD INDEX k_idx (k) INVISIBLE;

要改變現有索引的可見性,請在alter TABLE…ALTER INDEX操作:

ALTER TABLE t1 ALTER INDEX i_idx INVISIBLE;
ALTER TABLE t1 ALTER INDEX i_idx VISIBLE;

關于索引是可見還是不可見的信息可以從INFORMATION_SCHEMA中獲得。統計表或SHOW索引輸出。例如:

mysql> SELECT INDEX_NAME, IS_VISIBLE
       FROM INFORMATION_SCHEMA.STATISTICS
       WHERE TABLE_SCHEMA = 'db1' AND TABLE_NAME = 't1';
+------------+------------+
| INDEX_NAME | IS_VISIBLE |
+------------+------------+
| i_idx      | YES        |
| j_idx      | NO         |
| k_idx      | NO         |
+------------+------------+

不可見的索引使測試刪除索引對查詢性能的影響成為可能,而不需要進行破壞性的更改(如果需要索引就必須撤消)。對于大型表來說,刪除和重新添加索引的開銷可能很大,而讓它不可見和可見是快速的原位操作。

如果優化器確實需要或使用不可見的索引,有幾種方法可以注意到它的缺失對查詢表的影響:

  1. 對于包含引用不可見索引的索引提示的查詢,將發生錯誤。
  2. 性能模式數據顯示受影響查詢的工作負載增加。
  3. 查詢有不同的EXPLAIN執行計劃。
  4. 查詢出現在之前沒有出現的慢查詢日志中。
  5. 索引在默認情況下是可見的。要顯式地控制新索引的可見性,可以在CREATE TABLE、CREATE index或ALTER TABLE的索引定義中使用VISIBLE或INVISIBLE關鍵字:

optimizer_switch系統變量的use_invisible_indexes標志控制優化器是否使用不可見的索引來構建查詢執行計劃。如果該標志是關閉的(默認值),優化器將忽略不可見的索引(與引入該標志之前的行為相同)。如果標志是打開的,不可見的索引仍然是不可見的,但是優化器會在構建執行計劃時考慮它們。

使用SET_VAR優化器提示臨時更新optimizer_switch的值,你可以只在單個查詢期間啟用不可見索引,如下所示:

mysql> EXPLAIN SELECT /*+ SET_VAR(optimizer_switch = 'use_invisible_indexes=on') */
     >     i, j FROM t1 WHERE j >= 50\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: range
possible_keys: j_idx
          key: j_idx
      key_len: 5
          ref: NULL
         rows: 2
     filtered: 100.00
        Extra: Using index condition

mysql> EXPLAIN SELECT i, j FROM t1 WHERE j >= 50\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 5
     filtered: 33.33
        Extra: Using where

索引可見性不影響索引維護。例如,每次對表行進行更改,索引就會繼續更新,而惟一索引會防止向列中插入重復項,無論索引是可見的還是不可見的。

如果一個沒有顯式主鍵的表在NOT NULL列上有任何UNIQUE索引,那么它仍然可能有一個有效的隱式主鍵。在這種情況下,第一個這樣的索引將相同的約束作為顯式主鍵放在表行上,并且不能使索引不可見。考慮下表定義:

CREATE TABLE t2 (
  i INT NOT NULL,
  j INT NOT NULL,
  UNIQUE j_idx (j)
) ENGINE = InnoDB;

該定義不包含顯式的主鍵,但是NOT NULL列j上的索引將主鍵設置為相同的約束,并且不能使其不可見:

mysql> ALTER TABLE t2 ALTER INDEX j_idx INVISIBLE;
ERROR 3522 (HY000): A primary key index cannot be invisible.

現在假設一個顯式的主鍵被添加到表中:

ALTER TABLE t2 ADD PRIMARY KEY (i);

顯式主鍵不能變為不可見。此外,j上的唯一索引不再充當隱含的主鍵,因此可以使其不可見:

mysql> ALTER TABLE t2 ALTER INDEX j_idx INVISIBLE;
Query OK, 0 rows affected (0.03 sec)

2.14 降序索引

MySQL支持降序索引:索引定義中的DESC不再被忽略,而是導致按降序存儲鍵值。以前,可以以相反的順序掃描索引,但會造成性能損失。降序索引可以按正向順序掃描,這樣效率更高。降序索引還可以讓優化器使用多列索引,當最有效的掃描順序混合了一些列的升序和其他列的降序。

考慮下面的表定義,它包含兩個列和四個兩列索引定義,用于列上的升序和降序索引的各種組合:

CREATE TABLE t (
  c1 INT, c2 INT,
  INDEX idx1 (c1 ASC, c2 ASC),
  INDEX idx2 (c1 ASC, c2 DESC),
  INDEX idx3 (c1 DESC, c2 ASC),
  INDEX idx4 (c1 DESC, c2 DESC)
);

表定義產生四個不同的索引。優化器可以對每個ORDER BY子句執行正向索引掃描,不需要使用filesort操作:

ORDER BY c1 ASC, c2 ASC    -- optimizer can use idx1
ORDER BY c1 DESC, c2 DESC  -- optimizer can use idx4
ORDER BY c1 ASC, c2 DESC   -- optimizer can use idx2
ORDER BY c1 DESC, c2 ASC   -- optimizer can use idx3

降序索引的使用要符合以下條件:

  1. 降序索引只支持InnoDB存儲引擎,有以下限制:
    1.1 如果次要索引包含降序索引鍵列,或者主鍵包含降序索引列,則不支持更改緩沖。
    1.2 InnoDB SQL解析器不使用降序索引。對于InnoDB全文搜索,這意味著索引表的FTS_DOC_ID列上的索引不能定義為降序索引。要了解更多信息,請參見15.6.2.4節“InnoDB全文索引”。
  2. 所有可用升序索引的數據類型都支持降序索引。
  3. 普通(非生成的)列和生成的列(VIRTUAL和STORED)都支持降序索引。
  4. DISTINCT可以使用任何包含匹配列的索引,包括降序鍵部分。
  5. 具有降序關鍵部分的索引不用于調用聚合函數但沒有GROUP BY子句的查詢的MIN()/MAX()優化。
  6. BTREE支持降序索引,但不支持HASH索引。FULLTEXT或SPATIAL索引不支持降序索引。
  7. 為HASH、FULLTEXT和SPATIAL索引顯式指定ASC和DESC指示符會導致錯誤。

你可以在EXPLAIN輸出的Extra列中看到優化器可以使用降序索引,如下所示:

mysql> CREATE TABLE t1 (
    -> a INT, 
    -> b INT, 
    -> INDEX a_desc_b_asc (a DESC, b ASC)
    -> );

mysql> EXPLAIN SELECT * FROM t1 ORDER BY a ASC\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: index
possible_keys: NULL
          key: a_desc_b_asc
      key_len: 10
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Backward index scan; Using index

在EXPLAIN FORMAT=TREE輸出中,降序索引的使用是通過在索引名后面加(反向)來表示的,如下所示:

mysql> EXPLAIN FORMAT=TREE SELECT * FROM t1 ORDER BY a ASC\G 
*************************** 1. row ***************************
EXPLAIN: -> Index scan on t1 using a_desc_b_asc (reverse)  (cost=0.35 rows=1)

三.減少索引和數據的碎片

B-Tree索引可能會碎片化,這會降低查詢的效率。碎片化的索引可能會以很差或者無序的方式存儲在磁盤上。

根據設計,B-Tree需要隨機磁盤訪問才能定位到葉子頁,所以隨機訪問是不可避免的。然而,如果葉子頁在物理分布上是順序且緊密的,那么查詢的性能就會更好。否則,對于范圍查詢、索引覆蓋掃描等操作來說,速度可能會降低很多倍;對于索引覆蓋掃描這一點更加明顯。

表的數據存儲也可能碎片化。然而,數據存儲的碎片化比索引更加復雜。有三種類型的數據碎片。

  1. 行碎片(Row fragmentation)
    這種碎片指的是數據行被存儲為多個地方的多個片段中。即使查詢只從索引中訪問一行記錄,行碎片也會導致性能下降。

  2. 行間碎片(Intra-row fragmentation)
    行間碎片是指邏輯上順序的頁,或者行在磁盤上不是順序存儲的。行間碎片對諸如全表掃描和聚簇索引掃描之類的操作有很大的影響,因為這些操作原本能夠從磁盤上順序存儲的數據中獲益。

  3. 剩余空間碎片(Free space fragmentation)
    剩余空間碎片是指數據頁中有大量的空余空間。這會導致服務器讀取大量不需要的數據,從而造成浪費。

InnoDB不會出現短小的行碎片,InnoDB會移動短小的行并重寫到一個片段中。

那么如何整理表的碎片呢?
一般有optimize table tabname 和 alter table tabname engine=InnoDB 這兩個命令。

對于InnoDB存儲引擎,optimize命令會報如下信息:

mysql> optimize table fact_sale;
+----------------+----------+----------+-------------------------------------------------------------------+
| Table          | Op       | Msg_type | Msg_text                                                          |
+----------------+----------+----------+-------------------------------------------------------------------+
| test.fact_sale | optimize | note     | Table does not support optimize, doing recreate + analyze instead |
| test.fact_sale | optimize | status   | OK                                                                |
+----------------+----------+----------+-------------------------------------------------------------------+
2 rows in set (17 min 24.96 sec)

所以一般使用 alter table tabname engine=InnoDB這個命令

mysql> select count(*) from fact_sale_skew;
+----------+
| count(*) |
+----------+
| 99999000 |
+----------+
1 row in set (29.66 sec)

mysql> show table status from test like 'fact_sale_skew'\G
*************************** 1. row ***************************
           Name: fact_sale_skew
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 94816780
 Avg_row_length: 44
    Data_length: 4262461440
Max_data_length: 0
   Index_length: 0
      Data_free: 7340032
 Auto_increment: 99999001
    Create_time: 2021-01-22 17:34:23
    Update_time: NULL
     Check_time: NULL
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options: 
        Comment: 
1 row in set (0.00 sec)

mysql> 
mysql> alter table fact_sale_skew engine=innodb;
Query OK, 0 rows affected (2 min 42.15 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> show table status from test like 'fact_sale_skew'\G
*************************** 1. row ***************************
           Name: fact_sale_skew
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 99726030
 Avg_row_length: 51
    Data_length: 5123325952
Max_data_length: 0
   Index_length: 0
      Data_free: 3145728
 Auto_increment: 99999001
    Create_time: 2021-06-01 15:26:14
    Update_time: NULL
     Check_time: NULL
      Collation: utf8_general_ci
       Checksum: NULL
 Create_options: 
        Comment: 
1 row in set (0.04 sec)

mysql> 

Data_free顯示的是表占的存儲空間,可以看到存儲空間由 7340032 縮減到 3145728,大大的減少了。

四.索引案例學習

4.1 支持多種過濾條件

有一個表,我們創建了一個組合索引 (sex,country),sex只有兩個值 ‘f’和‘m’,country雖然唯一值不多,但是經常出現在where語句后面,所以這個地方創建了這樣的一個 (sex,country)。

select * from tabname where country = 'China';

如上的sql,因為不滿足組合索引的左前原則,所以是用不到組合索引的。

select * from tabname where sex in ('f','m') where country = 'China';

將第一個sql改為第二個sql就可以使用到索引了,雖然 sex in ('f','m') 沒有過濾任何數據行,但是卻可以優化這個sql,讓sql用到索引。

4.2 優化排序

有這么一個sql,既有order by也有limit,如果表數據量大,且沒辦法使用到索引的情況下,這個查詢會比較慢。

SELECT<cols> FROM profiles WHERE sex='M' ORDER BY rating LIMIT 10;

那么如何使這個查詢用上索引呢? sex這一列區分度不高,即便使用了也對這個查詢幫助不大。

此時可以使用到我們的組合索引(sex,rating),索引本身是有序的,所以可以快速定位到10行數據。

參考:

  1. 《高性能MySQL》
  2. https://www.cs.usfca.edu/~galles/visualization/Algorithms.html
  3. http://www.lxweimin.com/p/7ce804f97967
  4. http://www.lxweimin.com/p/db226e0196b4
  5. https://www.cnblogs.com/Chenjiabing/p/12600926.html
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容