標籤: 公衆號文章bash
爲了故事的順利發展,咱們須要先創建一個表:spa
CREATE TABLE single_table (
id INT NOT NULL AUTO_INCREMENT,
key1 VARCHAR(100),
key2 INT,
key3 VARCHAR(100),
key_part1 VARCHAR(100),
key_part2 VARCHAR(100),
key_part3 VARCHAR(100),
common_field VARCHAR(100),
PRIMARY KEY (id),
KEY idx_key1 (key1),
UNIQUE KEY uk_key2 (key2),
KEY idx_key3 (key3),
KEY idx_key_part(key_part1, key_part2, key_part3)
) Engine=InnoDB CHARSET=utf8;
複製代碼
咱們爲這個single_table表創建了1個聚簇索引和4個二級索引,分別是:3d
爲id列創建的聚簇索引。code
爲key1列創建的idx_key1二級索引。cdn
爲key2列創建的uk_key2二級索引,並且該索引是惟一二級索引。blog
爲key3列創建的idx_key3二級索引。排序
爲key_part一、key_part二、key_part3列創建的idx_key_part二級索引,這也是一個聯合索引。索引
而後咱們須要爲這個表插入10000行記錄,除id列外其他的列都插入隨機值就行了,具體的插入語句我就不寫了,本身寫個程序插入吧(id列是自增主鍵列,不須要咱們手動插入)。v8
咱們畫一下single_table表的聚簇索引的示意圖: get
經過聚簇索引對應的B+樹,咱們能夠很容易的定位到主鍵值等於某個值的聚簇索引記錄,比方說咱們想經過這個B+樹定位到id值爲1438的記錄,那麼示意圖就以下所示:
若是咱們想查找key1值等於某個值的二級索引記錄,那麼能夠經過idx_key1對應的B+樹,很容易的定位到第一條key1列的值等於某個值的二級索引記錄,而後沿着記錄所在單向鏈表向後掃描便可。比方說咱們想經過這個B+樹定位到第一條key1值爲'abc'的記錄,那麼示意圖就以下所示:
對於某個查詢來講,最粗暴的執行方案就是掃描表中的全部記錄,針對每一條記錄都判斷一下該記錄是否符合搜索條件,若是符合的話就將其發送到客戶端,不然就跳過該記錄。這種執行方案也被稱爲全表掃描。對於使用InnoDB存儲引擎的表來講,全表掃描意味着從聚簇索引第一個葉子節點的第一條記錄開始,沿着記錄所在的單向鏈表向後掃描,直到最後一個葉子節點的最後一條記錄爲止。雖然全表掃描是一種很笨的執行方案,但倒是一種萬能的執行方案,全部的查詢均可以使用這種方案來執行。
咱們以前介紹了利用B+樹查找索引列值等於某個值的記錄,這樣能夠明顯減小須要掃描的記錄數量。其實因爲B+樹的葉子節點中的記錄是按照索引列值由小到大的順序排序的,因此咱們只掃描在某個區間或者某些區間中的記錄也能夠明顯減小須要掃描的記錄數量。比方說對於下邊這個查詢語句來講:
SELECT * FROM single_table
WHERE id >= 2 AND id <= 100;
複製代碼
這個語句實際上是想查找全部id值在[2, 100]這個區間中的聚簇索引記錄,那麼咱們就能夠經過聚簇索引對應的B+樹快速地定位到id值爲2的那條聚簇索引記錄,而後沿着記錄所在的單向鏈表向後掃描,直到某條聚簇索引記錄的id值不在[2, 100]這個區間中爲止(其實也就是直到id值不符合id<=100這個條件爲止)。與掃描所有的聚簇索引記錄相比,掃描id值在[2, 100]這個區間中的記錄已經很大程度的減小了須要掃描的記錄數量,因此提高了查詢效率。爲簡便起見,咱們把這個例子中須要掃描的記錄的id值所在的區間稱爲掃描區間,把造成這個掃描區間的查詢條件,也就是id >= 2 AND id <= 100稱爲造成這個掃描區間的邊界條件。
小貼士: 其實對於全表掃描來講,至關於咱們須要掃描id值在(-∞, +∞)這個區間中的記錄,也就是說全表掃描對應的掃描區間就是(-∞, +∞)。
對於下邊這個查詢:
SELECT * FROM single_table
WHERE key2 IN (1438, 6328) OR
(key2 >= 38 AND key2 <= 79);
複製代碼
咱們固然能夠直接使用全表掃描的方式執行該查詢,可是觀察到該查詢的搜索條件涉及到了key2列,而咱們又正好爲key2列創建了uk_key2索引,若是咱們使用uk_key2索引執行這個查詢的話,那麼至關於從下邊的三個掃描區間中獲取二級索引記錄:
這些掃描區間對應到數軸上的示意圖就以下圖所示:
小貼士: 其實咱們不只僅可使用uk_key2執行上述查詢,使用idx_key一、idx_key三、idx_keypart均可以執行上述查詢。以idx_key1爲例,很顯然咱們沒法經過搜索條件造成合適的掃描區間來減小須要掃描的idx_key二級索引記錄數量,只能掃描idx_key1的所有二級索引記錄。針對獲取到的每一條二級索引記錄,都須要執行回表操做來獲取完整的用戶記錄。咱們也能夠說此時使用idx_key1執行查詢時對應的掃描區間就是(-∞, +∞)。 這樣子雖然行得通,但咱們圖啥呢?最粗暴的全表掃描方式已經要掃描所有的聚簇索引記錄了,你這裏除了要訪問所有的聚簇索引記錄,還要掃描所有的idx_key1二級索引記錄,這不是費力不討好麼。在這個過程當中沒有減小須要掃描的記錄數量,反而效率比全表掃描更差,因此若是咱們想使用某個索引來執行查詢,可是又沒法經過搜索條件造成合適的掃描區間來減小須要掃描的記錄數量時,那麼咱們是不考慮使用這個索引執行查詢的。
並非全部的搜索條件均可以成爲邊界條件,比方說下邊這個查詢:
SELECT * FROM single_table
WHERE key1 < 'a' AND
key3 > 'z' AND
common_field = 'abc';
複製代碼
那麼:
若是咱們使用idx_key1執行查詢的話,那麼相應的掃描區間就是(-∞, 'a'),造成該掃描區間的邊界條件就是key1 < 'a',而key3 > 'z' AND common_field = 'abc'就是普通的搜索條件,這些普通的搜索條件須要在獲取到idx_key1的二級索引記錄後,再執行回表操做,獲取到完整的用戶記錄後才能去判斷它們是否成立。
若是咱們使用idx_key3執行查詢的話,那麼相應的掃描區間就是('z', +∞),造成該掃描區間的邊界條件就是key3 > 'z',而key1 < 'a' AND common_field = 'abc'就是普通的搜索條件,這些普通的搜索條件須要在獲取到idx_key3的二級索引記錄後,再執行回表操做,獲取到完整的用戶記錄後才能去判斷它們是否成立。
聯合索引的索引列包含多個列,B+樹每一層頁面以及每一個頁面中的記錄採用的排序規則較爲複雜,以single_table表的idx_key_part聯合索引爲例,它採用的排序規則以下所示:
咱們畫一下idx_key_part索引的示意圖:
Q1:SELECT * FROM single_table
WHERE key_part1 = 'a';
複製代碼
因爲二級索引記錄是先按照key_part1列的值進行排序的,因此全部符合key_part1 = 'a'條件的記錄確定是相鄰的,咱們能夠定位到第一條符合key_part1 = 'a'條件的記錄,而後沿着記錄所在的單向鏈表向後掃描,直到某條記錄不符合key_part1 = 'a'條件爲止(固然,對於獲取到的每一條二級索引記錄都要執行回表操做,咱們這裏就不展現回表操做了),以下圖所示。
對於下邊這個查詢Q2來講:
Q2:SELECT * FROM single_table
WHERE key_part1 = 'a' AND
key_part2 = 'b';
複製代碼
因爲二級索引記錄是先按照key_part1列的值進行排序的;在key_part1列的值相等的狀況下,再按照key_part2列進行排序。因此符合key_part1 = 'a' AND key_part2 = 'b'條件的二級索引記錄確定是相鄰的,咱們能夠定位到第一條符合key_part1='a' AND key_part2='b'條件的記錄,而後沿着記錄所在的單向鏈表向後掃描,直到某條記錄不符合key_part1='a'條件或者key_part2='b'條件爲止(固然,對於獲取到的每一條二級索引記錄都要執行回表操做,咱們這裏就不展現回表操做了),以下圖所示。
對於下邊這個查詢Q3來講:
Q3:SELECT * FROM single_table
WHERE key_part1 = 'a' AND
key_part2 = 'b' AND
key_part3 = 'c';
複製代碼
因爲二級索引記錄是先按照key_part1列的值進行排序的;在keypart1列的值相等的狀況下,再按照key_part2列進行排序;在key_part1和key_part2列的值都相等的狀況下,再按照key_part3列進行排序。因此符合key_part1 = 'a' AND key_part2 = 'b' AND key_part3 = 'c'條件的二級索引記錄確定是相鄰的,咱們能夠定位到第一條符合key_part1='a' AND key_part2='b' AND key_part3='c'條件的記錄,而後沿着記錄所在的單向鏈表向後掃描,直到某條記錄不符合key_part1='a'條件或者key_part2='b'條件或者key_part3='c'條件爲止(固然,對於獲取到的每一條二級索引記錄都要執行回表操做),咱們就不畫示意圖了。若是咱們使用idx_key_part索引執行查詢Q3時,能夠造成掃描區間[('a', 'b', 'c'), ('a', 'b', 'c')],造成這個掃描區間的條件就是key_part1 = 'a' AND key_part2 = 'b' AND key_part3 = 'c'。
對於下邊這個查詢Q4來講:
Q4:SELECT * FROM single_table
WHERE key_part1 < 'a';
複製代碼
因爲二級索引記錄是先按照key_part1列的值進行排序的,因此全部符合key_part1 < 'a'條件的記錄確定是相鄰的,咱們能夠定位到第一條符合key_part1 < 'a'條件的記錄(其實就是idx_key_part索引第一個葉子節點的第一條記錄),而後沿着記錄所在的單向鏈表向後掃描,直到某條記錄不符合key_part1 < 'a'爲止(固然,對於獲取到的每一條二級索引記錄都要執行回表操做,咱們這裏就不展現回表操做了),以下圖所示。
對於下邊這個查詢Q5來講:
Q5:SELECT * FROM single_table
WHERE key_part1 = 'a' AND
key_part2 > 'a' AND
key_part2 < 'd';
複製代碼
因爲二級索引記錄是先按照key_part1列的值進行排序的;在key_part1列的值相等的狀況下,再按照key_part2列進行排序。也就是說在符合key_part1 = 'a'條件的二級索引記錄中,是按照key_part2列的值進行排序的,那麼此時符合key_part1 = 'a' AND key_part2 > 'a' AND key_part2 < 'd'條件的二級索引記錄確定是相鄰的。咱們能夠定位到第一條符合key_part1='a' AND key_part2 > 'a' AND key_part2 < 'c'條件的記錄,而後沿着記錄所在的單向鏈表向後掃描,直到某條記錄不符合key_part1='a'條件或者key_part2 > 'a'條件或者key_part2 < 'd'條件爲止(固然,對於獲取到的每一條二級索引記錄都要執行回表操做,咱們這裏就不展現回表操做了),以下圖所示。
對於下邊這個查詢Q6來講:
Q6:SELECT * FROM single_table
WHERE key_part2 = 'a';
複製代碼
因爲二級索引記錄不是直接按照key_part2列的值排序的,因此符合key_part2 = 'a'的二級索引記錄可能並不相鄰,也就意味着咱們不能經過這個key_part2 = 'a'搜索條件來減小須要掃描的記錄數量。在這種狀況下,咱們是不會使用idx_key_part索引執行查詢的。
對於下邊這個查詢Q7來講:
Q7:SELECT * FROM single_table
WHERE key_part1 = 'a' AND
key_part3 = 'c';
複製代碼
因爲二級索引記錄是先按照key_part1列的值進行排序的,因此符合key_part1 = 'a'條件的二級索引記錄確定是相鄰的,可是對於符合key_part1 = 'a'條件的二級索引記錄來講,並非直接按照key_part3列進行排序的,也就是說咱們不能根據搜索條件key_part3 = 'c'來進一步減小須要掃描的記錄數量。那麼若是咱們使用idx_key_part索引執行查詢的話,能夠定位到第一條符合key_part1='a'條件的記錄,而後沿着記錄所在的單向鏈表向後掃描,直到某條記錄不符合key_part1 = 'a'條件爲止。因此在使用idx_key_part索引執行查詢Q7的過程當中,對應的掃描區間實際上是['a', 'a'],造成該掃描區間的搜索條件是key_part1 = 'a',與key_part3 = 'c'無關。
小貼士: 針對獲取到的每一條二級索引記錄,若是沒有開啓索引條件下推特性的話,則必須先進行回表操做,獲取到完整的用戶記錄後再判斷key_part3 = 'c'這個條件是否成立;若是開啓了索引條件下推特性的話,能夠當即判斷該二級索引記錄是否符合key_part3 = 'c'這個條件,若是符合則再進行回表操做,若是不符合則不進行回表操做,直接跳到下一條二級索引記錄。索引條件下推特性是在MySQL 5.6中引入的,默認是開啓的。
對於下邊這個查詢Q8來講:
Q8:SELECT * FROM single_table
WHERE key_part1 < 'b' AND
key_part2 = 'a';
複製代碼
因爲二級索引記錄是先按照key_part1列的值進行排序的,因此符合key_part1 < 'b'條件的二級索引記錄確定是相鄰的,可是對於符合key_part1 < 'b'條件的二級索引記錄來講,並非直接按照key_part2列進行排序的,也就是說咱們不能根據搜索條件key_part2 = 'a'來進一步減小須要掃描的記錄數量。那麼若是咱們使用idx_key_part索引執行查詢的話,能夠定位到第一條符合key_part1<'b'條件的記錄(其實就是idx_key_part索引第一個葉子節點的第一條記錄),而後沿着記錄所在的單向鏈表向後掃描,直到某條記錄不符合key_part1 < 'b'條件爲止,以下圖所示。
對於下邊這個查詢Q9來講:
Q9:SELECT * FROM single_table
WHERE key_part1 <= 'b' AND
key_part2 = 'a';
複製代碼
很顯然Q8和Q9長得很是像,只不過在涉及key_part1的條件中,Q8中的條件是key_part1 < 'b',Q9中的條件是key_part1 <= 'b'。很顯然符合key_part1 <= 'b'條件的二級索引記錄是相鄰的,可是對於符合key_part1 <= 'b'條件的二級索引記錄來講,並非直接按照key_part2列進行排序的。可是,我這裏說可是哈,對於符合key_part1 = 'b'的二級索引記錄來講,是按照key_part2列的值進行排序的。那麼咱們在肯定須要掃描的二級索引記錄的範圍時,當二級索引記錄的key_part1列值爲'b'時,咱們也能夠經過key_part2 = 'a'這個條件來減小須要掃描的二級索引記錄範圍,也就是說當咱們掃描到第一條不符合 key_part1 = 'b' AND key_part2 = 'a'條件的記錄時,就能夠結束掃描,而不須要將全部key_part1列值爲'b'的記錄掃描完,示意圖以下:
可能將查詢Q9轉換爲下邊的這個形式後更容易理解使用idx_key_part索引執行它時對應的掃描區間以及造成掃描區間的條件:
SELECT * FROM single_table
WHERE (key_part1 < 'b' AND key_part2 = 'a')
OR (key_part1 = 'b' AND key_part2 = 'a');
複製代碼
更多內容請參考掘金小冊《MySQL是怎樣運行的:從根兒上理解MySQL》
本文首發於公衆號「咱們都是小青蛙」。
寫文章挺累的,有時候你以爲閱讀挺流暢的,那實際上是背後無數次修改的結果。若是你以爲不錯請幫忙轉發一下,萬分感謝~ 這裏是個人公衆號「咱們都是小青蛙」,裏邊有更多技術乾貨,時不時扯一下犢子,歡迎關注: