1. 程式人生 > 實用技巧 >MySQL索引的原理及使用

MySQL索引的原理及使用

前言:

  上篇文章中學習了MySQL庫的架構以及儲存引擎,瞭解了基本索引(普通索引,唯一索引,主鍵索引),著重介紹了innerDB的儲存方式以及記憶體模型,本篇文章和大家探討一下MySQL庫中索引的原理以及索引底層的資料結構。

1. 索引是什麼

1.1. 索引的定義

  維基百科對資料庫索引的定義:資料庫索引,是資料庫管理系統(DBMS)中一個排序的資料結構,以協助快速查詢、更新資料庫表中資料。   怎麼理解這個定義呢?

  首先資料是以檔案的形式存放在磁碟上面的,每一行資料都有它的磁碟地址。如果沒有索引的話,要從 500 萬行資料裡面檢索一條資料,只能依次遍歷這張表的全部資料,直到找到這條資料。但是有了索引之後,只需要在索引裡面去檢索這條資料就行了,因為它是一種特殊的專門用來快速檢索的資料結構,我們找到資料存放的磁碟地址以後,就可以拿到資料了。

  就像我們從一本 500 頁的書裡面去找特定的一小節的內容,肯定不可能從第一頁開始翻。那麼這本書有專門的目錄,它可能只有幾頁的內容,它是按頁碼來組織的,可以 根據拼音或者偏旁部首來查詢,只要確定內容對應的頁碼,就能很快地找到我們想要的內容。

1.2.索引型別

  如何建立一個索引?

  第一個是索引的名稱,第二個是索引的列,比如我們是要對 id 建立索引還是對 name建立索引。   在 InnoDB 裡面,索引型別有三種,普通索引、唯一索引(主鍵索引是特殊的唯一索引)、全文索引。   普通(Normal):也叫非唯一索引,是最普通的索引,沒有任何的限制。   唯一(Unique):唯一索引要求鍵值不能重複。另外需要注意的是,主鍵索引是一種特殊的唯一索引,它還多了一個限制條件,要求鍵值不能為空。主鍵索引用 primay key           建立。   全文
(Fulltext):針對比較大的資料,比如我們存放的是訊息內容,有幾 KB 的資料的這種情況,如果要解決 like 查詢效率低的問題,可以建立全文索引。只有文字型別             的欄位才可以建立全文索引,比如 char、varchar、text。   MyISAM 和 InnoDB 支援全文索引。這個是索引的三種類型:普通、唯一、全文。我們說索引是一種資料結構,那麼它到底應該選擇一種什麼資料結構,才能實現數 據的高效檢索呢?

2. 索引儲存模型推演

2.1. 二分查詢

  618過去之後,你女朋友跟你玩了一個猜數字的遊戲。   猜猜我昨天買了多少錢,給你五次機會。   10000?低了。30000?高了。接下來你會猜多少?   20000。為什麼你不猜 11000,也不猜 29000 呢?   哈哈,上來就是一連串的靈魂拷問,各位有物件的是否保護好自己的錢包了呢。   其實這個就是二分查詢的一種思想,也叫折半查詢,每一次,我們都把候選資料縮小了一半。如果資料已經排過序的話,這種方式效率比較高。所以第一個,我們可以考慮用有序陣列作為索引的資料結構。   有序陣列的等值查詢和比較查詢效率非常高,但是更新資料的時候會出現一個問題,可能要挪動大量的資料(改變 index),所以只適合儲存靜態的資料。   為了支援頻繁的修改,比如插入資料,我們需要採用連結串列。連結串列的話,如果是單鏈表,它的查詢效率還是不夠高。所以,有沒有可以使用二分查詢的連結串列呢?為了解決這個問題,BST(Binary Search Tree)也就是我們所說的二叉查詢樹誕生了。

