1. 索引的本質
MySQL官方對索引的定義為:索引是幫助MySQL高效獲取數據的數據結構。
數據庫查詢是數據庫的最主要功能之一。我們都希望查詢數據的速度盡可能的快,因此
數據庫系統的設計者會從查詢算法的角度進行優化。最基本的查詢算法是順序查找(liner search),這種復雜度為o(n)的算法在數據量大時,速度很慢。但是我們有更優秀的查找算法,例如二分查找(binary search)、二叉樹查找(binary tree search)等。如果稍微分析一下會發現,每種查找算法都只能用於特定的數據結構之上,例如二分查找要求被檢索數據有序,而二叉查找只能應用於二叉查找樹上,但是數據本身的組織結構不可能滿足各種數據結構(例如,一般不可能同時將兩列都按順序進行組織),所以在數據之外,數據庫系統還維護着滿足特定查找算法的數據結構,這些數據結構以某種方式引用(指向)數據,這樣就可以在這些數據結構上實訓高級查找算法。這種數據結構,就是索引。
為了加快查詢速度,可以維護一個二叉查找樹,每個節點分別包含索引鍵值和一個指向對應數據記錄物理地址的指針,這樣就可以運用二叉查找在o(logN)的復雜度內獲取到相應數據。
雖然這是一個貨真價實的索引,但是實際的數據庫系統幾乎沒有使用二叉查找樹實現的。。。
二叉排序樹
二叉排序樹的排序規則如下:
1)若左子樹不空,則左子樹上所有節點的值均小於它的根節點的值
2)若右子樹不空,則右子樹上所有節點的值均大於它的根節點的值
3)它的左右子樹也分別是二叉排序樹(遞歸定義)
二叉排序樹組織數據時,用於查找是比較方便的,因為每次經過一次節點時,最多可以減少一半的可能,不過極端情況的出現所有節點都位於同一側,直觀上看就是一條直線,那么這種查詢的效率就比較低了,因此需要對二叉樹左右子樹的高度進行平衡化處理,於是就有了平衡二叉樹(Balanced Binary Tree)。
所謂的“平衡”,說的是這棵樹的各分支的高度是均勻的,它的左右子樹高度差的絕對值小於等於1,這樣就不會出現一條支路特別長的情況。於是,在這樣的平衡樹種進行查找是,總共比較節點的次數不超過數的高度,這就確保了查詢的效率(時間復雜度為o(logN))
B樹
B樹實際上是一種平衡的多叉查找樹,也就是說最多可以開 m 個叉(m >= 2),我們稱之為 m 階B樹。
B樹的查詢過程和二叉排序樹比較類似,從根節點依次比較每個節點,因為每個節點中的關鍵字和左右子樹都是有序的,所以只要比較節點中的關鍵字,或者沿着指針就能很快的找到指定的關鍵字,如果查找失敗,則會返回葉子節點,即空指針。
B樹的特點可以總結如下:
1)關鍵字集合分布在整棵樹中;
2)任何一個關鍵字出現且只出現在一個節點中。
3)搜索有可能在非葉子節點結束
4)其搜索性能等價於在關鍵字集合內做二分查找
5)B樹在插入刪除新的數據記錄會破壞B-Tree的性質,因為在插入刪除時,需要對樹進行一個分裂、合並、轉義等操作,以保持B-Tree的性質。
B+ 樹
作為B樹的加強版,B+樹與B樹的差異在於
1)有 n 棵子樹的節點含有 n 個關鍵字(n-1個關鍵字)
2)所有的關鍵字全部存儲在葉子節點上,且葉子節點本身根據關鍵字自小而大順序連接
3)非葉子節點可以看成索引部分,節點中僅含有其子樹(根節點)中的最大(或最小)關鍵字。
B+ 樹的查找過程,與B樹類似,只不過查找時,如果在非葉子節點上的關鍵字等於給定值,並不終止,而是繼續沿着指針直到葉子節點位置。因此在B+樹,不管查找是否成功,每次查找都是走了一條從根到葉子節點的路徑。
B+ 樹的特性如下:
1)所有關鍵字都存儲在葉子節點上,且鏈表中的關鍵字恰好是有序的。
2)不可能非葉子節點命中返回。
3)非葉子節點相當於葉子節點的索引,葉子節點相當於是存儲(關鍵字)數據的數據層。
4)更適合文建索引系統。
帶有順序訪問指針的B+Tree
一般在數據庫系統或文件系統中使用的 B+Tree 結構都在經典 B+Tree 的基礎上進行了優化,增加了順序訪問指針。
在 B+Tree 的每個葉子節點增加一個指向相鄰葉子節點的指針,就形成了帶有順序訪問指針的 B+Tree。做這個優化的目的是為了提高區間訪問的性能。如上圖,要查詢key從18到49的所有數據記錄,當找到18后,只需順着節點和指針順序遍歷就可以一次性訪問到所有數據節點,極大提高了區間查詢效率。
MySQL為什么使用B樹(B+樹)
一般來說,索引本身也很大,不可能全部存儲在內存中,因此索引往往以索引文件的形式存儲在磁盤上。這樣的話,索引查找過程中就要產生磁盤 I/O 消耗,相對於內存存取,I/O存取的消耗要高幾個數量級,所以評價一個數據結構作為索引的優劣最重要的指標就是在查找過程中磁盤 I/O 的操作次數的漸進復雜度。換句話說,索引的結構組織要盡量減少查找過程中磁盤 I/O 的存取次數。
由於存儲介質的特性,磁盤本身存取就比主存慢很多,再加上機械運動耗費,磁盤的存取速度往往是主存的幾百分之一,因此為了提高效率,要盡量減少磁盤 I/O 。為了達到這個目的,磁盤往往不是嚴格按需讀取,而是每次都會預讀,即使只需要一個字節,磁盤也會從這個位置開始,順序往后讀取一定長度的數據放入內存。這樣做的理論依據是計算機科學中注明的局部性原理:
當一個數據被用到時,其附近的數據也通常會馬上被使用。
所以,程序運行期間所需要的數據通常應當比較集中。
由於磁盤順序讀取的效率很高(不需要尋道,只需要很少的旋轉時間),因此對於具有局部性的程序來說,預讀可以提高 I/O 效率。預讀的長度一般為頁的整數倍。頁是計算機管理存儲器的邏輯塊,硬件及操作系統往往將主存和磁盤存儲區分割成連續的大小相等的塊,每個存儲塊稱為一頁(大小通常為4K),主存和磁盤以頁為單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向后連續讀取一頁或幾頁載入內存中,然后異常返回,程序繼續運行。
B-/+Tree 索引的性能分析
上文說一般使用磁盤 I/O 次數評價索引結構的優劣。
B-Tree 檢索一次最多需要訪問 h 個節點。數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設為等於一個頁,這樣每個節點只需要一次 I/O 就可以完全載入。為了達到這個目的,在實際實現 B-Tree 還需要使用如下技巧:
每次新建節點時,直接申請一個頁的空間,這樣就保證一個節點物理上也存儲在一個頁里,加之計算機存儲分配都是按頁對齊的,就實現一個 node 只需一次 I/O 。
B-Tree 中一次檢索最多需要 h-1 次 I/O (根節點常駐內存),漸進復雜度為 o(h)=o(logN) d 為底。實際,出度 d 是非常大的數字,通常超過100,因此 h 非常小(通常不超過3)。(h 表示樹的高度 & 出度 d 表示的是樹的度,即樹中各節點度的最大值)
綜上,用 B-Tree 作為索引結構效率是非常高的。
而紅黑樹(二叉平衡查找樹),h 明顯深得多。由於邏輯上很近的節點(父子)物理上可能很遠,無法利用局部性,所以,紅黑樹的 I/O 漸進復雜度也為 o(h),效率明顯比 B-Tree 差很多。
上文還說,B+Tree 更適合外存索引,原因和內節點出度 d 有關。從上面分析可知,d 越大索引性能越好,而出度的上限取決於節點內 key 和 data 的大小:
dmax = floor(pagesize / (keysize + datasize + pointsize))
由於 B+Tree 內節點去掉了data 域,因此可以擁有更大的出度,有更好的性能。
MySQL索引實現
在MySQL中,索引屬於存儲引擎級別的概念,不同存儲引擎對索引的實現方式是不同的,本文主要討論MyIsam 和 InnoDB 兩個存儲引擎實現方式。
MyIsam 引擎使用 B+Tree 作為索引結構,葉節點的 data 域存放的是數據記錄的地址。下圖是 MyIsam 索引的原理圖:
這里設表一共有三列,假設我們以 col1 為主鍵,則上圖是一個 MyIsam表的主索引(Primary key)示意。可以看出 MyIsam 的索引文件僅僅保存數據記錄的地址。在 MyIsam 中,主索引和輔助索引(Secondary key)在結構上沒有任何區別,只是主索引要求key是唯一的,而輔助索引的 key 可以重復。如果我們在 col2 上建立一個輔助索引,則次索引的結構如下圖所示:
同樣也是一棵 B+Tree data域保存數據記錄的地址。因此,MyIsam中索引檢索的算法為首先按照 B+Tree 搜索算法搜索索引,如果指定的key存在,則取出其 data 域的值,然后以 data 域的值為地址,讀取相應數據記錄。
MyIsam 的索引方式也叫做“非聚集”的,之所以這么稱呼是為了與 InnoDB 的聚集索引區分。
InnoDB 索引實現
雖然 InnoDB 也使用 B+Tree 作為索引結構,但具體實現方式卻與 MyIsam 截然不同。
第一個重大區別是 InnoDB 的數據文件本身就是索引文件。從上文知,MyIsam 索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址。而在 InnoDB 中,表數據文件本身就是按 B+Tree 組織的一個索引結構,這棵樹的葉節點 data 域保存了完整的數據記錄。這個索引的 key 是數據表的主鍵,因此 InnoDB 表數據文件本身就是主索引。
InnoDB主索引(同時也是數據文件),其葉子節點包含了完整的數據記錄。這種索引叫做聚集索引。因為InnoDB 的數據文件本身要按主鍵聚集,所以 InnoDB要求表必須有主鍵(MyIsam可以沒有),如果沒有顯式指定,則MySQL會自動選擇一個可以唯一標識數據記錄的列為主鍵,如果不存在這樣的列,則MySQL自動為 InnoDB 表生成一個隱含字段為主鍵,這個字段長度6字節,類型是長整型。
第二個與MyIsam不同的是InnoDB的輔助索引data域存儲相應記錄主鍵的值,而不是地址。換句話說,InnoDB的所有輔助索引都引用主鍵作為data域。例如,下圖的輔助索引:
聚集索引這種實現方式使得按主鍵搜索十分高效,但是輔助索引需要檢索兩遍索引:先檢查輔助索引獲得主鍵,然后用主鍵到住索引中檢索獲得記錄。
了解不同的存儲引擎實現方式對於正確使用和優化索引都有幫助,例如知道了 InnoDB 的索引實現后,就容易明白為什么不建議使用過長的字段作為主鍵,因為所有輔助索引都引用主索引,過長的主索引會令輔助索引變得過大。再例如,用非單調的字段作為主鍵在 InnoDB 中不太好, 因為 InnoDB 數據文件本身是一棵 B+Tree,非單調的主鍵會造成在插入新記錄時數據文件為了維護 B+Tree 的特性而頻繁的分裂調整,十分低效,而使用自增字段作為主鍵則是一個很好的選擇。
2. 索引的類型
mysql索引有四種類型:主鍵索引、唯一索引、普通索引和全文索引。通過給字段添加索引,可以提高數據的讀取速度,提高項目的並發能力和抗壓能力。索引優化是 mysql 的一種優化方式。索引的作用相當於圖書的目錄,可以根據目錄中的頁碼快速找到所需的內容。
主鍵索引:主鍵是一種唯一性索引,但它必須指定為 PRIMARY KEY,每個表只能有一個主鍵。
alter table tabname add primary key('字段名');
唯一索引:索引列的所有值都只能出現一次,即必須唯一,值可以為 空。
alter table tabname add unique index('字段名');
普通索引:基本的索引類型,值可以為空,沒有唯一性的限制。
alter table tabname add index('字段名');
全文索引:全文索引的索引類型為 FULLTEXT。全文索引可以在 varchar、char、text 類型的列上創建。可以通過 ALTER TABLE 或者 CREATE INDEX 命令創建。對於大規模的數據集,通過 ALTER TABLE(或者CREATE INDEX)命令創建全文索引要比把記錄插入帶有全文索引的空表更快。MyIsam 支持全文索引,InnoDB 在mysql5.6 后支持了全文索引。
全文索引不支持中文,需要借 sphinx(coreseek)或訊搜<、code>技術處理中文。
alter table tabname add fulltext('字段名');
查看表的所有索引和刪除
show indexes from 表名;
show keys from 表名;
alter table 表名 drop index 索引名;
3. 執行計划explain

