看面試題的時候,總能看到MySQL在什麼狀況下用不上索引,以下:mysql
MySQL的WHERE子句中包含 IS NULL、IS NOT NULL、!= 這些條件時便不能使用索引查詢,只能使用全表掃描。面試
本身作了個驗證:
一個大概3萬數據的表,若是隻有10多個記錄是null
值,is null
走索引,not null
和!=
沒走索引,若是大部分都是null
值,只有部分幾條數據有值,is null
,not null
和!=
都走索引。算法
如下是搬過來網上的驗證,讓你們看看,結構以下:sql
CREATE TABLE s1 ( id INT NOT NULL AUTO_INCREMENT, key1 VARCHAR(100), key2 VARCHAR(100), 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), KEY idx_key2 (key2), KEY idx_key3 (key3), KEY idx_key_part(key_part1, key_part2, key_part3) ) Engine=InnoDB CHARSET=utf8;
這個表裏有10000條記錄:優化
mysql> SELECT COUNT(*) FROM s1; +----------+ | COUNT(*) | +----------+ | 10000 | +----------+ 1 row in set (0.00 sec)
下邊貼幾個圖:
上邊幾個查詢語句的WHERE
子句中用了IS NULL
、IS NOT NULL
、!=
這些條件,可是從它們的執行計劃中能夠看出來,這些語句都採用了相應的二級索引執行查詢,而不是使用所謂的全表掃描,謠言不攻自破。咱們來更細緻的分析一下這些查詢究竟是怎麼執行的。設計
在MySQL中,每一條記錄都有它固定的格式,咱們以InnoDB
存儲引擎的Compact
行格式爲例,來看一下NULL
值是怎樣存儲的。在Compact
行格式下,一條記錄是由下邊這幾個部分構成的:
code
新建一個稱之爲record_format_demo
的表:orm
CREATE TABLE record_format_demo ( c1 VARCHAR(10), c2 VARCHAR(10) NOT NULL, c3 CHAR(10), c4 VARCHAR(10) ) CHARSET=ascii ROW_FORMAT=COMPACT;
由於咱們的重點是NULL
值是如何存儲在記錄中的,因此重點嘮叨一下行格式的NULL值列表
部分。存儲NULL
值的過程以下:
1.首先統計表中容許存儲NULL
的列有哪些。blog
咱們前邊說過,主鍵列、被NOT NULL
修飾的列都是不能夠存儲NULL
值的,因此在統計的時候不會把這些列算進去。比方說表record_format_demo
的3個列c1
、c3
、c4
都是容許存儲NULL值的,而c2列是被NOT NULL修飾,不容許存儲NULL
值。排序
2.若是表中沒有容許存儲NULL
的列,則NULL值列表
也不存在了,不然將每一個容許存儲NULL
的列對應一個二進制位,二進制位按照列的順序逆序排列,二進制位表示的意義以下:
由於表record_format_demo
有3個值容許爲NULL
的列,因此這3個列和二進制位的對應關係就是這樣:
再一次強調,二進制位按照列的順序逆序排列,因此第一個列c1
和最後一個二進制位對應。
NULL
。NULL
。3.設計InnoDB
的大叔規定NULL值列表
必須用整數個字節的位表示,若是使用的二進制位個數不是整數個字節,則在字節的高位補0。
表record_format_demo
只有3個值容許爲NULL
的列,對應3個二進制位,不足一個字節,因此在字節的高位補0,效果就是這樣:
以此類推,若是一個表中有9個容許爲NULL
,那這個記錄的NULL值列表
部分就須要2個字節來表示了。
假設咱們如今向record_format_demo
表中插入一條記錄:
INSERT INTO record_format_demo(c1, c2, c3, c4) VALUES('eeee', 'fff', NULL, NULL);
這條記錄的c1
、c3
、c4
這3個列中c3
和c4
的值都爲NULL
,因此這3個列對應的二進制位的狀況就是:
因此這記錄的NULL值列表
用十六進制表示就是:0x06
。
對於InnoDB存儲引擎來講,記錄都是存儲在頁面中的(一個頁面默認是16KB大小),這些頁面能夠做爲B+
樹的節點而組成一個索引,相似這種樣子(只是用下邊的圖舉個B+樹的例子而已,跟咱們上邊列舉的表不要緊):
聚簇索引和二級索引都對應着像上圖同樣的B+
樹(也就是說有多少個索引就有多少棵對應的B+
樹),不過:
對於聚簇索引索引來講,頁面中的記錄是按照主鍵值進行排序的;而對於二級索引來講,頁面中的記錄是按照給定的索引列的值進行排序的。
對於聚簇索引來講,B+樹每一層節點(頁面)都是按照頁中記錄的主鍵值大小進行排序的;而對於二級索引來講,B+樹每一層節點(頁面)都是按照頁中記錄的給定的索引列的值進行排序的。
對於聚簇索引來講,B+樹葉子節點對應的頁面中存儲的是完整的用戶記錄(就是一條記錄中包含咱們定義的全部列值,還包含一些InnoDB本身添加的一些隱藏列);而對於二級索引來講,B+樹葉子節點對應的頁面中存儲的只是索引列的值 + 主鍵值
。
按規定,一條記錄的主鍵值不容許存儲NULL
值,因此下邊語句中的WHERE子句結果確定爲FALSE
:
SELECT * FROM tbl_name WHERE primary_key IS NULL;
像這樣的語句優化器本身就能斷定出WHERE子句一定爲NULL,因此壓根兒不會去執行它,不信咱們看(Extra信息提示WHERE子句壓根兒不成立):
對於二級索引來講,索引列的值可能爲NULL
。那對於索引列值爲NULL
的二級索引記錄來講,它們被放在B+樹的哪裏呢?答案是:放在B+
樹的最左邊。比方說咱們有以下查詢語句:
SELECT * FROM s1 WHERE key1 IS NULL;
那它的查詢示意圖就以下所示:
從圖中能夠看出,對於s1
表的二級索引idx_key1
來講,值爲NULL
的二級索引記錄都被放在了B+
樹的最左邊,這是由於設計InnoDB
的大叔有這樣的規定:
We define the SQL null to be the smallest possible value of a field.
也就是說他們把SQL中的NULL
值認爲是列中最小的值。
在經過二級索引idx_key1
對應的B+
樹快速定位到葉子節點中符合條件的最左邊的那條記錄後,也就是本例中id
值爲521
的那條記錄以後,就能夠順着每條記錄都有的next_record
屬性沿着由記錄組成的單向鏈表去獲取記錄了,直到某條記錄的key1列不爲NULL。
小貼士: 經過B+樹快速定位到葉子節點的記錄的過程是靠一個所謂的頁目錄(Page Directory)作到的,不過這不是本文的重點,你們能夠到小冊中翻看,都有詳細解釋。
那既然IS NULL
、IS NOT NULL
、!=
這些條件均可能使用到索引,那到底何時索引,何時採用全表掃描呢?
答案很簡單:成本。由於篇幅有限,咱們在這裏只准備定性的分析一下。對於使用二級索引進行查詢來講,成本組成主要有兩個方面:
讀取二級索引記錄的成本
將二級索引記錄執行回表操做,也就是到聚簇索引中找到完整的用戶記錄的操做所付出的成本。
很顯然,要掃描的二級索引記錄條數越多,那麼須要執行的回表操做的次數也就越多,達到了某個比例時,使用二級索引執行查詢的成本也就超過了全表掃描的成本(舉一個極端的例子,比方說要掃描的所有的二級索引記錄,那就要對每條記錄執行一遍回表操做,天然不如直接掃描聚簇索引來的快)。
因此MySQL優化器在真正執行查詢以前,對於每一個可能使用到的索引來講,都會預先計算一下須要掃描的二級索引記錄的數量,比方說對於下邊這個查詢:
SELECT * FROM s1 WHERE key1 IS NULL;
優化器會分析出此查詢只須要查找key1
值爲NULL
的記錄,而後訪問一下二級索引idx_key1
,看一下值爲NULL
的記錄有多少(若是符合條件的二級索引記錄數量較少,那麼統計結果是精確的,若是太多的話,會採用必定的手段計算一個模糊的值,固然算法也比較麻煩,咱們就不展開說了,小冊裏有說),這種在查詢真正執行前優化器就率先訪問索引來計算須要掃描的索引記錄數量的方式稱之爲index dive
。固然,對於某些查詢,比方說WHERE子句中有IN條件,而且IN條件中包含許多參數的話,比方說這樣:
SELECT * FROM s1 WHERE key1 IN ('a', 'b', 'c', ... , 'zzzzzzz');
這樣的話須要統計的key1
值所在的區間就太多了,這樣就不能採用index dive
的方式去真正的訪問二級索引idx_key1
,而是須要採用以前在背地裏產生的一些統計數據去估算匹配的二級索引記錄有多少條(很顯然根據統計數據去估算記錄條數比index dive
的方式精確性差了不少)。
反正不論採用index dive
仍是依據統計數據估算,最終要獲得一個須要掃描的二級索引記錄條數,若是這個條數佔整個記錄條數的比例特別大,那麼就趨向於使用全表掃描執行查詢,不然趨向於使用這個索引執行查詢。
理解了這個也就好理解爲何在WHERE子句中出現IS NULL
、IS NOT NULL
、!=
這些條件仍然可使用索引,本質上都是優化器去計算一下對應的二級索引數量佔全部記錄數量的比值而已。
你們能夠看到,MySQL中決定使不使用某個索引執行查詢的依據很簡單:就是成本夠不夠小。而不是是否在WHERE子句中用了IS NULL
、IS NOT NULL
、!=
這些條件。