2.2. 二叉查詢樹(BST Binary Search Tree)

  二叉查詢樹的特點是什麼?   左子樹所有的節點都小於父節點,右子樹所有的節點都大於父節點。投影到平面以後,就是一個有序的線性表。

  二叉查詢樹既能夠實現快速查詢,又能夠實現快速插入。
  但是二叉查詢樹有一個問題:
  就是它的查詢耗時是和這棵樹的深度相關的,在最壞的情況下時間複雜度會退化成O(n)。

  什麼情況是最壞的情況呢?我們開啟這樣一個網站來看一下,這裡面有各種各樣的資料結構的動態演示,包括 BST 二叉查詢樹:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html   還是剛才的這一批數字,如果我們插入的資料剛好是有序的,2、6、11、13、17、22。這個時候我們的二叉查詢樹變成了什麼樣了呢? 它會變成連結串列(我們把這種樹叫做“斜樹”),這種情況下不能達到加快檢索速度的目的,和順序查詢效率是沒有區別的。

  造成它傾斜的原因是什麼呢?因為左右子樹深度差太大,這棵樹的左子樹根本沒有節點——也就是它不夠平衡。所以,我們有沒有左右子樹深度相差不是那麼大,更加平衡的樹呢?這個就是平衡二叉樹,叫做 Balanced binary search trees,或者 AVL 樹(AVL 是發明這個資料結構的人的名字)。

2.3. 平衡二叉樹(AVL Tree)(左旋、右旋)

  AVL Trees (Balanced binary search trees)平衡二叉樹的定義:左右子樹深度差絕對值不能超過 1。是什麼意思呢?比如左子樹的深度是 2,右子樹的深度只能是 1 或者 3。 這個時候我們再按順序插入 1、2、3、4、5、6,一定是這樣,不會變成一棵“斜樹”。

  那它的平衡是怎麼做到的呢?怎麼保證左右子樹的深度差不能超過 1 呢?https://www.cs.usfca.edu/~galles/visualization/AVLtree.html插入 1、2、3。 我們注意看:當我們插入了 1、2 之後,如果按照二叉查詢樹的定義,3 肯定是要在2 的右邊的,這個時候根節點 1 的右節點深度會變成 2,但是左節點的深度是 0,因為它 沒有子節點,所以就會違反平衡二叉樹的定義。   那應該怎麼辦呢?因為它是右節點下面接一個右節點,右-右型,所以這個時候我們要把 2 提上去,這個操作叫做左旋。

  同樣的,如果我們插入 7、6、5,這個時候會變成左左型,就會發生右旋操作,把 6提上去。   所以為了保持平衡,AVL 樹在插入和更新資料的時候執行了一系列的計算和調整的操作。   平衡的問題我們解決了,那麼平衡二叉樹作為索引怎麼查詢資料?   在平衡二叉樹中,一個節點,它的大小是一個固定的單位,作為索引應該儲存什麼內容?   它應該儲存三塊的內容:   第一個是索引的鍵值。比如我們在 id 上面建立了一個索引,我在用 where id =1 的條件查詢的時候就會找到索引裡面的 id 的這個鍵值。   第二個是資料的磁碟地址,因為索引的作用就是去查詢資料的存放的地址。   第三個,因為是二叉樹,它必須還要有左子節點和右子節點的引用,這樣我們才能找到下一個節點。比如大於 26 的時候,走右邊,到下一個樹的節點,繼續判斷。

  如果是這樣儲存資料的話,我們來看一下會有什麼問題。在分析用 AVL 樹儲存索引資料之前,我們先來學習一下 InnoDB 的邏輯儲存結構。

2.3.1.InnoDB 邏輯儲存結構

  官網:

  https://dev.mysql.com/doc/refman/5.7/en/innodb-disk-management.html   https://dev.mysql.com/doc/refman/5.7/en/innodb-file-space.html   MySQL 的儲存結構分為 5 級:表空間、段、簇、頁、行。

表空間 Table Space

  表空間可以看做是 InnoDB 儲存引擎邏輯結構的最高層,所有的資料都存放在表空間中。分為:系統表空間、獨佔表空間、通用表空間、臨時表空間、Undo 表空間。

段 Segment

  表空間是由各個段組成的,常見的段有資料段、索引段、回滾段等,段是一個邏輯的概念。一個 ibd 檔案(獨立表空間檔案)裡面會由很多個段組成。建立一個索引會建立兩個段,一個是索引段:leaf node segment,一個是資料段:non-leaf node segment。索引段管理非葉子節點的資料。資料段管理葉子節點的資料。也就是說,一個表的段數,就是索引的個數乘以 2。

