目錄:
-
1、索引
-
1.1、索引圖解
-
1.2、索引類型
-
-
2、索引存儲模型推演
-
2.1. 二分查找
-
2.2. 二叉查找樹(BST Binary Search Tree)
-
2.3. 平衡二叉樹(AVL Tree)(左旋、右旋)
-
2.3.1.InnoDB 邏輯存儲結構
-
a、表空間 Table Space
-
b、段 Segment
-
c、簇 Extent
-
d、頁 Page
-
e、行 Row
-
-
2.3.2.AVL 樹用于存儲索引數(shù)據(jù)
-
-
2.4. 多路平衡查找樹(B Tree)(分裂、合并)
-
2.5. B+樹(加強版多路平衡查找樹)
-
2.6. 為什么不用紅黑樹?
-
-
3、索引使用原則
-
3.1. 列的離散(sàn)度
-
3.2. 聯(lián)合索引最左匹配
-
3.2.1、什么時候用到聯(lián)合索引
-
-
3.3、覆蓋索引
-
3.4. 索引條件下推(ICP)
-
-
4、索引的創(chuàng)建與使用
-
4.1、索引的創(chuàng)建
-
4.2、什么時候用不到索引?
-
-
1、索引
-
1.1、索引圖解
-
索引定義:數(shù)據(jù)庫索引,是數(shù)據(jù)庫管理系統(tǒng)(DBMS)中一個排序的數(shù)據(jù)結構,以協(xié)助快速查詢、 更新數(shù)據(jù)庫表中數(shù)據(jù)。
數(shù)據(jù)是以文件的形式存放在磁盤上的,每一行數(shù)據(jù)都有它的磁盤地址。如果沒有索引,要從 500 萬行數(shù)據(jù)里面檢索一條數(shù)據(jù),只能依次遍歷這張表的全部數(shù)據(jù), 直到找到這條數(shù)據(jù)。
但是有了索引之后,只需要在索引里面去檢索這條數(shù)據(jù)就行了,因為它是一種特殊的專門用來快速檢索的數(shù)據(jù)結構,找到數(shù)據(jù)存放的磁盤地址以后,就可以拿到數(shù)據(jù)了。
-
1.2、索引類型
- 普通(Normal):也叫非唯一索引,是最普通的索引,沒有任何的限制。
- 唯一(Unique):唯一索引要求鍵值不能重復。
另外需要注意的是,主鍵索引是一種特殊的唯一索引,它還多了一個限制條件,要求鍵值不能為空。
主鍵索引用 primay key 創(chuàng)建。 - 全文(Fulltext):針對比較大的數(shù)據(jù),比如存放的是消息內(nèi)容,有幾 KB 的數(shù) 據(jù)的這種情況,如果要解決 like 查詢效率低的問題,可以創(chuàng)建全文索引。只有文本類型的字段才可以創(chuàng)建全文索引,比如 char、varchar、text。
-
2、索引存儲模型推演
-
2.1. 二分查找
-
有序數(shù)組的等值查詢和比較查詢效率非常高,但是更新數(shù)據(jù)的時候會出現(xiàn)一個問題, 可能要挪動大量的數(shù)據(jù)(改變 index),所以只適合存儲靜態(tài)的數(shù)據(jù)。
為了支持頻繁的修改,比如插入數(shù)據(jù),我們需要采用鏈表。鏈表的話,如果是單鏈表,它的查找效率還是不夠高。
所以,有沒有可以使用二分查找的鏈表呢?
為了解決這個問題,BST(Binary Search Tree)也就是我們所說的二叉查找樹誕生 了。
-
2.2. 二叉查找樹(BST Binary Search Tree)
特點:左子樹所有的節(jié)點都小于父節(jié)點,右子樹所有的節(jié)點都大于父節(jié)點。投影到平面以 后,就是一個有序的線性表。
優(yōu)點:二叉查找樹既能夠實現(xiàn)快速查找,又能夠實現(xiàn)快速插入。
但是二叉查找樹有一個問題:就是它的查找耗時是和這棵樹的深度相關的,在最壞的情況下時間復雜度會退化成
O(n)
。還是剛才的這一批數(shù)字,如果插入的數(shù)據(jù)剛好是有序的,2、6、11、13、17、 22。
這個時候二叉查找樹變成了什么樣了呢?
它會變成鏈表(這種樹叫做“斜樹”),這種情況下不能達到加快檢索速度的目的,和順序查找效率是沒有區(qū)別的。
因為左右子樹深度差太大,這棵樹的左子樹根本沒有節(jié)點——也就是它不夠平衡。有沒有左右子樹深度相差不是那么大,更加平衡的樹呢?
平衡二叉樹,叫做 Balanced binary search trees,或者 AVL 樹(AVL 是發(fā)明這個數(shù)據(jù)結構的人的名字)。
-
2.3. 平衡二叉樹(AVL Tree)(左旋、右旋)
-
平衡二叉樹的定義:左右子樹深度差絕對值不能超過 1。
在平衡二叉樹中,一個節(jié)點的大小是一個固定的單位,作為索引應該存儲什么內(nèi)容?
它存儲三塊的內(nèi)容:
第一個是索引的鍵值。比如在 id 上面創(chuàng)建了一個索引,在用 where id =1 的條件查詢的時候就會找到索引里面 id 的這個鍵值。
第二個是數(shù)據(jù)的磁盤地址,因為索引的作用就是去查找數(shù)據(jù)的存放的地址。
第三個,因為是二叉樹,必須還要有左子節(jié)點和右子節(jié)點的引用,這樣才能 找到下一個節(jié)點。
平衡二叉樹結構
-
平衡二叉樹的定義:左右子樹深度差絕對值不能超過 1。
【思考??】如果是這樣存儲數(shù)據(jù)結構,會有什么問題?
在分析用 AVL 樹存儲索引數(shù)據(jù)之前,先來學習一下 InnoDB 的邏輯存儲結構。
-
2.3.1.InnoDB 邏輯存儲結構
-
a、表空間(Table Space)
表空間可以看做是 InnoDB 存儲引擎邏輯結構的最高層,所有的數(shù)據(jù)都存放在表空間中。分為:系統(tǒng)表空間、獨占表空間、通用表空間、 臨時表空間、Undo 表空間。
-
b、段(Segment)
表空間是由各個段組成的,常見的段有數(shù)據(jù)段、索引段、回滾段等,段是一個邏輯的概念。一個 ibd 文件(獨立表空間文件)里面會由很多個段組成。
創(chuàng)建索引會創(chuàng)建兩個段:索引段(leaf node segment),數(shù)據(jù)段(non-leaf node segment)。
索引段管理非葉子節(jié)點的數(shù)據(jù)。數(shù)據(jù)段管理葉子節(jié)點的數(shù)據(jù)。 也就是說,一個表的段數(shù),就是索引的個數(shù)乘以 2。
-
c、簇 Extent
??????一個段(Segment)又由很多的簇(也可以叫區(qū))組成,每個區(qū)的大小是 1MB(64 個連續(xù)的頁)。
每一個段至少會有一個簇,一個段所管理的空間大小是無限的,可以一直擴展下去, 但是擴展的最小單位就是簇。
-
d、頁 Page
??????高效管理物理空間,對簇進一步細分,就得到了頁。簇是由連續(xù)的頁(Page) 組成的空間,一個簇中有 64 個連續(xù)的頁。 (1MB/16KB=64)。這些頁面在物理上和邏輯上都是連續(xù)的(I/O 實現(xiàn)順序讀)。
InnoDB 也有頁的概念(也可以稱為塊),每個頁默認 16KB。 頁是 InnoDB 存儲引擎磁盤管理的最小單位,通過 innodb_page_size
設置。
??????一個表空間最多擁有 2^32 個頁,默認情況下一個頁的大小為 16KB,也就是說一個表空間最多存儲 64TB 的數(shù)據(jù)。
??????同樣,文件系統(tǒng)中也有頁的概念。 操作系統(tǒng)和內(nèi)存打交道,最小的單位是頁 Page,文件系統(tǒng)的內(nèi)存頁通常是 4K。
SHOW VARIABLES LIKE 'innodb_page_size';
eg:舉例:一個頁放 3 行數(shù)據(jù)。
往表中插入數(shù)據(jù)時,如果一個頁面已經(jīng)寫完,產(chǎn)生一個新的葉頁面。如果一個簇的所有的頁面都被用完,會從當前頁面所在段新分配一個簇。
如果數(shù)據(jù)不是連續(xù)的,往已經(jīng)寫滿的頁中插入數(shù)據(jù),會導致葉頁面分裂:
-
e、行(Row)
?????? InnoDB 存儲引擎是面向行的(row-oriented),也就是說數(shù)據(jù)是按行進行存放。
?????? Antelope[??nt?l??p](羚羊)是 InnoDB 內(nèi)置的文件格式,有兩種行格式:
????????????REDUNDANT[r??d?nd?nt] Row Format
????????????COMPACT Row Format(5.6 默認)
?????? Barracuda[?b?r??kju?d?](梭子魚)是 InnoDB Plugin 支持的文件格式,新增了兩種行格式:
????????????DYNAMIC Row Format(5.7 默認)
????????????COMPRESSED Row Format
文件格式 | 行格式 | 描述 |
---|---|---|
Antelope (Innodb-base) | ROW_FORMAT=COMPACT ROW_FORMAT=REDUNDANT |
Compact 和 redumdant 的區(qū)別在就是在于首部的存內(nèi)容區(qū)別。 compact 存儲格式首部為一個非 NULL 的變長字段長度列表。 redundant 存儲格式首部是一個字段長度偏移列表(每個字段占用的字節(jié)長度及其相應的位移)。 在 Antelope 中對于變長字段,低于 768 字節(jié)的,不會進行 overflow page 存儲,某些情況下會減少結果 集 IO。 |
Barracuda (innodb-plugin) | ROW_FORMAT=DYNAMIC ROW_FORMAT=COMPRESSED |
這兩者主要是功能上的區(qū)別,另外在行里的變長字段和 Antelope 的區(qū)別是只存 20 個字節(jié), 其它的 overflow page 存儲。 另外這兩都需要開啟 innodb_file_per_table=1。 |
innodb_file_format 在配置文件中指定;
row_format 則在創(chuàng)建數(shù)據(jù)表時指定;
SET GLOBAL innodb_file_format=Barracuda;
在創(chuàng)建表的時候可以指定行格式;
CREATE TABLE tf0 (id INT PRIMARY KEY)
ROW_FORMAT=COMPRESSED
KEY_BLOCK_SIZE=8;
-
2.3.2.AVL 樹用于存儲索引數(shù)據(jù)
?????? 當用樹的結構來存儲索引,訪問一個節(jié)點就要跟磁盤之間發(fā)生一次 IO。 InnoDB 操作磁盤的最小的單位是一頁(或者叫一個磁盤塊),大小是 16K(16384 字節(jié))。
那么,一個樹的節(jié)點就是 16K 的大小。
如果一個節(jié)點只存一個鍵值+數(shù)據(jù)+引用,例如整形的字段,可能只用了十幾個或者幾十個字節(jié),它遠遠達不到 16K 的容量,所以訪問一個樹節(jié)點,進行一次 IO 的時候, 浪費了大量的空間。
所以如果每個節(jié)點存儲的數(shù)據(jù)太少,從索引中找到我們需要的數(shù)據(jù),就要訪問更多的節(jié)點,意味著跟磁盤交互次數(shù)就會過多。
如果是機械硬盤時代,每次從磁盤讀取數(shù)據(jù)需要 10ms 左右的尋址時間,交互次數(shù)越多,消耗的時間就越多。
?????? 比如上面這張圖,一張表里面有 6 條數(shù)據(jù),當我們查詢 id=37 的時候,要查詢兩個子節(jié)點,就需要跟磁盤交互 3 次,如果有幾百萬的數(shù)據(jù)呢?這個時間更加難以估計。
針對上面的解決方案是什么呢?
第一個就是讓每個節(jié)點存儲更多的數(shù)據(jù)。
第二個,節(jié)點上的關鍵字的數(shù)量越多,指針數(shù)也越多,也就是意味著可以有更多的分叉(把它叫做“路數(shù)”)。
因為分叉數(shù)越多,樹的深度就會減少(根節(jié)點是 0)。 這樣的樹是不是從原來的高瘦高瘦的樣子,變成了矮胖矮胖的樣子?
這個時候,我們的樹就不再是二叉了,而是多叉 or 多路。
-
2.4. 多路平衡查找樹(B Tree)(分裂、合并)
?????? Balanced Tree這個就是我們的多路平衡查找樹,叫做 B Tree(B 代表平衡)。
跟 AVL 樹一樣,B 樹在枝節(jié)點和葉子節(jié)點存儲鍵值、數(shù)據(jù)地址、節(jié)點引用。 它有一個特點:分叉數(shù)(路數(shù))永遠比關鍵字數(shù)多 1。比如我們畫的這棵樹,每個節(jié)
點存儲兩個關鍵字,那么就會有三個指針指向三個子節(jié)點。
B Tree 的查找規(guī)則是什么樣的呢? 比如我們要在這張表里面查找 15。
因為 15 小于 17,走左邊。
因為 15 大于 12,走右邊。
在磁盤塊 7 里面就找到了 15,只用了
3 次 IO
。
如果刪除節(jié)點,會有相反的合并的操作。 注意這里是分裂和合并,跟 AVL 樹的左旋和右旋是不一樣的。 我們繼續(xù)插入 4 和 5,B Tree 又會出現(xiàn)分裂和合并的操作。
從這個里面我們也能看到,在更新索引的時候會有大量的索引的結構的調(diào)整,所以 解釋了為什么我們不要在頻繁更新的列上建索引,或者為什么不要更新主鍵。
節(jié)點的分裂和合并,其實就是 InnoDB 頁的分裂和合并。
-
2.5. B+樹(加強版多路平衡查找樹)
B Tree 的效率已經(jīng)很高了,為什么 MySQL 還要對 B Tree 進行改良,最終使用了 B+Tree 呢?
總體上來說,這個 B 樹的改良版本解決的問題比 B Tree 更全面。 我們來看一下 InnoDB 里面的 B+樹的存儲結構:
MySQL 中的 B+Tree 有幾個特點:
- 1、它的關鍵字的數(shù)量是跟路數(shù)相等的;
- 2、B+Tree 的根節(jié)點和枝節(jié)點中都不會存儲數(shù)據(jù),只有葉子節(jié)點才存儲數(shù)據(jù)。搜索
到關鍵字不會直接返回,會到最后一層的葉子節(jié)點。比如我們搜索 id=28,雖然在第一 層直接命中了,但是全部的數(shù)據(jù)在葉子節(jié)點上面,所以我還要繼續(xù)往下搜索,一直到葉 子節(jié)點。
舉個例子:假設一條記錄是 1K,一個葉子節(jié)點(一頁)可以存儲 16 條記錄。非葉 子節(jié)點可以存儲多少個指針?
假設索引字段是 bigint 類型,長度為 8 字節(jié)。指針大小在 InnoDB 源碼中設置為 6 字節(jié),這樣一共 14 字節(jié)。非葉子節(jié)點(一頁)可以存儲 16384/14=1170 個這樣的 單元(鍵值+指針),代表有 1170 個指針。
樹深度為 2 的時候,有 1170^2 個葉子節(jié)點,可以存儲的數(shù)據(jù)為 1170117016=21902400。
在查找數(shù)據(jù)時一次頁的查找代表一次 IO,也就是說,一張 2000 萬左右的表,查詢 數(shù)據(jù)最多需要訪問 3 次磁盤。
所以在 InnoDB 中 B+ 樹深度一般為 1-3 層,它就能滿足千萬級的數(shù)據(jù)存儲。
- 3、B+Tree 的每個葉子節(jié)點增加了一個指向相鄰葉子節(jié)點的指針,它的最后一個數(shù) 據(jù)會指向下一個葉子節(jié)點的第一個數(shù)據(jù),形成了一個有序鏈表的結構。
- 4、它是根據(jù)左閉右開的區(qū)間 [ )來檢索數(shù)據(jù)。我們來看一下 B+Tree 的數(shù)據(jù)搜尋過程:
- 1)、比如我們要查找 28,在根節(jié)點就找到了鍵值,但是因為它不是頁子節(jié)點,所以 會繼續(xù)往下搜尋,28 是[28,66)的左閉右開的區(qū)間的臨界值,所以會走中間的子節(jié)點,然 后繼續(xù)搜索,它又是[28,34)的左閉右開的區(qū)間的臨界值,所以會走左邊的子節(jié)點,最后 在葉子節(jié)點上找到了需要的數(shù)據(jù)。
- 2)、第二個,如果是范圍查詢,比如要查詢從 22 到 60 的數(shù)據(jù),當找到 22 之后,只 需要順著節(jié)點和指針順序遍歷就可以一次性訪問到所有的數(shù)據(jù)節(jié)點,這樣就極大地提高了區(qū)間查詢效率(不需要返回上層父節(jié)點重復遍歷查找)。
總結一下,InnoDB 中的 B+Tree 的特點:
?????? 1)、它是 B Tree 的變種,B Tree 能解決的問題,它都能解決。B Tree 解決的兩大問題 是什么?(每個節(jié)點存儲更多關鍵字;路數(shù)更多)
?????? 2)、掃庫、掃表能力更強(如果我們要對表進行全表掃描,只需要遍歷葉子節(jié)點就可以 了,不需要遍歷整棵 B+Tree 拿到所有的數(shù)據(jù))
?????? 3)、B+Tree 的磁盤讀寫能力相對于 B Tree 來說更強(根節(jié)點和枝節(jié)點不保存數(shù)據(jù)區(qū), 所以一個節(jié)點可以保存更多的關鍵字,一次磁盤加載的關鍵字更多)
?????? 4)、排序能力更強(因為葉子節(jié)點上有下一個數(shù)據(jù)區(qū)的指針,數(shù)據(jù)形成了鏈表) 5)效率更加穩(wěn)定(B+Tree 永遠是在葉子節(jié)點拿到數(shù)據(jù),所以 IO 次數(shù)是穩(wěn)定的)
【思考:??為什么不用紅黑樹?】
紅黑樹也是 BST 樹,但是不是嚴格平衡的。
必須滿足 5 個約束:
1、節(jié)點分為紅色或者黑色。
2、根節(jié)點必須是黑色的。
3、葉子節(jié)點都是黑色的 NULL 節(jié)點。
4、紅色節(jié)點的兩個子節(jié)點都是黑色(不允許兩個相鄰的紅色節(jié)點)。
5、從任意節(jié)點出發(fā),到其每個葉子節(jié)點的路徑中包含相同數(shù)量的黑色節(jié)點。 插入:60、56、68、45、64、58、72、43、49
基于以上規(guī)則,可以推導出:
從根節(jié)點到葉子節(jié)點的最長路徑(紅黑相間的路徑)不大于最短路徑(全部是黑色 節(jié)點)的 2 倍。 為什么不用紅黑樹?
1、只有兩路;
2、不夠平衡。 紅黑樹一般只放在內(nèi)存里面用。例如 Java 的 TreeMap。
3、索引使用原則
??????容易有這樣一個誤區(qū),在經(jīng)常使用的查詢條件上都建立索引,索引越多越好, 那到底是不是這樣呢?
-
3.1、列的離散(sàn)度
?????? 列的離散度的公式:count(distinct(column_name)) : count(1)
,列的全部不同值和所有數(shù)據(jù)行的比例。
數(shù)據(jù)行數(shù)相同的情況下,分子越大,列的離散度就越高。
id | name | gender | iphone |
---|---|---|---|
1 | kobe | 1 | 18612548765 |
2 | james | 1 | 18817064436 |
3 | Jenner | 0 | 18755326679 |
4 | me | 1 | 188------- |
簡單來說,如果列的重復值越多,離散度就越低,重復值越少,離散度就越高。
了解了離散度的概念之后,思考一個問題,在 name 上面建立索引和 在 gender 上面建立索引有什么區(qū)別?
當我們用在 gender 上建立的索引去檢索數(shù)據(jù)的時候,由于重復值太多,需要掃描的 行數(shù)就更多。
例如,我們現(xiàn)在在 gender 列上面創(chuàng)建一個索引,然后看一下執(zhí)行計劃。
ALTER TABLE user_innodb ADD INDEX idx_user_gender (gender);
EXPLAIN SELECT * FROM `user_innodb` WHERE gender = 0;
show indexes from user_innodb;
而 name 的離散度更高,比如“Jenner”的這名字,只需要掃描一行。
ALTER TABLE user_innodb ADD INDEX idx_user_name (name);
EXPLAIN SELECT * FROM `user_innodb` WHERE name = '青山';
查看表上的索引,Cardinality [kɑ:d?'n?l?t?] 代表基數(shù),代表預估的不重復的值的數(shù)量。
索引的基數(shù)與表總行數(shù)越接近,列的離散度就越高。
結論:如果在 B+Tree 里面的重復值太多,MySQL 的優(yōu)化器發(fā)現(xiàn)走索引跟使用全表掃描差,不了多少的時候,就算建了索引,也不一定會走索引。
這個給我們的啟發(fā)是什么?建立索引,要使用離散度(選擇度)更高的字段。
3.2、聯(lián)合索引最左匹配
?????? 前面都是針對單列創(chuàng)建的索引,對于多條件查詢的時候,也會建立聯(lián)合索引。
單列索引可以看成是特殊的聯(lián)合索引。 比如我們在 user 表上面,給 name 和 phone 建立了一個聯(lián)合索引。
ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);
聯(lián)合索引在 B+Tree 中是復合的數(shù)據(jù)結構,它是按照從左到右的順序來建立搜索樹(name 在左邊,phone 在右邊)。
從這張圖可以看出來,name 是有序的,phone 是無序的。
當 name 相等的時候, phone 才是有序的。 這個時候我們使用 where name= 'Janner' and phone = '136xx '去查詢數(shù)據(jù)的時候, B+Tree 會優(yōu)先比較 name 來確定下一步應該搜索的方向,往左還是往右。如果 name 相同的時候再比較 phone。但是如果查詢條件沒有 name,就不知道第一步應該查哪個節(jié)點,因為建立搜索樹的時候 name 是第一個比較因子,所以用不到索引。
【思考:??什么時候用到聯(lián)合索引?】
- eg:我們在建立聯(lián)合索引的時候,一定要把最常用的列放在最左邊。 比如下面的三條語句,能用到聯(lián)合索引嗎?
- 1)使用兩個字段,可以用到聯(lián)合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= 'Janner' AND phone = '15204661800';
- 2)使用左邊的 name 字段,可以用到聯(lián)合索引:
EXPLAIN SELECT * FROM user_innodb WHERE name= 'Janner';
- 3)使用右邊的 phone 字段,
無法使用索引,全表掃描:
EXPLAIN SELECT * FROM user_innodb WHERE phone = '15204661800';
3.3、覆蓋索引
?????? 回表: 非主鍵索引,先通過索引找到主鍵索引的鍵值,再通過主鍵值查出索引里面沒有的數(shù)據(jù),它比基于主鍵索引的查詢多掃描了一棵索引樹,這個過程就叫回表。
?????? 在輔助索引里面,不管是單列索引還是聯(lián)合索引,如果 select 的數(shù)據(jù)列只用從索引 中就能夠取得,不必從數(shù)據(jù)區(qū)中讀取,這時候使用的索引就叫做覆蓋索引,這樣就避免了回表。
這三個查詢語句都用到了覆蓋索引:
-- 創(chuàng)建聯(lián)合索引
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= 'Janner' AND phone = ' 13666666666';
EXPLAIN SELECT name FROM user_innodb WHERE name= 'Janner' AND phone = ' 13666666666';
EXPLAIN SELECT phone FROM user_innodb WHERE name= 'Janner' AND phone = ' 13666666666';
Extra 里面值為“Using index”代表使用了覆蓋索引。
select * ,用不到覆蓋索引。
結論:很明顯,因為覆蓋索引減少了 IO 次數(shù),減少了數(shù)據(jù)的訪問量,可以大大地提升查詢效率。
3.4、索引下推(ICP)
?????? 有這么一張表,在 last_name 和 first_name 上面創(chuàng)建聯(lián)合索引。
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;
-- 創(chuàng)建聯(lián)合索引
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';
現(xiàn)在我們要查詢所有姓 wang,并且名字最后一個字是 zi 的員工,比如王胖子,王瘦子。查詢的 SQL:
select * from employees where last_name='wang' and first_name LIKE '%zi' ;
這條 SQL 有兩種執(zhí)行方式:
?????? 1、根據(jù)聯(lián)合索引查出所有姓 wang 的二級索引數(shù)據(jù),然后回表,到主鍵索引上查詢?nèi)糠蠗l件的數(shù)據(jù)(3 條數(shù)據(jù))。然后返回給 Server 層,在 Server 層過濾出名字以 zi 結尾的員工。
?????? 2、根據(jù)聯(lián)合索引查出所有姓 wang 的二級索引數(shù)據(jù)(3 個索引),然后從二級索引中篩選出 first_name 以 zi 結尾的索引(1 個索引),然后再回表,到主鍵索引上查詢?nèi)?部符合條件的數(shù)據(jù)(1 條數(shù)據(jù)),返回給 Server 層。
很明顯,第二種方式到主鍵索引上查詢的數(shù)據(jù)更少。
注意,索引的比較是在存儲引擎進行的,數(shù)據(jù)記錄的比較,是在 Server 層進行的。
而當 first_name 的條件不能用于索引過濾時,Server 層不會把 first_name 的條件傳遞給存儲引擎,所以讀取了兩條沒有必要的記錄。
這時候,如果滿足 last_name='wang'的記錄有 100000 條,就會有 99999 條沒有 必要讀取的記錄。
執(zhí)行以下 SQL,Using where:
explain select * from employees where last_name='wang' and first_name LIKE '%zi' ;
?????? Using Where 代表從存儲引擎取回的數(shù)據(jù)不全部滿足條件,需要在 Server 層過濾。
先用 last_name 條件進行索引范圍掃描,讀取數(shù)據(jù)表記錄,此時是3條,然后進行比較,檢查是否符合 first_name LIKE '%zi' 的條件。此時 3 條中只有 1 條符合條件。
開啟 ICP:
set optimizer_switch='index_condition_pushdown=on';
此時的執(zhí)行計劃,Using index condition:
?????? 把 first_name LIKE '%zi'下推給存儲引擎后,只會從數(shù)據(jù)表讀取所需的 1 條記錄。
?????? 索引條件下推(Index Condition Pushdown),5.6 以后完善的功能。
只適用于二級索引。ICP 的目標是減少訪問表的完整行的讀數(shù)量從而減少 I/O 操作。
5、面試常見問題
-
5.1、為什么不建議使用ForeignKey 外鍵?
- 1、MySQL、PostgreSQL 等關系型數(shù)據(jù)庫很難水平擴容,但是無狀態(tài)的服務往往都可以很容易地擴容。由于外鍵等特性需要數(shù)據(jù)庫執(zhí)行額外的工作,這些操作會占用數(shù)據(jù)庫的計算資源,所以可以將大部分的需求都遷移到無狀態(tài)的服務中完成以降低數(shù)據(jù)庫的工作負載?!疽驗閼盟綌U展代價更小】
- 2、不同類型的外鍵會給數(shù)據(jù)庫帶來不同的額外開銷。
- a、使用 RESTRICT 會在更新或者刪除記錄時對外鍵對應的記錄是否存在進行一致性檢查;
- b、使用 CASCADE 會在更新或者刪除記錄時觸發(fā)級聯(lián)更新或者刪除操作。
參考資料:
《數(shù)據(jù)庫查詢優(yōu)化器的藝術-原理解析與SQL性能優(yōu)化》
《MySQL高性能書籍第3版(中文)》
《MySQL技術內(nèi)幕-InnoDB存儲引擎第2版》