MySQL查詢成本和範圍區間

上一篇文章MySQL中IS NULL、IS NOT NULL、!=不能用索引?胡扯!中嘮叨了在WHERE子句中出現IS NULL、IS NOT NULL、!=這些條件時仍然可能使用索引,強調了一個查詢成本的問題,很多同窗反映對這個查詢成本仍是沒啥概念,咱們今天再來稍微深刻的嘮叨一下。算法

B+樹結構

咱們說對於InnoDB存儲引擎來講,表中的數據都存儲在所謂的B+樹中,咱們每多創建一個索引,就至關於多創建一棵B+樹。bash

  • 對於聚簇索引對應的B+樹來講,葉子節點處存儲了完整的用戶記錄(所謂完整用戶記錄,就是指一條聚簇索引記錄中包含全部用戶定義的列已經一些內建的列),而且這些聚簇索引記錄按照主鍵值從小到大排序。post

  • 對於二級索引對應的B+樹來講,葉子節點處存儲了不完整的用戶記錄(所謂不完整用戶記錄,就是指一條二級索引記錄只包含索引列和主鍵),而且這些二級索引記錄按照索引列的值從小到大排序。優化

咱們向表中存儲了多少條記錄,每一棵B+樹的葉子節點中就包含多少條記錄(注意是「每一棵」,包括聚簇索引對應的B+樹以及二級索引對應的B+樹)。ui

示例

咱們舉個例子:spa

CREATE TABLE t (
    id INT UNSIGNED NOT NULL AUTO_INCREMENT,
    key1 INT,
    common_field VARCHAR(100),
    PRIMARY KEY (id),
    KEY idx_key1 (key1)
) Engine=InnoDB CHARSET=utf8;
複製代碼

這個表就包含2個索引(也就是2棵B+樹):設計

  • id列爲主鍵對應的聚簇索引。3d

  • key1列創建的二級索引idx_key1code

咱們向表中插入一些記錄:orm

INSERT INTO t VALUES
    (1, 30, 'b'),
    (2, 80, 'b'),
    (3, 23, 'b'),
    (4, NULL, 'b'),
    (5, 11, 'b'),
    (6, 53, 'b'),
    (7, 63, 'b'),
    (8, NULL, 'b'),
    (9, 99, 'b'),
    (10, 12, 'b'),
    (11, 66, 'b'),
    (12, NULL, 'b'),
    (13, 66, 'b'),
    (14, 30, 'b'),
    (15, 11, 'b'),
    (16, 90, 'b');
複製代碼

因此如今s1表的聚簇索引示意圖就是這樣:

image_1dg2m0lab11lhlib1eq7g5dius2d.png-77.1kB

s1表的二級索引示意圖就是這樣:

image_1dg2m44tf1o4cv3nro1oul111n2q.png-66.9kB

從圖中能夠看出,值爲NULL的二級索引記錄都被放到了B+樹的最左邊,這是由於設計InnoDB的大叔們有規定:

We define the SQL null to be the smallest possible value of a field.

也就是認爲NULL值是最小的。

小貼士: 原諒咱們把B+樹的結構作了一個如此這般的簡化,咱們省略了頁面的結構,省略了全部的內節點(只畫了了三角形替代),省略了記錄之間的鏈表,由於這些不是本文的重點,畫成若是所示的樣子只是爲了突出葉子節點處的記錄是按照給定索引的鍵值進行排序的。

比方說咱們如今執行下邊這個查詢語句:

SELECT * FROM t WHERE key1 = 53;
複製代碼

那麼語句的執行過程就以下圖所示:

image_1dg2me2kg1tvh1ohlera1ge712837.png-80.5kB

用文字描述一下這個過程也就是:

  • 先經過二級索引idx_key1對應的B+樹快速定位到key1列值爲53的那條二級索引記錄。

  • 而後經過二級索引記錄上的主鍵值,也就是6到執行回表操做,也就是到聚簇索引中再找到id列值爲6的聚簇索引記錄。

小貼士: B+樹葉子節點中的記錄都是按照鍵值按照從小到大的順序排好序的,經過B+樹索引定位到葉子節點中的一條記錄是很是快速的。不過因爲咱們並無嘮叨內節點、頁目錄這些東西,因此經過B+樹索引定位到葉子節點中的一條記錄的過程就不詳細嘮叨了,這些東西其實都在《MySQL是怎樣運行的:從根兒上理解MySQL》的掘金小冊裏詳細講述過。