簇 Extent

  一個段(Segment)又由很多的簇(也可以叫區)組成,每個區的大小是 1MB(64個連續的頁)。每一個段至少會有一個簇,一個段所管理的空間大小是無限的,可以一直擴充套件下去,但是擴充套件的最小單位就是簇。

頁 Page

  為了高效管理物理空間,對簇進一步細分,就得到了頁。簇是由連續的頁(Page)組成的空間,一個簇中有 64 個連續的頁。 (1MB/16KB=64)。這些頁面在物理上和 邏輯上都是連續的。   跟大多數資料庫一樣,InnoDB 也有頁的概念(也可以稱為塊),每個頁預設 16KB。頁是 InnoDB 儲存引擎磁碟管理的最小單位,通過 innodb_page_size 設定。   一個表空間最多擁有 2^32 個頁,預設情況下一個頁的大小為 16KB,也就是說一個表空間最多儲存 64TB 的資料。   注意,檔案系統中,也有頁的概念。   作業系統和記憶體打交道,最小的單位是頁 Page。檔案系統的記憶體頁通常是 4K。

  假設一行資料大小是 1K,那麼一個數據頁可以放 16 行這樣的資料。舉例:一個頁放 3 行資料

  往表中插入資料時,如果一個頁面已經寫完,產生一個新的葉頁面。如果一個簇的所有的頁面都被用完,會從當前頁面所在段新分配一個簇。

如果資料不是連續的,往已經寫滿的頁中插入資料,會導致葉頁面分裂:

行 Row

  InnoDB 儲存引擎是面向行的(row-oriented),也就是說資料的存放按行進行存放。

2.3.2.AVL 樹用於儲存索引資料

  首先,索引的資料,是放在硬碟上的。檢視資料和索引的大小:
select CONCAT(ROUND(SUM(DATA_LENGTH/1024/1024),2),'MB') AS data_len,
CONCAT(ROUND(SUM(INDEX_LENGTH/1024/1024),2),'MB') as index_len from information_schema.TABLES where table_schema='gupao' and table_name='user_innodb';
  當我們用樹的結構來儲存索引的時候,訪問一個節點就要跟磁碟之間發生一次 IO。InnoDB 操作磁碟的最小的單位是一頁(或者叫一個磁碟塊),大小是 16K(16384 位元組)。那麼,一個樹的節點就是 16K 的大小。   如果我們一個節點只存一個鍵值+資料+引用,例如整形的欄位,可能只用了十幾個或者幾十個位元組,它遠遠達不到 16K 的容量,所以訪問一個樹節點,進行一次 IO 的時候,浪費了大量的空間。   所以如果每個節點儲存的資料太少,從索引中找到我們需要的資料,就要訪問更多的節點,意味著跟磁碟互動次數就會過多。 如果是機械硬碟時代,每次從磁碟讀取資料需要 10ms 左右的定址時間,互動次數越多,消耗的時間就越多。

  比如上面這張圖,我們一張表裡面有 6 條資料,當我們查詢 id=37 的時候,要查詢兩個子節點,就需要跟磁碟互動 3 次,如果我們有幾百萬的資料呢?這個時間更加難以估計。

  所以我們的解決方案是什麼呢?   第一個就是讓每個節點儲存更多的資料。   第二個,節點上的關鍵字的數量越多,我們的指標數也越多,也就是意味著可以有更多的分叉(我們把它叫做“路數”)。   因為分叉數越多,樹的深度就會減少(根節點是 0)。   這樣,我們的樹是不是從原來的高瘦高瘦的樣子,變成了矮胖矮胖的樣子?   這個時候,我們的樹就不再是二叉了,而是多叉,或者叫做多路。

