學習筆記,MySQL中的索引
Mysql 索引
索引是提高 MySQL 查詢效能的一個重要途徑,但過多的索引可能會導致過高的磁碟使用率以及過高的記憶體佔用,從而影響應用程式的整體效能。應當儘量避免事後才想起新增索引,因為事後可能需要監控大量的 SQL 才能定位到問題所在,而且新增索引的時間肯定是遠大於初始新增索引所需要的時間,可見索引的新增也是非常有技術含量的。
接下來將向你展示一系列建立高效能索引的策略,以及每條策略其背後的工作原理。但在此之前,先了解與索引相關的一些演算法和資料結構,將有助於更好的理解後文的內容。
1. 索引簡介
索引優化應該是查詢效能優化的最有效手段。
1.1. 索引的優缺點
B+ 樹索引,按照順序儲存資料,所以 Mysql 可以用來做 ORDER BY 和 GROUP BY 操作。因為資料是有序的,所以 B+ 樹也就會將相關的列值都儲存在一起。最後,因為索引中儲存了實際的列值,所以某些查詢只使用索引就能夠完成全部查詢。
✔ 索引的優點:
- 索引大大減少了伺服器需要掃描的資料量,從而加快檢索速度。
- 支援行級鎖的資料庫,如 InnoDB 會在訪問行的時候加鎖。使用索引可以減少訪問的行數,從而減少鎖的競爭,提高併發。
- 索引可以幫助伺服器避免排序和臨時表。
- 索引可以將隨機 I/O 變為順序 I/O。
- 唯一索引可以確保每一行資料的唯一性,通過使用索引,可以在查詢的過程中使用優化隱藏器,提高系統的效能。
❌ 索引的缺點:
- 建立和維護索引要耗費時間,這會隨著資料量的增加而增加。
- 索引需要佔用額外的物理空間,除了資料表佔資料空間之外,每一個索引還要佔一定的物理空間,如果要建立組合索引那麼需要的空間就會更大。
- 寫操作(
INSERT
UPDATE
/DELETE
)時很可能需要更新索引,導致資料庫的寫操作效能降低。
1.2. 何時使用索引
索引能夠輕易將查詢效能提升幾個數量級。
✔ 什麼情況適用索引:
- 表經常進行
SELECT
操作; - 表的資料量比較大;
- 列名經常出現在
WHERE
或連線(JOIN
)條件中
❌ 什麼情況不適用索引:
- 頻繁寫操作(
INSERT
/UPDATE
/DELETE
)- 需要更新索引空間; - 非常小的表 - 對於非常小的表,大部分情況下簡單的全表掃描更高效。
- 列名不經常出現在
WHERE
或連線(JOIN
)條件中 - 索引就會經常不命中,沒有意義,還增加空間開銷。 - 對於特大型表,建立和使用索引的代價將隨之增長。可以考慮使用分割槽技術或 Nosql。
2. 索引的資料結構
2.1. 雜湊索引
Hash 索引只有精確匹配索引所有列的查詢才有效。
雜湊表是一種以鍵 - 值(key-value)儲存資料的結構,我們只要輸入待查詢的值即 key,就可以找到其對應的值即 Value。雜湊的思路很簡單,把值放在數組裡,用一個雜湊函式把 key 換算成一個確定的位置,然後把 value 放在陣列的這個位置。
對於每一行資料,對所有的索引列計算一個 hashcode
。雜湊索引將所有的 hashcode
儲存在索引中,同時在 Hash 表中儲存指向每個資料行的指標。
雜湊索引的優點:
- 因為索引資料結構緊湊,所以查詢速度非常快。
雜湊索引的缺點:
- 雜湊索引資料不是按照索引值順序儲存的,所以無法用於排序。
- 雜湊索引不支援部分索引匹配查詢。如,在資料列 (A,B) 上建立雜湊索引,如果查詢只有資料列 A,無法使用該索引。
- 雜湊索引只支援等值比較查詢,不支援任何範圍查詢,如
WHERE price > 100
。 - 雜湊索引有可能出現雜湊衝突,出現雜湊衝突時,必須遍歷連結串列中所有的行指標,逐行比較,直到找到符合條件的行。
2.2. B 樹索引
通常我們所說的索引是指B-Tree
索引,它是目前關係型資料庫中查詢資料最為常用和有效的索引,大多數儲存引擎都支援這種索引。使用B-Tree
這個術語,是因為 MySQL 在CREATE TABLE
或其它語句中使用了這個關鍵字,但實際上不同的儲存引擎可能使用不同的資料結構,比如 InnoDB 就是使用的B+Tree
。
B+Tree
中的 B 是指balance
,意為平衡。需要注意的是,B+樹索引並不能找到一個給定鍵值的具體行,它找到的只是被查詢資料行所在的頁,接著資料庫會把頁讀入到記憶體,再在記憶體中進行查詢,最後得到要查詢的資料。
二叉搜尋樹
二叉搜尋樹的特點是:每個節點的左兒子小於父節點,父節點又小於右兒子。其查詢時間複雜度是 $$O(log(N))$$。
當然為了維持 $$O(log(N))$$ 的查詢複雜度,你就需要保持這棵樹是平衡二叉樹。為了做這個保證,更新的時間複雜度也是 $$O(log(N))$$。
隨著資料庫中資料的增加,索引本身大小隨之增加,不可能全部儲存在記憶體中,因此索引往往以索引檔案的形式儲存的磁碟上。這樣的話,索引查詢過程中就要產生磁碟 I/O 消耗,相對於記憶體存取,I/O 存取的消耗要高几個數量級。可以想象一下一棵幾百萬節點的二叉樹的深度是多少?如果將這麼大深度的一顆二叉樹放磁碟上,每讀取一個節點,需要一次磁碟的 I/O 讀取,整個查詢的耗時顯然是不能夠接受的。那麼如何減少查詢過程中的 I/O 存取次數?
一種行之有效的解決方法是減少樹的深度,將二叉樹變為 N 叉樹(多路搜尋樹),而 B+ 樹就是一種多路搜尋樹。
B+ 樹
B+ 樹索引適用於全鍵值查詢、鍵值範圍查詢和鍵字首查詢,其中鍵字首查詢只適用於最左字首查詢。
理解B+Tree
時,只需要理解其最重要的兩個特徵即可:
- 第一,所有的關鍵字(可以理解為資料)都儲存在葉子節點,非葉子節點並不儲存真正的資料,所有記錄節點都是按鍵值大小順序存放在同一層葉子節點上。
- 其次,所有的葉子節點由指標連線。如下圖為簡化了的
B+Tree
。
根據葉子節點的內容,索引型別分為主鍵索引和非主鍵索引。
- 聚簇索引(clustered):又稱為主鍵索引,其葉子節點存的是整行資料。因為無法同時把資料行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。InnoDB 的聚簇索引實際是在同一個結構中儲存了 B 樹的索引和資料行。
- 非主鍵索引的葉子節點內容是主鍵的值。在 InnoDB 裡,非主鍵索引也被稱為二級索引(secondary)。資料儲存在一個位置,索引儲存在另一個位置,索引中包含指向資料儲存位置的指標。可以有多個,小於 249 個。
聚簇表示資料行和相鄰的鍵值緊湊地儲存在一起,因為資料緊湊,所以訪問快。因為無法同時把資料行存放在兩個不同的地方,所以一個表只能有一個聚簇索引。
聚簇索引和非聚簇索引的查詢有什麼區別
- 如果語句是
select * from T where ID=500
,即聚簇索引查詢方式,則只需要搜尋 ID 這棵 B+ 樹; - 如果語句是
select * from T where k=5
,即非聚簇索引查詢方式,則需要先搜尋 k 索引樹,得到 ID 的值為 500,再到 ID 索引樹搜尋一次。這個過程稱為回表。
也就是說,基於非聚簇索引的查詢需要多掃描一棵索引樹。因此,我們在應用中應該儘量使用主鍵查詢。
顯然,主鍵長度越小,非聚簇索引的葉子節點就越小,非聚簇索引佔用的空間也就越小。
自增主鍵是指自增列上定義的主鍵,在建表語句中一般是這麼定義的: NOT NULL PRIMARY KEY AUTO_INCREMENT。從效能和儲存空間方面考量,自增主鍵往往是更合理的選擇。有沒有什麼場景適合用業務欄位直接做主鍵的呢?還是有的。比如,有些業務的場景需求是這樣的:
- 只有一個索引;
- 該索引必須是唯一索引。
由於沒有其他索引,所以也就不用考慮其他索引的葉子節點大小的問題。
這時候我們就要優先考慮上一段提到的“儘量使用主鍵查詢”原則,直接將這個索引設定為主鍵,可以避免每次查詢需要搜尋兩棵樹。
2.3. 全文索引
MyISAM 儲存引擎支援全文索引,用於查詢文字中的關鍵詞,而不是直接比較是否相等。查詢條件使用 MATCH AGAINST,而不是普通的 WHERE。
全文索引一般使用倒排索引實現,它記錄著關鍵詞到其所在文件的對映。
InnoDB 儲存引擎在 MySQL 5.6.4 版本中也開始支援全文索引。
2.4. 空間資料索引
MyISAM 儲存引擎支援空間資料索引(R-Tree),可以用於地理資料儲存。空間資料索引會從所有維度來索引資料,可以有效地使用任意維度來進行組合查詢。
必須使用 GIS 相關的函式來維護資料。
3. 索引的型別
主流的關係型資料庫一般都支援以下索引型別:
3.1. 主鍵索引(PRIMARY
)
主鍵索引:一種特殊的唯一索引,不允許有空值。一個表只能有一個主鍵(在 InnoDB 中本質上即聚簇索引),一般是在建表的時候同時建立主鍵索引。
CREATE TABLE `table` (
`id` int(11) NOT NULL AUTO_INCREMENT,
...
PRIMARY KEY (`id`)
)
3.2. 唯一索引(UNIQUE
)
唯一索引:索引列的值必須唯一,但允許有空值。如果是組合索引,則列值的組合必須唯一。
CREATE TABLE `table` (
...
UNIQUE indexName (title(length))
)
3.3. 普通索引(INDEX
)
普通索引:最基本的索引,沒有任何限制。
CREATE TABLE `table` (
...
INDEX index_name (title(length))
)
3.4. 全文索引(FULLTEXT
)
全文索引:主要用來查詢文字中的關鍵字,而不是直接與索引中的值相比較。
全文索引跟其它索引大不相同,它更像是一個搜尋引擎,而不是簡單的 WHERE 語句的引數匹配。全文索引配合 match against
操作使用,而不是一般的 WHERE 語句加 LIKE。它可以在 CREATE TABLE
,ALTER TABLE
,CREATE INDEX
使用,不過目前只有 char
、varchar
,text
列上可以建立全文索引。值得一提的是,在資料量較大時候,現將資料放入一個沒有全域性索引的表中,然後再用 CREATE INDEX
建立全文索引,要比先為一張表建立全文索引然後再將資料寫入的速度快很多。
CREATE TABLE `table` (
`content` text CHARACTER NULL,
...
FULLTEXT (content)
)
3.5. 聯合索引
組合索引:多個欄位上建立的索引,只有在查詢條件中使用了建立索引時的第一個欄位,索引才會被使用。使用組合索引時遵循最左字首集合。
CREATE TABLE `table` (
...
INDEX index_name (title(length), title(length), ...)
)
4. 索引的策略
假設有以下表:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
4.1. 索引基本原則
- 索引不是越多越好,不要為所有列都建立索引。要考慮到索引的維護代價、空間佔用和查詢時回表的代價。索引一定是按需建立的,並且要儘可能確保足夠輕量。一旦建立了多欄位的聯合索引,我們要考慮儘可能利用索引本身完成資料查詢,減少回表的成本。
- 要儘量避免冗餘和重複索引。
- 要考慮刪除未使用的索引。
- 儘量的擴充套件索引,不要新建索引。
- 頻繁作為
WHERE
過濾條件的列應該考慮新增索引。
4.2. 獨立的列
“獨立的列” 是指索引列不能是表示式的一部分,也不能是函式的引數。
對索引欄位做函式操作,可能會破壞索引值的有序性,因此優化器就決定放棄走樹搜尋功能。
如果查詢中的列不是獨立的列,則資料庫不會使用索引。
❌ 錯誤示例:
SELECT actor_id FROM actor WHERE actor_id + 1 = 5;
SELECT ... WHERE TO_DAYS(current_date) - TO_DAYS(date_col) <= 10;
4.3. 覆蓋索引
覆蓋索引是指,索引上的資訊足夠滿足查詢請求,不需要回表查詢資料。
【示例】範圍查詢
create table T (
ID int primary key,
k int NOT NULL DEFAULT 0,
s varchar(16) NOT NULL DEFAULT '',
index k(k))
engine=InnoDB;
insert into T values(100,1, 'aa'),(200,2,'bb'),(300,3,'cc'),(500,5,'ee'),(600,6,'ff'),(700,7,'gg');
select * from T where k between 3 and 5
需要執行幾次樹的搜尋操作,會掃描多少行?
- 在 k 索引樹上找到 k=3 的記錄,取得 ID = 300;
- 再到 ID 索引樹查到 ID=300 對應的 R3;
- 在 k 索引樹取下一個值 k=5,取得 ID=500;
- 再回到 ID 索引樹查到 ID=500 對應的 R4;
- 在 k 索引樹取下一個值 k=6,不滿足條件,迴圈結束。
在這個過程中,回到主鍵索引樹搜尋的過程,我們稱為回表。可以看到,這個查詢過程讀了 k 索引樹的 3 條記錄(步驟 1、3 和 5),回表了兩次(步驟 2 和 4)。
如果執行的語句是 select ID from T where k between 3 and 5,這時只需要查 ID 的值,而 ID 的值已經在 k 索引樹上了,因此可以直接提供查詢結果,不需要回表。索引包含所有需要查詢的欄位的值,稱為覆蓋索引。
由於覆蓋索引可以減少樹的搜尋次數,顯著提升查詢效能,所以使用覆蓋索引是一個常用的效能優化手段。
4.4. 使用索引來排序
Mysql 有兩種方式可以生成排序結果:通過排序操作;或者按索引順序掃描。
索引最好既滿足排序,又用於查詢行。這樣,就可以通過命中覆蓋索引直接將結果查出來,也就不再需要排序了。
這樣整個查詢語句的執行流程就變成了:
- 從索引 (city,name,age) 找到第一個滿足 city='杭州’條件的記錄,取出其中的 city、name 和 age 這三個欄位的值,作為結果集的一部分直接返回;
- 從索引 (city,name,age) 取下一個記錄,同樣取出這三個欄位的值,作為結果集的一部分直接返回;
- 重複執行步驟 2,直到查到第 1000 條記錄,或者是不滿足 city='杭州’條件時迴圈結束。
4.5. 字首索引
有時候需要索引很長的字元列,這會讓索引變得大且慢。
這時,可以使用字首索引,即只索引開始的部分字元,這樣可以大大節約索引空間,從而提高索引效率。但這樣也會降低索引的選擇性。對於 BLOB
/TEXT
/VARCHAR
這種文字型別的列,必須使用字首索引,因為資料庫往往不允許索引這些列的完整長度。
索引的選擇性是指:不重複的索引值和資料表記錄總數的比值。最大值為 1,此時每個記錄都有唯一的索引與其對應。選擇性越高,查詢效率也越高。如果存在多條命中字首索引的情況,就需要依次掃描,直到最終找到正確記錄。
使用字首索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本。
那麼,如何確定字首索引合適的長度呢?
可以使用下面這個語句,算出這個列上有多少個不同的值:
select count(distinct email) as L from SUser;
然後,依次選取不同長度的字首來看這個值,比如我們要看一下 4~7 個位元組的字首索引,可以用這個語句:
select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;
當然,使用字首索引很可能會損失區分度,所以你需要預先設定一個可以接受的損失比例,比如 5%。然後,在返回的 L4~L7 中,找出不小於 L * 95% 的值,假設這裡 L6、L7 都滿足,你就可以選擇字首長度為 6。
此外,order by
無法使用字首索引,無法把字首索引用作覆蓋索引。
4.6. 最左字首匹配原則
不只是索引的全部定義,只要滿足最左字首,就可以利用索引來加速檢索。這個最左字首可以是聯合索引的最左 N 個欄位,也可以是字串索引的最左 M 個字元。
MySQL 會一直向右匹配直到遇到範圍查詢 (>,<,BETWEEN,LIKE)
就停止匹配。
- 索引可以簡單如一個列(a),也可以複雜如多個列(a, b, c, d),即聯合索引。
- 如果是聯合索引,那麼 key 也由多個列組成,同時,索引只能用於查詢 key 是否存在(相等),遇到範圍查詢(>、<、between、like 左匹配)等就不能進一步匹配了,後續退化為線性查詢。
- 因此,列的排列順序決定了可命中索引的列數。
不要為每個列都建立獨立索引。
將選擇性高的列或基數大的列優先排在多列索引最前列。但有時,也需要考慮 WHERE
子句中的排序、分組和範圍條件等因素,這些因素也會對查詢效能造成較大影響。
例如:a = 1 and b = 2 and c > 3 and d = 4
,如果建立(a,b,c,d)順序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索引則都可以用到,a,b,d 的順序可以任意調整。
讓選擇性最強的索引列放在前面,索引的選擇性是指:不重複的索引值和記錄總數的比值。最大值為 1,此時每個記錄都有唯一的索引與其對應。選擇性越高,查詢效率也越高。
例如下面顯示的結果中 customer_id 的選擇性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
COUNT(*)
FROM payment;
staff_id_selectivity: 0.0001
customer_id_selectivity: 0.0373
COUNT(*): 16049
4.7. = 和 in 可以亂序
不需要考慮 =
、IN
等的順序,Mysql 會自動優化這些條件的順序,以匹配儘可能多的索引列。
【示例】如有索引 (a, b, c, d),查詢條件 c > 3 and b = 2 and a = 1 and d < 4
與 a = 1 and c > 3 and b = 2 and d < 4
等順序都是可以的,MySQL 會自動優化為 a = 1 and b = 2 and c > 3 and d < 4,依次命中 a、b、c、d。
5. 索引最佳實踐
建立了索引,並非一定有效。比如不滿足字首索引、最左字首匹配原則、查詢條件涉及函式計算等情況都無法使用索引。此外,即使 SQL 本身符合索引的使用條件,MySQL 也會通過評估各種查詢方式的代價,來決定是否走索引,以及走哪個索引。
因此,在嘗試通過索引進行 SQL 效能優化的時候,務必通過執行計劃(EXPLAIN
)或實際的效果來確認索引是否能有效改善效能問題,否則增加了索引不但沒解決效能問題,還增加了資料庫增刪改的負擔。如果對 EXPLAIN 給出的執行計劃有疑問的話,你還可以利用 optimizer_trace
檢視詳細的執行計劃做進一步分析。