1)id
執行編號,標識 select 所屬的行。如果語句中沒有子查詢或關聯查詢,只有唯一的 select ,每行都將顯示1.否則,內層的 select 語句一般會順序編號,對應於其在原始語句中的位置。
列數字越大越先執行,如果數字一樣大,那么就從上到下執行,id列為 null 的就表示這是一個結果集,不需要使用它來進行查詢。
- 關聯優化器會為查詢選擇關聯順序,左側深度優先
- 當 from 中有子查詢的時候,表名是 derivedN 的形式,N指向子查詢,也就是 explain 結果中的下一列
- 當有 union result 的時候,表名是 union 1,2 等的形式,1,2 表示參與 union 的query id
4. 索引的使用場景
1)MySQL目前支持前導列
mysql 只支持最左前綴原則進行篩選
創建復合索引
2)索引列上的范圍查找
對於某個條件進行范圍查找時,如果這個列上有索引,且使用 where ... between and ... >,< 等范圍操作,那么可能用到索引范圍查找,如果索引范圍查找的成本太高,數據庫可能會選擇全表掃描。
IN 也是范圍查找的范疇,但是帶操作函數的 IN 不屬於查找的范疇
3)join 列
在聯合查詢兩個表時,比如查詢語句為 select a.col1, b.col2 from a join b on a.id = b.id,其中id 為兩個表的主鍵,如果 a 是小表,那么 a 就被視為驅動表,那么數據庫可能全表掃描 a 表,並用 a 表的每一個 id 去探測 b 表的索引查詢匹配的記錄。
4)where 子句
形如:
where a = ? and b = ? and c > 1000
where a = ? and b = ? and c = ? and d > 1000
where 子句的條件列是復合索引前面的索引列 + 另一個列的范圍查找
create index idx_a_b_c_d on tb1(a, b, c, d);
上面才會用到這個索引。
下面的兩個查詢:
where a=? and b=? and c>1000 and d<10000 這個例子中 d < 10000 這個操作並不會走索引
where a>? and b=? and c>1000 and d<10000 這個例子 a 上有范圍查找,那么 b、c、d 列上的索引信息都不能被利用
注,創建索引,考慮把復合索引的范圍查找放到最后。
5)mysql 優化器
mysql 優化器會做一些特殊優化,比如對於索引查找 max(索引列)可以直接進行定位。遇到 max, min 是可以在列上做索引。
5、索引失效
https://www.jianshu.com/p/aedf4c0972e8
https://www.jianshu.com/p/3ccca0444432
1)where 條件中的索引列不能是表達式的一部分,mysql 不支持函數索引,即對查詢的列上有運算或者函數
2)InnoDB 二級索引底層葉子節點存儲的是索引+主鍵值
InnoDB的非主鍵索引存儲的不是實際的記錄的指針,而是主鍵的值,所以主鍵最好是整數型,如自增ID,基於主鍵存取數據是最高效的,使用二級索引存取數據則需要進行二次索引查找。
3)索引盡量是高選擇性的,如性別,男女兩種情況不宜使用索引,基本會全表掃描
4)查詢條件中有 or , 仍是全表掃描
5)like 查詢是以 ‘%’ 開頭
6)如果列類型是字符串,那一定要在條件中將數據使用引號引起來,否則不使用索引。
explain select name, age, address from user where name=10 ---- 這樣的不使用索引,走全表掃描。這里的原因和上面第一條是一樣的,首先 mysql 有個類型轉換規則就是將字符串轉成數字 explain select name, age, address from user where cast(name as signed)=10。很明顯,name上有函數,所以不會走索引。
7)左連接查詢或者右連接查詢關聯的字段編碼格式不一樣
explain select a.name, b.job from user a left join job b on a.name=b.name
如果 user 表的 name 字段編碼是 utf8mb4, 而 job 表的 name 字段編碼是 gbk。執行左外連接查詢 b 表仍然走的是全表掃描。
8)如果mysql 估計使用全表掃描要比使用索引快,則不使用索引
數據量很少的情況。
9)連接查詢中,按照優化器順序的第一張表不會走索引
條件:a 表 name 有索引, b 表name 沒有索引
結果: explain a.name, a.age, b.name, b.job from user a left join job —— a表 b表都是全表掃描
explain select a.name, a.age, b.name, b.job from user a right join job b on a.name=b.name——a表使用上了索引
explain select a.name, a.age, b.name, b.job from user a inner join job b on a.name = b.name——a表使用到了索引
從上面3個連接查詢來看只有左外連接a 表沒有用到索引的,這就是因為由於是左外連接,所以優化器的執行順序是 a 表、 b 表,也就是說首先全表掃描 a 表,再根據 a 表的 name 查詢 b 表的值,所以 a 表無法用到索引。所以,一般這種連接查詢,A表關聯B表,要在將優化器順序的第二張表上關聯的字段加索引,而第一張不用加,無用的索引也會影響性能。而第三個例子中的內連接就不一樣了,如果是內連接優化器本身就會根據索引情況,連接表的大小去選擇執行順序,所以上例中的內連接執行順序是 b, a,這樣仍然可以用到 a 的索引。
10)如果查詢中沒有用到聯合索引的第一個字段,則不會走索引。
注意:
索引盡量是高選擇性的
使用更短的索引:可以考慮前綴索引,但應確保選擇的前綴的長度可以保證大部分值是唯一的。衡量不同前綴索引唯一值比例,select count(distinct left(col_name, 5))/count(*)) as sele5; select count(distinct left(col_name, 6))/count(*)) as sele6; select count(distinct left(col_name, 7))/count(*)) as sele7;
避免創建過多的索引:索引過多可能會浪費大量空間,尤其本身字段量較大的字符串,索引過多可能會浪費空間,且降低修改數據的速度,所以不要創建過多的索引,也不要創建重復的索引。
如果是唯一值的列,創建唯一索引會更佳,也可以確保不會出現重復數據
使用覆蓋索引能大大提高性能:所有數據都可以從索引中得到,而不需要去讀物理記錄。
利用索引排序:mysql 有兩種方法可以產生有序結果,一種是使用文件排序,另一種是掃描有序的索引,我們盡量使用索引來排序:1)盡量保證索引列和 order by 的列相同,且各列按照相同的順序排序,2)如果連接多張表,那么 order by 引用的列需要在表連接順序的首張表內
添加冗余索引要權衡:如果一個索引 column A ,那么新的索引(columnA, columnB)就是冗余索引。一般情況下不論是新增冗余索引,還是擴展原索引為冗余索引,都會導致索引文件的增大,並且增加了維護索引的開銷。比如更改了列值也要對此列所以在索引進行重新排序。