2.4. 多路平衡查詢樹(B Tree)(分裂、合併)

  Balanced Tree   這個就是我們的多路平衡查詢樹,叫做 B Tree(B 代表平衡)。   跟 AVL 樹一樣,B 樹在枝節點和葉子節點儲存鍵值、資料地址、節點引用。   它有一個特點:分叉數(路數)永遠比關鍵字數多 1。比如我們畫的這棵樹,每個節點儲存兩個關鍵字,那麼就會有三個指標指向三個子節點。   B Tree 的查詢規則是什麼樣的呢?   比如我們要在這張表裡面查詢 15。   因為 15 小於 17,走左邊。   因為 15 大於 12,走右邊。   在磁碟塊 7 裡面就找到了 15,只用了 3 次 IO。   這個是不是比 AVL 樹效率更高呢?那 B Tree 又是怎麼實現一個節點儲存多個關鍵字,還保持平衡的呢?跟 AVL 樹有什麼區別?   https://www.cs.usfca.edu/~galles/visualization/Algorithms.html   比如 Max Degree(路數)是 3 的時候,我們插入資料 1、2、3,在插入 3 的時候,本來應該在第一個磁碟塊,但是如果一個節點有三個關鍵字的時候,意味著有 4 個指標,子節點會變成 4 路,所以這個時候必須進行分裂。把中間的資料 2 提上去,把 1 和 3 變成 2 的子節點。   如果刪除節點,會有相反的合併的操作。注意這裡是分裂和合並,跟 AVL 樹的左旋和右旋是不一樣的。我們繼續插入 4 和 5,B Tree 又會出現分裂和合並的操作。   從這個裡面我們也能看到,在更新索引的時候會有大量的索引的結構的調整,所以解釋了為什麼我們不要在頻繁更新的列上建索引,或者為什麼不要更新主鍵。 節點的分裂和合並,其實就是 InnoDB 頁的分裂和合並。

2.5. B+樹(加強版多路平衡查詢樹)

  B Tree 的效率已經很高了,為什麼 MySQL 還要對 B Tree 進行改良,最終使用了B+Tree 呢?總體上來說,這個 B 樹的改良版本解決的問題比 B Tree 更全面。 我們來看一下 InnoDB 裡面的 B+樹的儲存結構:

  MySQL 中的 B+Tree 有幾個特點:

  1、它的關鍵字的數量是跟路數相等的;   2、B+Tree 的根節點和枝節點中都不會儲存資料,只有葉子節點才儲存資料。搜尋到關鍵字不會直接返回,會到最後一層的葉子節點。比如我們搜尋 id=28,雖然在第一 層直接命中了,但是全部的資料在葉子節點上面,所以我還要繼續往下搜尋,一直到葉子節點。   舉個例子:假設一條記錄是 1K,一個葉子節點(一頁)可以儲存 16 條記錄。非葉子節點可以儲存多少個指標?假設索引欄位是 bigint 型別,長度為 8 位元組。指標大小在 InnoDB 原始碼中設定為6 位元組,這樣一共 14 位元組。非葉子節點(一頁)可以存16384/14=1170 個這樣的單元(鍵值+指標),代表有 1170 個指標。樹 深 度 為 2 的 時 候 , 有 1170^2 個 葉 子 節 點 , 可 以 存 儲 的 數 據 為1170*1170*16=21902400。   在查詢資料時一次頁的查詢代表一次 IO,也就是說,一張 2000 萬左右的表,查詢資料最多需要訪問 3 次磁碟。所以在 InnoDB 中 B+ 樹深度一般為 1-3 層,它就能滿足千萬級的資料儲存。   3、B+Tree 的每個葉子節點增加了一個指向相鄰葉子節點的指標,它的最後一個數據會指向下一個葉子節點的第一個資料,形成了一個有序連結串列的結構。   4、它是根據左閉右開的區間 [ )來檢索資料。   我們來看一下 B+Tree 的資料搜尋過程:   1)比如我們要查詢 28,在根節點就找到了鍵值,但是因為它不是頁子節點,所以會繼續往下搜尋,28 是[28,66)的左閉右開的區間的臨界值,所以會走中間的子節點,然    後繼續搜尋,它又是[28,34)的左閉右開的區間的臨界值,所以會走左邊的子節點,最後在葉子節點上找到了需要的資料。   2)第二個,如果是範圍查詢,比如要查詢從 22 到 60 的資料,當找到 22 之後,只需要順著節點和指標順序遍歷就可以一次性訪問到所有的資料節點,這樣就極大地提高    了區間查詢效率(不需要返回上層父節點重複遍歷查詢)。   總結一下,InnoDB 中的 B+Tree 的特點:   1)它是 B Tree 的變種,B Tree 能解決的問題,它都能解決。B Tree 解決的兩大問題是什麼?(每個節點儲存更多關鍵字;路數更多)   2)掃庫、掃表能力更強(如果我們要對錶進行全表掃描,只需要遍歷葉子節點就可以了,不需要遍歷整棵 B+Tree 拿到所有的資料)   3) B+Tree 的磁碟讀寫能力相對於 B Tree 來說更強(根節點和枝節點不儲存資料區,所以一個節點可以儲存更多的關鍵字,一次磁碟載入的關鍵字更多)   4)排序能力更強(因為葉子節點上有下一個資料區的指標,資料形成了連結串列)   5)效率更加穩定(B+Tree 永遠是在葉子節點拿到資料,所以 IO 次數是穩定的)

2.6. 為什麼不用紅黑樹?

  紅黑樹也是 BST 樹,但是不是嚴格平衡的。   必須滿足 5 個約束:   1、節點分為紅色或者黑色。   2、根節點必須是黑色的。   3、葉子節點都是黑色的 NULL 節點。   4、紅色節點的兩個子節點都是黑色(不允許兩個相鄰的紅色節點)。   5、從任意節點出發,到其每個葉子節點的路徑中包含相同數量的黑色節點。   插入:60、56、68、45、64、58、72、43、49   基於以上規則,可以推匯出:   從根節點到葉子節點的最長路徑(紅黑相間的路徑)不大於最短路徑(全部是黑色節點)的 2 倍。   為什麼不用紅黑樹?1、只有兩路;2、不夠平衡。   紅黑樹一般只放在記憶體裡面用。例如 Java 的 TreeMap。 2.7. 索引方式:真的是用的 B+Tree 嗎?

  在 Navicat 的工具中,建立索引,索引方式有兩種,Hash 和 B Tree。
  HASH:以 KV 的形式檢索資料,也就是說,它會根據索引欄位生成雜湊碼和指標, 指標指向資料。

  雜湊索引有什麼特點呢?

  第一個,它的時間複雜度是 O(1),查詢速度比較快。因為雜湊索引裡面的資料不是按順序儲存的,所以不能用於排序。

  第二個,我們在查詢資料的時候要根據鍵值計算雜湊碼,所以它只能支援等值查詢= IN),不支援範圍查詢(> < >= <= between and)。另外一個就是如果欄位重複值很多的時候,會出現大量的雜湊衝突(採用拉鍊法解決),效率會降低。

  InnoDB 可以在客戶端建立一個索引,使用雜湊索引嗎?

  官網:https://dev.mysql.com/doc/refman/5.7/en/innodb-introduction.html

  InnoDB utilizes hash indexes internally for its Adaptive Hash Index feature

  直接翻譯過來就是:InnoDB 內部使用雜湊索引來實現自適應雜湊索引特性。

  這句話的意思是 InnoDB 只支援顯式建立 B+Tree 索引,對於一些熱點資料頁,InnoDB 會自動建立自適應 Hash 索引,也就是在 B+Tree 索引基礎上建立 Hash 索引,

這個過程對於客戶端是不可控制的,隱式的。我們在 Navicat 工具裡面選擇索引方法是雜湊,但是它建立的還是 B+Tree 索引,這個不是我們可以手動控制的。

  因為B Tree 和B+Tree 的特性,它們廣泛地用在檔案系統和資料庫中,例如Windows的 HPFS 檔案系統,Oracel、MySQL、SQLServer 資料庫。

3. B+Tree 落地形式

3.1. MySQL 架構

  MySQL 是一個支援外掛式儲存引擎的資料庫。在 MySQL 裡面,每個表在建立的時候都可以指定它所使用的儲存引擎。這裡我們主要關注一下最常用的兩個儲存引擎,MyISAM 和 InnoDB 的索引的實現。

3.2. MySQL 資料儲存檔案

  首先,MySQL 的資料都是檔案的形式存放在磁碟中的,我們可以找到這個資料目錄的地址。在 MySQL 中有這麼一個引數,我們來看一下:
show VARIABLES LIKE 'datadir';
  每個資料庫有一個目錄,我們新建了一個叫做 gupao 的資料庫,那麼這裡就有一個gupao 的資料夾。   這個資料庫裡面我們又建了 5 張表:archive、innodb、memory、myisam、csv。   我們進入 gupao 的目錄,發現這裡面有一些跟我們建立的表名對應的檔案。   在這裡我們能看到,每張 InnoDB 的表有兩個檔案(.frm 和.ibd),MyISAM 的表有三個檔案(.frm、.MYD、.MYI)。   有一個是相同的檔案,.frm。 .frm 是 MySQL 裡面表結構定義的檔案,不管你建表的時候選用任何一個儲存引擎都會生成。我們主要看一下其他兩個檔案是怎麼實現 MySQL 不同的儲存引擎的索引的。

