MySQL效能優化(三):深入理解索引的這點事
索引,對於良好的資料庫效能非常關鍵。只要提及到資料庫效能優化,都會首先想到“索引”,看看錶中是否新增索引。尤其是當表中的資料量越來越大時,索引對效能的影響尤為突出。在資料量較小且負載較低時,沒有索引或者不恰當索引對效能的影響可能還不明顯,但當資料量逐漸增大時,效能則會急劇下降。
不過,索引卻經常被忽略,有時候甚至被誤解、誤用,在實際使用中經常會遇到糟糕索引而導致的效能問題。本文就索引的概念、型別、優點等方面聊聊,一起深入理解索引的這點事,更有助於你清楚的理解索引,能夠正確的使用它,便於利用它來進行資料庫的優化。
一、什麼是索引
索引(Index
),是幫助MySQL高效獲取資料的資料結構,是儲存引擎用於快速找到記錄的一種資料結構。
要理解MySQL中索引是如何工作的,最簡單的例子就是去看看一本書的目錄“索引”部分。如果想在一本書中找到某個章節,一般我們會先看書的目錄“索引”,就會立即找到對應的頁碼。
在MySQL中,儲存引擎也是用類似的方法使用索引,首先在索引中找到對應的值,然後根據匹配的索引記錄找到對應的資料行。
查詢是資料庫中最常用的操作,我們都希望查詢的速度儘可能快,因此資料庫系統的設計者會從查詢演算法角度去進行優化。最基本的查詢演算法當然就是順序查詢,但是這種演算法的複雜度為O(n),在資料量很大時就會顯得非常糟糕。例如,在一張使用者表t_user中有如下資料,想要查詢到年齡為89歲的人,如果按照順序查詢,則得逐行掃描,可見查詢效率有多低下(數量量越大,資料分佈越不均勻,則花費的時間就更長)。
mysql> select * from t_user; +----+----------+-----+ | id | name | age | +----+----------+-----+ | 1 | xcbeyond | 22 | | 2 | jack | 34 | | 3 | tom | 77 | | 4 | kitty | 5 | | 5 | make | 91 | | 6 | Mickey | 23 | | 7 | Andy | 89 | +----+----------+-----+ 7 rows in set
好在,資料庫系統的設計者早都意識到了這一點,參考了更優秀的查詢演算法,如二分查詢、二叉樹查詢等等,但是分析之後發現,每種查詢演算法都只能應用於特定資料結構之上,如二分查詢要求被查詢的資料有序,而二叉樹查詢只能應用於二叉查詢樹。鑑於此,在資料之外,資料庫系統還維護著滿足特定查詢演算法的資料結構,這些資料結構以某種方式引用(指向)資料,這樣就可以在這些資料結構上實現高階查詢演算法,即:這就是資料庫中的索引。
為了更好的理解索引,下圖就以表t_user
中的資料,展示了一種可能的索引方式。
左邊是表中的資料,一共7條記錄,為加快age
列的查詢,維護了一個右邊所示的二叉查詢樹,每個節點包含索引鍵值及一個指向對應資料記錄的指標,這樣運用二叉查詢就能很快的查詢對應的資料了,時間複雜度為O(log2 N)
。
然而,在實際資料庫中,幾乎沒有使用這樣的二叉查詢樹來實現(因為二叉查詢樹對資料是有要求的),但其原理和這類似。
二、索引操作
在正式介紹索引之前,先一起來看看MySQL是如何建立索引、重建索引、查詢索引、刪除索引等操作的,以備後續使用。(建議單獨儲存收藏)
1. 建立索引
索引的建立可以在CREATE TABLE
語句中進行,也可以單獨用CREATE INDEX
或ALTER TABLE
來給表增加索引。
語法:
CREATE [UNIQUE/FULLTEXT] INDEX <索引名> ON <表名>(<列名>)
ALTER TABLE <表名> ADD INDEX|UNIQUE|PRIMARY KEY|FULLTEXT <索引名>(<列名>)
其中,建立索引時,可以指定索引型別:主鍵索引(PRIMARY KEY
)、 唯一索引(UNIQUE
)、 全文索引(FULLTEXT
)、 普通索引(INDEX
)。
例如:
1)以表index_test
為例說明,先建立一個普通的表index_test
:
(建立表時,也可以直接建立索引,此處為了說明索引的建立,則單獨建立索引)
mysql> create table index_test(id int,ch varchar(32)); Query OK, 0 rows affected
2)為表index_test
單獨建立索引:
mysql> create index idx on index_test(id); Query OK, 0 rows affected Records: 0 Duplicates: 0 Warnings: 0
或者
mysql> alter table index_test add index idx(id); Query OK, 0 rows affected Records: 0 Duplicates: 0 Warnings: 0
2.重建索引
重建索引,在常規的資料庫維護操作中經常使用。在資料庫運行了較長時間後,索引都有損壞的可能,這時就需要重建。對資料重建索引可以起到提高檢索效率。
重建索引,實質上的對錶的修復。
例如:
mysql> repair table index_test quick; +-----------------+--------+----------+---------------------------------------------------------+ | Table | Op | Msg_type | Msg_text | +-----------------+--------+----------+---------------------------------------------------------+ | test.index_test | repair | note | The storage engine for the table doesn't support repair | +-----------------+--------+----------+---------------------------------------------------------+ 1 row in set
3. 查詢索引
有時,為了檢視某張表是否有索引,索引情況如何,就需要通過命令show index from|in table_name
來檢視索引。
語法:
SHOW INDEX FROM|IN <表名>
例如:
mysql> show index from index_test; +------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | +------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ | index_test | 1 | idx | 1 | id | A | 0 | NULL | NULL | YES | BTREE | | | +------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+ 1 row in set
細心的你,或許看到查詢結果中的欄位
index_type
的值BTREE
,這也就是接下來會講到的B Tree
索引,從另一方面也可以知道InnoDB
預設的索引型別為B Tree
。
4. 刪除索引
刪除索引可以使用DROP INDEX
或 ALTER TABLE
語句來實現。
語法:
DROP INDEX <索引名> ON <表名>
ALTER TABLE <表名> DROP INDEX <索引名>
例如:
mysql> drop index idx on index_test; Query OK, 0 rows affected Records: 0 Duplicates: 0 Warnings: 0
或者
mysql> alter table index_test drop index idx; Query OK, 0 rows affected Records: 0 Duplicates: 0 Warnings: 0
三、索引型別
索引有很多種型別,可以為不同的場景提供更好的效能。在MySQL中,索引是在儲存引擎層實現的,所以,並沒有統一的索引標準:不同儲存引擎的索引的工作方式也是不一樣的,也不是所有的儲存引擎都支援所有型別的索引。
從儲存結構上來劃分:
BTree索引(B-Tree或B+Tree索引)
雜湊索引
全文索引(full-index)
從應用層次來分:
普通索引:即一個索引只包含單個列,一個表可以有多個單列索引。
唯一索引:索引列的值必須唯一,但允許有空值。
複合索引:即一個索引包含多個列。
下面我們從索引的儲存結構上,來看看MySQL支援的索引型別,底層是如何實現的,以及它們的優缺點。
MySQL預設儲存引擎是
Innodb
,只顯式支援B-Tree
索引,對於頻繁訪問的表,Innodb
會透明建立自適應雜湊索引,即在B樹索引基礎上建立雜湊索引,可以顯著提高查詢效率,對於客戶端是透明的,不可控制的,隱式的。
1. B-Tree索引
當大家在談論索引的時候,如果沒有特別指明型別,多半說的是B-Tree索引,它使用B-Tree資料結構來儲存資料,可以讓系統高效的找到資料所在的磁碟塊。
B代表平衡(
balance
),而不是二叉(binary
),因為B Tree是從最早的平衡二叉樹演化而來的。
B-Tree是為磁碟等外儲存裝置設計的一種平衡查詢樹。因此在講B-Tree之前先了解下磁碟的相關知識。
系統從磁碟讀取資料到記憶體時是以磁碟塊(block
)為基本單位的,位於同一個磁碟塊中的資料會被一次性讀取出來,而不是需要什麼取什麼。
InnoDB
儲存引擎中有頁(Page
)的概念,頁是其磁碟管理的最小單位。InnoDB
儲存引擎中預設每個頁的大小為16KB
,可通過引數innodb_page_size
將頁的大小設定為4K、8K、16K
,在MySQL中可通過如下命令檢視頁的大小:
mysql> show variables like 'innodb_page_size'; +------------------+-------+ | Variable_name | Value | +------------------+-------+ | innodb_page_size | 16384 | +------------------+-------+ 1 row in set
而系統一個磁碟塊的儲存空間往往沒有這麼大,因此InnoDB
每次申請磁碟空間時都會是若干地址連續磁碟塊來達到頁的大小16KB
。InnoDB
在把磁碟資料讀入到磁碟時會以頁為基本單位,在查詢資料時如果一個頁中的每條資料都能有助於定位資料記錄的位置,這將會減少磁碟I/O次數,提高查詢效率。
B-Tree定義資料記錄為一個二元組[key、data]:
key
為記錄的主鍵,即表中的主鍵值,用於記錄唯一的資料行,key值是唯一且互不相同的。data
為一行記錄中除主鍵外的資料。
一棵m
階的B-Tree有如下特性:
每個節點最多有m個孩子。
除了根節點和葉子節點外,其它每個節點至少有
ceil(m/2)
個孩子。若根節點不是葉子節點,則至少有2個孩子。
所有葉子節點都在同一層,且不包含其它關鍵字資訊。
每個非終端節點包含n個關鍵字資訊(
p0,p1,...pn,k1,...kn
)關鍵字key的個數n滿足:
ceil(m/2)-1 <= n <= m-1
ki(i=1,…n)
為關鍵字,且關鍵字升序排序。pi(i=1,…n)
為指向子節點的指標。p(i-1)
指向的子樹的所有節點關鍵字均小於ki
,但都大於k(i-1)
。
注:ceil()
為取整函式。
B-Tree中的每個節點根據實際情況,可以包含大量的鍵值key
、資料data
、和指標p
。如下圖所示為一個3階的B-Tree索引結構:
每個節點佔用一個磁碟塊空間,一個節點上有兩個升序排序的關鍵字key
和三個指向子節點的指標p
,指標儲存的是子節點所在磁碟塊的地址。兩個關鍵詞key
劃分成為三個範圍域對應的三個指標p
,並指向的子節點的資料的範圍域。以根節點為例,關鍵字為17
和35
,p1
指標指向的子節點的資料範圍為小於17
,p2
指標指向的子節點的資料範圍為17~35
,p3
指標指向的子節點的資料範圍為大於35
。
模擬查詢關鍵字為29
資料行的過程:
根據根節點找到磁碟塊1,讀入記憶體。【磁碟I/O操作第1次】
比較關鍵字
29
在區間(17,35)
,找到磁碟塊1的指標p2
。根據
p2
指標找到磁碟塊3,讀入記憶體。【磁碟I/O操作第2次】比較關鍵字
29
在區間(26,30)
,找到磁碟塊3的指標p2
。根據`p2’指標找到磁碟塊8,讀入記憶體。【磁碟I/O操作第3次】
在磁碟塊8中的關鍵字列表中找到關鍵字
29
。
分析上面過程,發現需要3
次磁碟I/O
操作,和3
次記憶體查詢操作。由於記憶體中的關鍵字key
是一個有序表結構,可以利用二分法查詢提高效率。而3
次磁碟I/O
操作是影響整個B-Tree查詢效率的決定因素。B-Tree
相對於AVLTree
(高度平衡的二叉樹)縮減了節點個數,使每次磁碟I/O
取到記憶體的資料都發揮了作用,從而提高了查詢效率。
2. B+Tree索引
B+Tree
是在B-Tree
基礎上的一種優化,使其更適合實現儲存索引結構,InnoDB
儲存引擎就是用B+Tree
實現其索引結構。
從上一節中的B-Tree
結構圖中可以看到每個節點中不僅包含資料的key
值,還有data
值。而每一個頁的儲存空間是有限的,如果data
資料較大時將會導致每個節點(即一個頁)能儲存的key
的數量很小,當儲存的資料量很大時同樣會導致B-Tree
的深度較大,增大查詢時的磁碟I/O
次數,進而影響查詢效率。在B+Tree
中,所有資料記錄節點都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只儲存key
值資訊,這樣可以大大加大每個節點儲存的key
值數量,降低B+Tree
的高度。
B+Tree
相對於B-Tree
有幾點不同:
非葉子節點只儲存鍵值資訊。
所有葉子節點之間都有一個鏈指標。
資料記錄都存放在葉子節點中。
將上一小節中的B-Tree
進行優化,由於B+Tree
的非葉子節點只儲存鍵值資訊,假設每個磁碟塊能儲存4個鍵值及指標資訊,則變成B+Tree
後其結構如下圖所示:
通常在B+Tree
上有兩個頭指標,一個指向根節點,另一個指向關鍵字最小的葉子節點,而且所有葉子節點(即資料節點)之間是一種鏈式環結構。因此可以對B+Tree
進行兩種查詢運算:一種是對於主鍵的範圍查詢和分頁查詢,另一種是從根節點開始,進行隨機查詢。
可能上面例子中只有22條資料記錄,看不出B+Tree
的優點,下面做一個推算:
InnoDB
儲存引擎中頁的大小為16KB
,一般表的主鍵型別為INT
(佔用4個位元組)或BIGINT
(佔用8個位元組),指標型別也一般為4或8個位元組,也就是說一個頁(B+Tree
中的一個節點)中大概儲存16KB/(8B+8B)=1K
個鍵值(因為是估值,為方便計算,這裡的K取值為10^3
)。也就是說一個深度為3的B+Tree
索引可以維護10^3 * 10^3 * 10^3 = 10億
條記錄。
實際情況中每個節點可能不能填充滿,因此在資料庫中,B+Tree的高度一般都在2~4層。MySQL的InnoDB
儲存引擎在設計時是將根節點常駐記憶體的,也就是說查詢某一鍵值的行記錄時最多隻需要1~3
次磁碟I/O操作。
3. 雜湊索引
雜湊索引(hash index
),是基於雜湊表實現的。對於每一行資料,儲存引擎都會對所有的索引列計算一個雜湊值(hash value
),不同鍵值的行計算出來的雜湊值也不一樣。雜湊索引將所有的雜湊值儲存在索引中,同時在雜湊表中儲存指向每個資料行的指標。
在MySQL中,只有Memory
引擎顯示支援雜湊索引,同時雜湊索引也是Memory
儲存引擎的預設索引型別,並且Memory
儲存引擎也是支援B-Tree
索引。
如果多個列的雜湊值相同,索引會以連結串列的方式存放多個記錄指標到同一個雜湊值。
繼續以表t_user
中的資料舉例說明,並對欄位name
設定雜湊索引。假設索引使用的雜湊函式是f()
,則計算出來的雜湊值(都是舉例資料,並非真實資料)為:
f('xcbeyond')=2390 f('jack')=4010 f('tom')=5178 f('kitty')=1067 f('make')=7901 f('Mickey')=3079 f('Andy')=8301
計算出來的雜湊值,會指向對應資料行的資料,指向關係如下圖:
執行如下查詢,並能夠查詢到對應的資料。
mysql> select * from t_user where name = 'xcbeyond'; +----+----------+-----+ | id | name | age | +----+----------+-----+ | 1 | xcbeyond | 22 | +----+----------+-----+ 1 row in set
先計算出xcbeyond
的雜湊值,根據該雜湊值尋找到對應指向的資料行。f('xcbeyond')=2390
,所以MySQL在索引中查詢2390
,並找到指向第1行的資料行,然後比較第1行的值是否等於xcbeyond
,以確保查詢到資料的準確性。
因為索引自身只需儲存對應的雜湊值,所有索引的結構十分緊湊,這也讓雜湊索引查詢的速度非常快。然而,雜湊索引也有它的限制,即:索引失效。
雜湊索引資料並不是按照索引值順序儲存的,所以無法用於排序。
雜湊索引不支援部分索引列匹配查詢,因為雜湊索引始終是使用索引列的全部內容來計算雜湊值的。例如,在資料列(A,B)上都建立雜湊索引,如果查詢時只有資料列A,則無法使用該索引。
雜湊索引只支援等比較查詢,包括
=
、in()
,不支援任何範圍、模糊查詢,例如,where age > 20
、where name like '%xc%'
。如果雜湊衝突很多的話,儲存引擎必須進行連結串列來維護,維護這些連結串列的操作代價會很大,則查詢的效能會很低。
4. 全文索引
全文索引是一種特殊型別的索引,它查詢的是文字中的關鍵字,而不是比較索引中的值。
全文索引和其他型別索引的匹配方式完全不一樣,它有許多需要注意的細節。更類似於搜尋引擎做的事情,而不是簡單的where
條件匹配。
在相同的列上同時建立全文索引和基於值的B-Tree索引是不會有衝突的,全文索引適用於全文模糊搜尋(MATCH AGAINST)操作,而不是普通的where條件操作。
四、索引優點
索引可以讓MySQL伺服器快速地定位到表的指定位置,但這並不是索引的唯一作用,到目前為止可以看到,根據建立索引的資料結構不同,索引也有一些其他的附加作用。
最常見的B-Tree索引,按照順序儲存資料,所以MySQL可以用來做ORDER BY和GROUP BY操作。因為資料是有序的,所以B-Tree也就會將相關的列值都儲存在一起。最後,因為索引中儲存了實際的列值,所以某些查詢只使用索引就能夠完成全部查詢。據此特性,總結下來索引有如下優點:
索引大大減少了MySQL伺服器需要掃描的資料量。(全表掃描)
索引可以幫助MySQL伺服器避免排序和臨時表。
索引可以將隨機I/O變為順序I/O。
索引是最好的解決方案嗎?
索引並不總是最好的方案。總的來說,只有當索引幫助儲存引擎快速查詢到記錄帶來的好處大於其帶來的額外工作時,索引才是有效的。對於非常小的表,大部分情況下簡單的全表掃描更高效。對於中到大型的表,索引就非常有效。但對於特大型的表,建立和使用索引的代價將隨之增長,這種情況下,則需要一種技術可以直接區分出查詢需要的一組資料,而不是一條記錄一條記錄的匹配,如可以採用表分割槽的方式。
如果表的數量特別多,可以建立一個元資料資訊表,用來查詢需要用到的某些特性。例如執行那些需要聚合多個應用分佈在多個表的資料的查詢,則需要記錄“哪個使用者的資訊存放在哪個表中”的元資料,這樣在查詢時就可以直接忽略那些不包含指定使用者資訊的表。對於大型系統,這是一個常用的技巧。
參考文章:
https://www.jianshu.com/p/d67c637776d6
https://cloud.tencent.com/developer/article/1125452