本文直切主題,針對InnoDB引擎描述索引及優化策略。在開始之前,需要讀者瞭解:1)二叉查找樹(包括2-3查找樹、紅黑樹等數據結構)2)MySQL的InnoDB引擎基礎知識
索引初探
要了解索引,當然要了解其數據結構。樹有很多應用,流行的用法之一是包括UNIX和DOS在內的許多常用操作系統中的目錄結構,二叉查找樹又是Java中兩種集合類TreeSet和TreeMap實現的基礎。那麼對於數據庫,I/O是其性能瓶頸所在,減少樹的深度是直接有效的,BTree和B+Tree應運而生。
BTree和B+Tree(Balance-Tree,多路搜索樹,非二叉)
BTree
BTree是一種查找樹,如同二叉查找樹,紅黑樹等,都是爲提高查找效率而產生的,BTree也是如此,可以把它看做二叉查找樹的優化升級。二叉查找樹的特點是每個非葉節點都最多隻有兩個子節點,但是當數據量非常大時,二叉查找樹的深度過深,搜索算法自根節點向下搜索時,需要訪問的節點也就變的相當多。如果這些節點存儲在外存儲器(磁盤)中,每訪問一個節點,相當於就是進行了一次I/O操作,隨着樹高度的增加,頻繁的I/O操作一定會降低查詢的效率。BTree改二叉爲多叉,每個節點存儲更多的指針信息,以此達到減少樹的深度、降低I/O操作數。
使用BTree結構可以顯著減少定位記錄時所經歷的中間過程,從而加快存取速度。
定義(對於一個m階BTree)
特性
B+Tree
InnoDB 存儲引擎在絕大多數情況下使用B+Tree建立索引,B+Tree也是關係型數據庫中最爲常用和有效的索引結構,但是B+Tree索引並不能找到一個給定鍵對應的具體值,它只能找到數據行對應的頁,然後正如上一節所提到的,數據庫把整個頁讀入到內存中,並在內存中查找具體的數據行。
定義(其定義基本與 BTree同,除了:)
-
所有葉節點之間都有一個鏈指針;
-
所有關鍵字都在葉子結點出現;
-
非葉子節點只存儲鍵值信息,數據記錄都存放在葉節點中。
特性
- 單節點可以存儲更多的元素,使得查詢磁盤IO次數更少,更加高效的單元素查找;
- 所有查詢都要查找到葉子節點,查詢性能穩定;
- 葉子節點會包含所有的關鍵字,以及指向數據記錄的指針,並且葉子節點本身是根據關鍵字的大小從小到大順序鏈接,範圍查找性能更優。
區別
B+Tree是BTree的一種變形樹,它與BTree的差異在於:
-
B+Tree只有達到葉子結點才命中(BTree可以在非葉子結點命中),其性能也等價於在關鍵字全集做一次二分查找;
-
BTree樹每個葉子節點都有雙向指針;
-
BTree分支節點和葉節點均保存記錄的關鍵碼和記錄的指針;B+Tree分支節點只保存記錄關鍵碼的複製,無記錄指針。所有記錄都集中在葉節點一層,並且葉節點可以構成一維線性表,便於連續訪問和範圍查詢。
聚集索引和輔助索引
數據庫中的 B+Tree索引可以分爲聚集索引(clustered index)和輔助索引(secondary index),它們之間的最大區別就是,聚集索引中存放着一條行記錄的全部信息,而輔助索引中只包含索引列和一個用於查找對應行記錄的“書籤”。即在數據庫的聚集索引中,葉子節點直接包含衛星數據。在輔助索引(NonClustered Index)中,葉節點帶有指向衛星數據的指針。
聚集索引
InnoDB使用了聚集索引存儲數據。
與非聚集索引的區別則是,聚集索引既存儲了索引,也存儲了行值。當一個表有一個聚集索引,它的數據是存儲在索引的葉子頁(leaf pages)上的。因此可以說InnoDB是基於索引的表。
當我們使用聚集索引對錶中的數據進行檢索時,可以直接獲得聚集索引所對應的整條行記錄數據所在的頁,不需要進行第二次操作。
索引的建立規則
- 如果一個主鍵被定義了,那麼這個主鍵就是作爲聚集索引
- 如果沒有主鍵被定義,那麼該表的第一個唯一非空索引被作爲聚集索引
- 如果沒有主鍵也沒有合適的唯一索引,那麼InnoDB內部會生成一個隱藏的主鍵作爲聚集索引,這個隱藏的主鍵是一個6個字節的列,改列的值會隨着數據的插入自增
輔助索引
輔助索引,也叫做非聚集索引,葉節點不包含行的全部數據。除了包含關鍵字外,還包含了一個標記,這個標記用來告訴InnoDB引擎從哪裏可以找到與索引相對應的行數據。由於InnoDB引擎是索引組織表,因此,這個標記就是相應的行數據的聚集索引關鍵字。
輔助索引的存在並不影響數據在聚集索引中的組織,因此一個表可以有多個輔助索引。
使用輔助索引查找一條表記錄的過程:通過輔助索引查找到對應的關鍵字,最後在聚集索引中使用關鍵字獲取對應的行記錄,這也是通常情況下行記錄的查找方式。
使用建議
聚集索引的優先選擇列
-
含有大量非重複值的列
-
使用 between,>或<返回一個範圍值的列
-
需要經常排序的列,列順序和最常用的排序一致
-
返回大量結果集的查詢
-
經常被 join 的列
不建議的聚集索引列
-
修改頻繁的列
-
低選擇性的列,如性別
-
新增內容太過離散隨機的列
規範與建議
- 命名規則:表名_字段名
- 需要加索引的字段,要在where條件中
- 如果where條件中是OR關係,加索引不起作用
- 能用小類型別用大類型字段
- 索引 key_len 長度過大,也會影響 SQL 性能。所以儘量不默認 null,會佔用字節、索引長度。
-
常用的字段放在前面;選擇性高的字段放在前面
-
對較長的字符數據類型的字段建索引,優先考慮前綴索引,如 index(url(64))
-
只創建需要的索引,避免冗餘索引,如:index(a,b),index(a)
-
使用聯合索引,以避免回表,達到覆蓋索引
- 聯合索引遵循最左原則
- 索引不可濫用,索引會佔用存儲空間並且增加數據更新操作的複雜度,降低CUD(create/update/delate)效率
回表
先了解一個概念,MySQL對 WHERE 中條件的處理,根據索引使用情況分成三種:index key, index filter, table filter
1. index key
用於確定SQL查詢在索引中的連續範圍(起始範圍+結束範圍)的查詢條件,被稱之爲Index Key。由於一個範圍,至少包含一個起始與一個終止,因此Index Key也被拆分爲Index First Key和Index Last Key,分別用於定位索引查找的起始,以及索引查詢的終止條件。
2. index filter
在使用 index key 確定了起始範圍和介紹範圍之後,在此範圍之內,還有一些記錄不符合 WHERE 條件,如果這些條件可以使用索引進行過濾,那麼就是 index filter。
3. table filter
WHERE 中的條件不能使用索引進行處理的,只能訪問table,進行條件過濾了。
從普通索引查出主鍵索引,然後查詢出數據的過程叫做回表。回表一次就會執行一次查詢,所以避免回表是減少數據庫壓力、提高效率的有效手段。在InnoDB中,使用聯合索引配合主鍵索引可以直接返回結果而不需要回表查詢。
聯合索引(複合索引)與前綴索引(最左原則)
Mysql從左到右的使用索引中的字段,一個查詢可以只使用索引中的一部份,但只能是最左側部分。例如索引是(a,b,c),可以支持 a | a,b | a,b,c 3種組合進行查找,但不支持 b,c 進行查找。這是最左原則的第一層意思:聯合索引的多個字段中,只有當查詢條件爲聯合索引的第一個字段時,索引纔會有效。
條件 WHERE a LIKE 'perfix%'; 索引也會有效。這是最左原則的第二層意思:根據字段值最左若干個字符進行的模糊查詢,索引有效。
覆蓋索引
覆蓋索引是對聯合索引的合理利用。
比如 SELECT a, b FROM table WHERE a = 'wangnima'; ,如果我們已經創建了(a)或(a,b)的聯合索引,那麼這條語句會直接從索引返回而不會發生回表。即創建索引的字段覆蓋了查詢字段。
如果執行 SELECT c FROM table WHERE a = 'wangnima'; ,就會發生回表,因爲我們的輔助索引樹中,沒有字段 c 的數據,需要拿到主鍵索引的關鍵字,去主鍵索引中回表查詢。
但是需要注意的是,索引雖好不可濫用。
索引下推(Index Condition Pushdown (ICP))
結合在 回表 概念中引出的三種索引使用情況(index key, index filter, table filter),ICP 技術,就是 index filter 技術。MySQL的架構分爲服務器層和引擎層。
官方解釋(https://dev.mysql.com/doc/refman/5.6/en/index-condition-pushdown-optimization.html)
索引條件下推(ICP)是對MySQL使用索引從表中檢索行的情況的優化。如果沒有ICP,存儲引擎將遍歷索引以定位基表中的行,並將它們返回到MySQL服務器,該服務器將計算基錶行的where條件。在啓用ICP的情況下,如果部分where條件可以通過只使用索引中的列來計算,MySQL服務器會把where條件的這部分 推入 存儲引擎。然後,存儲引擎通過使用索引條目來評估所推送的索引條件,並且只有在滿足該條件時才從表中讀取行。ICP可以減少存儲引擎必須訪問基本表的次數和MySQL服務器必須訪問存儲引擎的次數。
根據官方的指導,我們來做個驗證:
EXPLAIN
SELECT * FROM people
WHERE zipcode='95054'
AND lastname LIKE '%lao%'
AND address LIKE '%Main Street%';
官方解釋:
EXPLAIN
使用“索引條件下推”時,輸出顯示 Using index condition
在 Extra
列中。
假設一個表包含有關人員及其地址的信息,並且該表的索引定義爲 INDEX (zipcode, lastname, firstname)
。如果我們知道一個人的zipcode
價值但不確定姓氏,我們可以這樣搜索:
SELECT * FROM people
WHERE zipcode='95054'
AND lastname LIKE '%etrunia%'
AND address LIKE '%Main Street%';
MySQL可以使用索引來掃描人 zipcode='95054'
。第二部分(lastname LIKE '%etrunia%'
)不能用於限制必須掃描的行數,因此如果沒有Index Condition Pushdown,此查詢必須爲所有擁有的人檢索完整的錶行 zipcode='95054'
。
使用索引條件下推,MySQL lastname LIKE '%etrunia%'
在讀取整個錶行之前檢查該 部分。這樣可以避免讀取與索引元組相對應的完整行,這些行匹配 zipcode
條件而不是 lastname
條件。
默認情況下啓用索引條件下推。可以optimizer_switch
通過設置index_condition_pushdown
標誌來控制 系統變量 :
SET optimizer_switch = 'index_condition_pushdown=off';
SET optimizer_switch = 'index_condition_pushdown=on';
實踐
*注意語句中的“[ ··· ]”中括號指代變量,書寫時記得去掉
普通索引
這是最基本的索引,它沒有任何限制。它有以下幾種創建方式:
1. 創建索引
CREATE INDEX indexName ON mytable(username(length));
如果不是字符類型的字段,如int,則不要指定length;如果是CHAR,VARCHAR類型,length可以不指定,也可以小於字段實際長度;如果是BLOB和TEXT類型,必須指定 length。
2. 修改表結構(添加索引)
ALTER table tableName ADD INDEX indexName(columnName)
3. 創建表的時候直接指定
CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL, INDEX [indexName] (username(length)) );
唯一索引
它與前面的普通索引類似,不同的就是:索引列的值必須唯一,但允許有空值。如果是組合索引,則列值的組合必須唯一。它有以下幾種創建方式:
1. 創建索引
CREATE UNIQUE INDEX indexName ON mytable(username(length))
2. 修改表結構
ALTER table mytable ADD UNIQUE [indexName] (username(length))
3. 創建表的時候直接指定
CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL, UNIQUE [indexName] (username(length)) );
刪除索引的語法
DROP INDEX [indexName] ON mytable;
總結
合理利用索引對於提升數據庫的性能、減輕數據庫服務器的負擔是最直接有效的手段。
其實,索引的本質就是通過縮小範圍、把隨機事件變成順序事件來篩選出最終結果,同時可以總是用同一種查找方式來定位數據,這樣就可以兼顧高效率和穩定性。