像下邊這個查詢:

SELECT * FROM t WHERE key1 > 20 AND key1 < 50;
複製代碼

它的執行示意圖就是這樣:

image_1dg2ohcgd1psqaut11levmk1oki3k.png-88.7kB

用文字表述就是這樣:

  • 先經過二級索引idx_key1對應的B+樹快速定位到知足key1 > 20的第一條記錄,也就是咱們圖中所示的key1值爲23的那條記錄,而後根據該二級索引中的主鍵值3執行回表操做,獲得完整的用戶記錄後發送到客戶端。

  • 而後根據上一步驟中獲取到的key1列值爲23的二級索引記錄的next_record屬性,找到緊鄰着的下一條二級索引記錄,也就是key1列值爲30的記錄,而後執行回表操做,獲得完整用戶記錄後發送到客戶端。

  • 而後再找上一步驟中獲取到的key1列值爲30的二級索引記錄的下一條記錄,該記錄的key1列值也爲30,繼續執行回表操做將完整的用戶記錄發送到客戶端。

  • 而後再找上一步驟中獲取到的key1列值爲30的二級索引記錄的下一條記錄,該記錄的key1列值爲53,不知足key1 < 50的條件,因此查詢就此終止。

從上邊的步驟中也能夠看出來:須要掃描的二級索引記錄越多,須要執行的回表操做也就越多。若是須要掃描的二級索引記錄佔所有記錄的比例達到某個範圍,那優化器就可能選擇使用全表掃描的方式執行查詢(一個極端的例子就是掃描所有的二級索引記錄,那麼將對全部的二級索引記錄執行回表操做,顯然還不如直接全表掃描)。

小貼士: 咱們這裏仍是定型的分析成本,而不定量分析。定量分析的過程比較複雜,不太小冊裏有寫,有興趣的同窗能夠去看。

因此如今的結論就是:斷定某個查詢是否可使用索引的條件就是須要掃描的二級索引記錄佔所有記錄的比例是否比較低,較低的話說明成本較低,那就可使用二級索引來執行查詢,不然要採用全表掃描

具體的查詢條件分析

咱們分別看一下WHERE子句中出現IS NULLIS NOT NULL!=這些條件時優化器是怎麼作決策的。

IS NULL的狀況

比方說這個查詢:

SELECT * FROM t WHERE key1 IS NULL;
複製代碼

優化器在真正執行查詢前,會首先少許的訪問一下索引,調查一下key1[NULL, NULL]這個區間的記錄有多少條:

image_1dg2r7u6s1ms3162d1erp52immf8c.png-28.9kB

小貼士: [NULL, NULL]這個區間表明區間裏只有一個NULL值。

優化器通過調查得知,須要掃描的二級索引記錄佔總記錄條數的比例是3/16,它以爲這個查詢使用二級索引來執行比較靠譜,因此在執行計劃中就顯示使用這個idx_key1來執行查詢:

image_1dg2pki1r12stmt419ae1ep7125a5e.png-40.2kB

IS NOT NULL的狀況

比方說這個查詢:

SELECT * FROM t WHERE key1 IS NOT NULL;
複製代碼

優化器在真正執行查詢前,會首先少許的訪問一下索引,調查一下key1(NULL, +∞)這個區間內記錄有多少條:

image_1dg2rbnspvavkk710k0adkhpua9.png-28.9kB

小貼士: 咱們這裏把NULL看成是最小值對待,你能夠認爲它比-∞都小。另外注意區間(NULL, +∞)是開區間,也就意味這不包括NULL值。

優化器通過調查得知,須要掃描的二級索引記錄佔總記錄條數的比例是13/16,跟顯然這個比例已經很是大了,因此優化器決定使用全表掃描的方式來執行查詢:

image_1dg2pu5kc79oh5c1m2vj131inu6l.png-36.4kB

那怎麼才能讓使用IS NOT NULL條件的查詢使用到二級索引呢?這還不簡單,讓表中符合IS NOT NULL條件的記錄少不就好了,咱們能夠執行一下:

UPDATE t SET key1 = NULL WHERE key1 < 80;
複製代碼

這樣再去執行這個查詢:

SELECT * FROM t WHERE key1 IS NOT NULL;
複製代碼

優化器在真正執行查詢前,會首先少許的訪問一下索引,調查一下key1(NULL, +∞)這個區間內記錄有多少條::

image_1dg2remb8gts2j6rjnvuic8b6.png-29.3kB

優化器通過調查得知,須要掃描的二級索引記錄佔總記錄條數的比例是3/16,它以爲這個查詢使用二級索引來執行比較靠譜,因此在執行計劃中就顯示使用這個idx_key1來執行查詢:

image_1dg2q4glvgea1kt01q71h5q1b2p72.png-40kB

!= 的狀況

比方說這個查詢:

SELECT * FROM t WHERE key1 != 80;
複製代碼

優化器在真正執行查詢前,會首先少許的訪問一下索引,調查一下key1(NULL, 80)(80, +∞)這兩個區間內記錄有多少條:

image_1dg2rst6h1egu1cue1idnbk67s1bj.png-31.5kB

優化器通過調查得知,須要掃描的二級索引記錄佔總記錄條數的比例是2/16,它以爲這個查詢使用二級索引來執行比較靠譜,因此在執行計劃中就顯示使用這個idx_key1來執行查詢:

image_1dg2pki1r12stmt419ae1ep7125a5e.png-40.2kB

且慢!爲啥執行計劃的rows列的值爲3呢???這是個什麼鬼,明明只有2條記錄符合條件嘛。哈哈,咱們羅列一下每一個區間找到的符合條件的記錄數量:

  • (NULL, 80)區間中有0條記錄知足條件key1 != 80

  • (80, +∞)區間中有2條記錄知足條件key1 != 80

但是設計優化器的大叔在這裏有個規定:當某個範圍區間符合給定條件的記錄數量爲0時,硬生生的把它掰成1。也就是說實際優化器認爲在(NULL, 80)這個範圍區間中有1條記錄符合條件key1 != 80。因此執行計劃的rows列才顯示了3

小貼士: 下邊是設計優化器的大叔本身對當某個範圍區間符合給定條件的記錄數量爲0時硬生生的把它掰成1的解釋(能看懂的就看,看不懂趕忙跳過): The MySQL optimizer seems to believe an estimate of 0 rows is always accurate and may return the result 'Empty set' based on that. The accuracy is not guaranteed, and even if it were, for a locking read we should anyway perform the search to set the next-key lock. Add 1 to the value to make sure MySQL does not make the assumption!

總結

至此,咱們分別分析了擁有IS NULLIS NOT NULL!=這三個條件的查詢是在什麼狀況下使用二級索引來執行的,核心結論就是:成本決定執行計劃,跟使用什麼查詢條件並無什麼關係。優化器會首先針對可能使用到的二級索引劃分幾個範圍區間,而後分別調查這些區間內有多少條記錄,在這些範圍區間內的二級索引記錄的總和佔總共的記錄數量的比例達到某個值時,優化器將放棄使用二級索引執行查詢,轉而採用全表掃描。

小貼士: 其實範圍區間劃分的太多也會影響優化器的決策,比方說IN條件中有太多個參數,將會下降優化器決定使用二級索引執行查詢的概率。 另外,優化器調查在某個範圍區間內的索引記錄的條數的方式有兩種,一種是所謂的index dive(這種方式在數據少的時候是精確的,在數據多時會有些誤差),一種是依賴index statistics,也就是統計數據來作調查(這種方式的統計是很不精確的,有時候誤差是超級巨大的),反正不論採用哪一種方式,優化器都會將各個範圍區間中的索引記錄數量給計算出來。關於這兩種調查方式在小冊中都給出了詳細的算法,固然都佔用了至關大的篇幅,寫在公衆號文章裏就有點殺雞用牛刀了。

題外話

寫文章挺累的,有時候你以爲閱讀挺流暢的,那實際上是背後無數次修改的結果。若是你以爲不錯請幫忙轉發一下,萬分感謝~ 這裏是個人公衆號「咱們都是小青蛙」,裏邊有更多技術乾貨,時不時扯一下犢子,歡迎關注:

相關文章
相關標籤/搜索