4.2.1.MyISAM

  在 MyISAM 裡面,另外有兩個檔案:   一個是.MYD 檔案,D 代表 Data,是 MyISAM 的資料檔案,存放資料記錄,比如我們的 user_myisam 表的所有的表資料。   一個是.MYI 檔案,I 代表 Index,是 MyISAM 的索引檔案,存放索引,比如我們在id 欄位上面建立了一個主鍵索引,那麼主鍵索引就是在這個索引檔案裡面。 也就是說,在 MyISAM 裡面,索引和資料是兩個獨立的檔案。   那我們怎麼根據索引找到資料呢?   MyISAM 的 B+Tree 裡面,葉子節點儲存的是資料檔案對應的磁碟地址。所以從索引檔案.MYI 中找到鍵值後,會到資料檔案.MYD 中獲取相應的資料記錄。   這裡是主鍵索引,如果是輔助索引,有什麼不一樣呢?   在 MyISAM 裡面,輔助索引也在這個.MYI 檔案裡面。   輔助索引跟主鍵索引儲存和檢索資料的方式是沒有任何區別的,一樣是在索引檔案裡面找到磁碟地址,然後到資料檔案裡面獲取資料。


4.2.2.InnoDB

  InnoDB 只有一個檔案(.ibd 檔案),那索引放在哪裡呢?   在 InnoDB 裡面,它是以主鍵為索引來組織資料的儲存的,所以索引檔案和資料檔案是同一個檔案,都在.ibd 檔案裡面。   在 InnoDB 的主鍵索引的葉子節點上,它直接儲存了我們的資料。

  什麼叫做聚集索引(聚簇索引)?   就是索引鍵值的邏輯順序跟表資料行的物理儲存順序是一致的。(比如字典的目錄是按拼音排序的,內容也是按拼音排序的,按拼音排序的這種目錄就叫聚集索引)。 在 InnoDB 裡面,它組織資料的方式叫做叫做(聚集)索引組織表(clustered indexorganize table),所以主鍵索引是聚集索引,非主鍵都是非聚集索引。如果 InnoDB 裡面主鍵是這樣儲存的,那主鍵之外的索引,比如我們在 name 欄位上面建的普通索引,又是怎麼儲存和檢索資料的呢?

  InnoDB 中,主鍵索引和輔助索引是有一個主次之分的。   輔助索引儲存的是輔助索引和主鍵值。如果使用輔助索引查詢,會根據主鍵值在主鍵索引中查詢,最終取得資料。比如我們用 name 索引查詢 name= '青山',它會在葉子節點找到主鍵值,也就是id=1,然後再到主鍵索引的葉子節點拿到資料。   為什麼在輔助索引裡面儲存的是主鍵值而不是主鍵的磁碟地址呢?如果主鍵的資料型別比較大,是不是比存地址更消耗空間呢?   我們前面說到 B Tree 是怎麼實現一個節點儲存多個關鍵字,還保持平衡的呢?是因為有分叉和合並的操作,這個時候鍵值的地址會發生變化,所以在輔助索引裡 面不能儲存地址。   另一個問題,如果一張表沒有主鍵怎麼辦?   1、如果我們定義了主鍵(PRIMARY KEY),那麼 InnoDB 會選擇主鍵作為聚集索引。   2、如果沒有顯式定義主鍵,則 InnoDB 會選擇第一個不包含有 NULL 值的唯一索引作為主鍵索引。   3、如果也沒有這樣的唯一索引,則 InnoDB 會選擇內建 6 位元組長的 ROWID 作為隱藏的聚集索引,它會隨著行記錄的寫入而主鍵遞增。
select _rowid name from t2;

4. 索引使用原則

  我們容易有以一個誤區,就是在經常使用的查詢條件上都建立索引,索引越多越好,那到底是不是這樣呢?

4.1. 列的離散(sàn)度

  第一個叫做列的離散度,我們先來看一下列的離散度的公式:count(distinct(column_name)) : count(*),列的全部不同值和所有資料行的比例。資料行數相同的情況下,分子越大,列的離散度就越高。

  簡單來說,如果列的重複值越多,離散度就越低,重複值越少,離散度就越高。瞭解了離散度的概念之後,我們再來思考一個問題,我們在 name 上面建立索引和 在 gender 上面建立索引有什麼區別。當我們用在 gender 上建立的索引去檢索資料的時候,由於重複值太多,需要掃描的行數就更多。例如,我們現在在 gender 列上面建立一個索引,然後看一下執行計劃。
ALTER TABLE user_innodb DROP INDEX idx_user_gender; 
ALTER TABLE user_innodb ADD INDEX idx_user_gender (gender); -- 耗時比較久
EXPLAIN SELECT * FROM `user_innodb` WHERE gender = 0;

而 name 的離散度更高,比如“青山”的這名字,只需要掃描一行。

  查看錶上的索引,Cardinality [kɑ:dɪ'nælɪtɪ] 代表基數,代表預估的不重複的值

  show indexes from user_innodb;   如果在 B+Tree 裡面的重複值太多,MySQL 的優化器發現走索引跟使用全表掃描差不了多少的時候,就算建了索引,也不一定會走索引

4.2. 聯合索引最左匹配

  前面我們說的都是針對單列建立的索引,但有的時候我們的多條件查詢的時候,也會建立聯合索引。單列索引可以看成是特殊的聯合索引。比如我們在 user 表上面,給 name 和 phone 建立了一個聯合索引。
ALTER TABLE user_innodb DROP INDEX comidx_name_phone; ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);

  聯合索引在 B+Tree 中是複合的資料結構,它是按照從左到右的順序來建立搜尋樹的(name 在左邊,phone 在右邊)。從這張圖可以看出來,name 是有序的,phone 是無序的。當 name 相等的時候,phone 才是有序的。這個時候我們使用 where name= '青山' and phone = '136xx '去查詢資料的時候,B+Tree 會優先比較 name 來確定下一步應該搜尋的方向,往左還是往右。如果 name相同的時候再比較 phone。但是如果查詢條件沒有 name,就不知道第一步應該查哪個節點,因為建立搜尋樹的時候 name 是第一個比較因子,所以用不到索引。

5.2.1.什麼時候用到聯合索引

  所以,我們在建立聯合索引的時候,一定要把最常用的列放在最左邊。   比如下面的三條語句,能用到聯合索引嗎?   1)使用兩個欄位,可以用到聯合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= '權亮' AND phone = '15204661800';

  2)使用左邊的 name 欄位,可以用到聯合索引:

  3)使用右邊的 phone 欄位,無法使用索引,全表掃描:

5.2.2.如何建立聯合索引

  有一天我們的我們的專案裡面有兩個查詢很慢。   按照我們的想法,一個查詢建立一個索引,所以我們針對這兩條 SQL 建立了兩個索引,這種做法覺得正確嗎?

  當我們建立一個聯合索引的時候,按照最左匹配原則,用左邊的欄位 name 去查詢的時候,也能用到索引,所以第一個索引完全沒必要。 相當於建立了兩個聯合索引(name),(name,phone)。如果我們建立三個欄位的索引 index(a,b,c),相當於建立三個索引: index(a) index(a,b) index(a,b,c) 用 where b=? 和 where b=? and c=? 和 where a=? and c=?是不能使用到索引的。不能不用第一個欄位,不能中斷。 這裡就是 MySQL 聯合索引的最左匹配原則。

4.3. 覆蓋索引

  回表:   非主鍵索引,我們先通過索引找到主鍵索引的鍵值,再通過主鍵值查出索引裡面沒有的資料,它比基於主鍵索引的查詢多掃描了一棵索引樹,這個過程就叫回表。 例如:select * from user_innodb where name = '青山';   在輔助索引裡面,不管是單列索引還是聯合索引,如果 select 的資料列只用從索引中就能夠取得,不必從資料區中讀取,這時候使用的索引就叫做覆蓋索引,這樣就避免 了回表。 我們先來建立一個聯合索引:
-- 建立聯合索引 
ALTER TABLE user_innodb DROP INDEX comixd_name_phone;
ALTER TABLE user_innodb add INDEX `comixd_name_phone` (`name`,`phone`);
這三個查詢語句都用到了覆蓋索引:
EXPLAIN SELECT name,phone FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666'; 
EXPLAIN SELECT name FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666'; 
EXPLAIN SELECT phone FROM user_innodb WHERE name= '青山' AND phone = ' 13666666666';

  Extra 裡面值為“Using index”代表使用了覆蓋索引。

  select * ,用不到覆蓋索引。

  如果改成只用 where phone = 查詢呢?動手試試?   很明顯,因為覆蓋索引減少了 IO 次數,減少了資料的訪問量,可以大大地提升查詢效率。

4.4. 索引條件下推(ICP)

  https://dev.mysql.com/doc/refman/5.7/en/index-condition-pushdown-optimization.html   再來看這麼一張表,在 last_name 和 first_name 上面建立聯合索引。
drop table employees; 
CREATE TABLE `employees` 
( `emp_no` int(11) NOT NULL, 
`birth_date` date NULL, 
`first_name` varchar(14) NOT NULL,
`last_name` varchar(16) NOT NULL, `
gender` enum('M','F') NOT NULL, 
`hire_date` date NULL, 
PRIMARY KEY (`emp_no`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1;

alter table employees add index idx_lastname_firstname(last_name,first_name);

INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (1, NULL, '698', 'liu', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (2, NULL, 'd99', 'zheng', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (3, NULL, 'e08', 'huang', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (4, NULL, '59d', 'lu', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (5, NULL, '0dc', 'yu', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (6, NULL, '989', 'wang', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (7, NULL, 'e38', 'wang', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (8, NULL, '0zi', 'wang', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (9, NULL, 'dc9', 'xie', 'F', NULL); INSERT INTO `employees` (`emp_no`, `birth_date`, `first_name`, `last_name`, `gender`, `hire_date`) VALUES (10, NULL, '5ba', 'zhou', 'F', NULL);
關閉 ICP: set optimizer_switch='index_condition_pushdown=off'; 檢視引數: show variables like 'optimizer_switch'; 現在我們要查詢所有姓 wang,並且名字最後一個字是 zi 的員工,比如王胖子,王 瘦子。查詢的 SQL:
select * from employees where last_name='wang' and first_name LIKE '%zi' ;
這條 SQL 有兩種執行方式: 1、根據聯合索引查出所有姓 wang 的二級索引資料,然後回表,到主鍵索引上查詢全部符合條件的資料(3 條資料)。然後返回給 Server 層,在 Server 層過濾出名字以 zi 結尾的員工。 2、根據聯合索引查出所有姓 wang 的二級索引資料(3 個索引),然後從二級索引中篩選出 first_name 以 zi 結尾的索引(1 個索引),然後再回表,到主鍵索引上查詢全 部符合條件的資料(1 條資料),返回給 Server 層。   很明顯,第二種方式到主鍵索引上查詢的資料更少。   注意,索引的比較是在儲存引擎進行的,資料記錄的比較,是在 Server 層進行的。而當 first_name 的條件不能用於索引過濾時,Server 層不會把 first_name 的條件傳遞 給儲存引擎,所以讀取了兩條沒有必要的記錄。這時候,如果滿足 last_name='wang'的記錄有 100000 條,就會有 99999 條沒有必要讀取的記錄。   執行以下 SQL,Using where:
explain select * from employees where last_name='wang' and first_name LIKE '%zi' ;

  Using Where 代表從儲存引擎取回的資料不全部滿足條件,需要在 Server 層過濾。先用 last_name 條件進行索引範圍掃描,讀取資料表記錄,然後進行比較,檢查是 否符合 first_name LIKE '%zi' 的條件。此時 3 條中只有 1 條符合條件。 開啟 ICP: set optimizer_switch='index_condition_pushdown=on'; 此時的執行計劃,Using index condition:

把 first_name LIKE '%zi'下推給儲存引擎後,只會從資料表讀取所需的 1 條記錄。

  索引條件下推(Index Condition Pushdown),5.6 以後完善的功能。只適用於二級索引。ICP 的目標是減少訪問表的完整行的讀數量從而減少 I/O 操作。