索引——MySQL技術內幕 InnoDB儲存引擎
InnoDB支援以下索引
- B+樹索引
- 雜湊索引
- 全文索引
其中,雜湊索引是由InnoDB自動建立的自適應雜湊索引,人工無法干預。
我們最常見到的,也是大部分資料庫系統都在用的索引型別就是B+樹索引。
索引並不是越多越好,想要提升查詢效能就要在更新效能上做一定的讓步,我們需要在這兩種效能之間尋找一個平衡點,而不是一味的建立索引。
B+樹索引
不會過多介紹什麼是B+樹以及什麼是聚集索引,非聚集索引(輔助索引)。
聚集索引
我們通過一個例子看看InnoDB如何儲存聚集索引,當前頁大小為16KB,我們人為讓一個頁只能儲存兩行資料。
CREATE TABLE test_cidx( a INT PRIMARY KEY, b VARCHAR(8000), c INT NOT NULL, KEY idx_c (c) ); INSERT INTO test_cidx SELECT 1, REPEAT('a', 7000), -1; INSERT INTO test_cidx SELECT 2, REPEAT('a', 7000), -2; INSERT INTO test_cidx SELECT 3, REPEAT('a', 7000), -3; INSERT INTO test_cidx SELECT 4, REPEAT('a', 7000), -4;
檢視現在該表空間的頁情況
這裡有了5個B+樹索引頁,並且通過觀察prev
和next
,我們能得知6,7,8是葉子節點。而4和5都是非葉子節點。我通過觀察發現4這個頁才是聚集索引的B+樹的根節點,而關於5,那是我們在列c上建立的輔助索引。
對於資料頁(葉子節點的B+樹頁),上一篇筆記已經分析過了,我們只看索引頁(非葉子節點的B+樹頁),也就是第4個頁。
通過計算得知,第四個頁的偏移量在00010000
,通過hexdump工具將二進位制檔案轉換為文字並找到00010000
這一行。
我們關心索引頁具體是如何儲存的,而不關心它的其他結構,如FILE_HEADER
......所以我們需要找到infimum
和supremum
,這兩個虛擬行中間就是索引頁中儲存的B+樹記錄。
這個69 6e 66 69 6d 75 6d
代表的就是infimum
,它前面的五個位元組中的最後兩個就是相對於infimum
記錄的起始位置,下一條記錄的偏移量。也就是00 1b
。
也就是說,00010063 + 0000001b = 00010079
,就是實際的B+樹中第一條記錄的位置。也就是這個80
開始的位置。
80 00 00 01
最後的01,代表主鍵列的值為1
(32位有符號整數,第一位符號位),後面的00 00 00 06
代表具有該索引值的記錄在第6頁中。按此方式尋找,還能找到幾個另外的記錄以及所在的頁。
主鍵id 所在頁
1 6
2 7
4 8
所以我們可以推測,主鍵id為3的記錄也在頁2裡,也就是說頁2儲存了兩行資料,其它的每一個數據頁分別儲存一行資料。
需要注意,很多資料庫書籍上給人的印象就是聚集索引按主鍵順序對記錄進行儲存,但在實現中,維護這個順序過於複雜,所以,很多時候只是使用這種雙向連結串列來維護一個聚集索引的邏輯順序。
聚集索引對主鍵的排序查詢和範圍查詢非常快,因為它邏輯有序,所以根本不用進行實際的外部或內部歸併排序。
輔助索引
輔助索引有很多名字,比如,非聚集索引,二級索引,它們說的都是一個東西。即按照某一列(幾列)作為索引碼構建B+樹,但它的葉子節點不儲存整行資料,而是儲存一個書籤(bookmark),用於找到與索引對應的行資料。
InnoDB中,這個書籤就是主鍵列的值,因為InnoDB會保證每張表都有一個基於主鍵的聚集索引,查詢非聚集索引時只需要拿到這個書籤,再去查詢聚集索引即可獲得對應的行記錄。
還是剛剛的表空間,分析頁5,也就是建立在列c上的輔助索引。第五頁的起始位置是00014000
。
還是找69 6e 66 69 6d 75 6d infimum
,它在00014063
的位置。
01 00 02 00 41 ## 00 41為偏移量
69 6e 66 69 6d 75 6d ## infimum
所以第一行在:00014063 + 00000041 = 000140A4
第一行資料是7f ff ff fc 80 00 00 04
,格式化一下應該會看的清楚一點
7f ff ff fc ## -4
80 00 00 04 ## 4
我不知道InnoDB的整數使用什麼規範,反正好像不是補碼,但是第一位也是標誌位。唉,計算機原理學的不好。。
總之,這一行資料證明了輔助索引列c為-4
的那一條記錄的主鍵值為4
,然後InnoDB就會通過主鍵值去聚集索引裡再次查詢這行資料。
對於其它的幾個輔助索引,也能看到
7f ff ff ff ## -1
80 00 00 01 ## 1
====
7f ff ff fe ## -2
80 00 00 02 ## 2
====
7f ff ff fd ## -3
80 00 00 02 ## 3
InnoDB中的B+樹節點分裂
最簡單的方法就是從中間分裂開,但當插入是順序的時候,如果從中間分裂節點,會導致節點利用率變低,因為分裂後,左邊的節點永遠不會插入新資料。
上一篇筆記中的InnoDB頁中的Page Header有這樣幾個欄位,當時還想,存這幾個欄位有啥用呢。
這幾個欄位就是用來儲存最近插入的方向已經已經連續多少次同方向插入了。通過這幾個欄位就可以判斷資料是連續的還是隨機插入的。然後InnoDB會選擇一個最合適的分裂位置進行分裂。
InnoDB B+樹索引管理
建立刪除索引
ALTER TABLE tablename
ADD | DROP {INDEX | KEY} [index_name] [index_type] (index_col_name,...) [index_option] ...
CREATE [UNIQUE] INDEX index_name
[index_type]
ON tablename (index_col_name,...)
DROP INDEX index_name ON tablename
檢視索引資訊
SHOW INDEX FROM tablename\G;
比較關鍵的是Cardinality
,這個代表表中這一個索引列大概有多少不重複資料,如果它和表的行數差距太大的話,可以考慮刪除這個索引了。SQL優化器也會考慮這個值,如果這個索引太垃圾了,優化器就考慮不走這個索引。這個值不是實時更新的,所以只是一個估計值。
可以使用ANALYZE TABLE
命令對錶進行分析,從而更新這個值。
Fast Index Creation
MySQL 5.5 之前如果想後期對錶的索引進行改動,會執行如下操作
- 建立與原表具有新結構的臨時表
- 把原表中的資料匯入到臨時表
- 刪除原表
- 把臨時表重新命名為原表的名字
這樣做會消耗很長時間,並且修改時不能處理有關該表的其他事務。
InnoDB 1.0.x支援了FIC,即快速索引建立。就是當建立輔助索引時先對錶新增一個S鎖,然後不用重建表,即可在原表中對錶結構進行改動。對於需要獲取X鎖的寫事務,同樣無法處理,建立和刪除主鍵同樣需要重建表。
Online Schema Change
一種線上執行DDL的方式,允許執行DDL時對錶進行讀寫操作。不過這是在資料庫外部實現的。
Facebook的OSC實現也採用了表複製的辦法。
Online DDL
MySQL 5.6開始支援Online DDL。在資料庫層面允許建立輔助索引的同時執行其他DML操作,除了建立輔助索引,還支援線上執行以下DDL操作:
- 輔助索引建立與刪除
- 改變自增長值
- 新增或刪除外來鍵約束
- 列的重新命名
可以在ALTER TABLE
語句中新增一個ALGORITHM
引數,用於啟用Online DDL。它的可選值有DEFAULT | INPLACE | COPY
,COPY
即原始的複製方式,INPLACE
即無需複製的方式,DEFALUT
根據引數old_alter_table
來判斷使用什麼演算法。
OFF說明預設使用INPLACE
。
ALTER TABLE
還增加了一個LOCK
引數,用於指定執行DDL時表的鎖定狀態。可以選擇NONE | SHARE | EXCLUSIVE | DEFAULT
。分別是不上鎖,共享鎖,排他鎖和預設。
預設情況下,資料庫會判斷當前DDL操作能否使用NONE
,如果不能再判斷SHARE
,如果還不能再判斷EXCLUSIVE
。
InnoDB通過在執行DDL時對DML語句的操作日誌儲存到快取中,等待DDL執行完成再將重做應用到表上。快取的大小由innodb_online_alter_log_max_size
控制。如果快取不夠大,DDL執行不成功。
Cardinality值
在介紹資料庫系統概念的書中,在索引一章都會提到索引的選擇性。選擇性就是索引列不重複的行數/所有行數
,這個值應該無限接近於1,如果這個值非常小,那麼建立索引的意義不大。索引的意義在於從大量資料中快速檢索出其中的一小部分(或一條)。
在MySQL中使用SHOW INDEX
命令得到的Cardinality
欄位代表索引列在表中不重複的行數的估計值,InnoDB是如何產生這個估計值的呢?
InnoDB的Cardinality統計
在一個訪問量不小的生產系統中,不可能每次產生索引更新操作就統計Cardinality,一是要統計的表可能非常大,二是太浪費系統的計算能力。
InnoDB通過兩個條件觸發Cardinality的更新
- 表中1/16的資料已經發生變化
- stat_modified_counter > 2 000 000 000
第二個主要適用於更新頻繁的發生在一些固定的行內時。
上面是何時更新,下面是怎樣更新,InnoDB和大部分資料庫一樣,通過取樣的方式更新這個值,所以說這是個預估值。
InnoDB隨機抽取8個葉子節點,統計其中不同記錄的個數,分別記作\(P_1, P_2, ... ,P_8\),然後\(Cardinality=(P_1+P2+...+P_8) * A / 8\)(A為表中所有葉子節點的數量)。
B+樹索引的使用
聯合索引
如下是使用多個列進行聯合索引的B+樹,可以看到B+樹中的排序依據是使用兩個列一起比較,也可以理解為先按第一個列排序,若第一個列相等再按第二個列排序。
假如上圖中的第一個列名為a
,第二個列名為b
,那麼下面的語句可以用到索引
# 語句1 因為a已經有序
SELECT * FROM t WHERE a = 'xxx';
# 語句2 因為所有a相等的專案,它們按照b來排序
SELECT * FROM t WHERE a = 'xxx' and b='xxx';
下面的語句不能使用索引
# 語句1 因為沒有條件限定a的話,b是無序的
SELECT * FROM t WHERE b = 'xxx';
聯合索引可以規避一些排序操作,比如獲取使用者最近3次的購買記錄。
CREATE TABLE buy_log(
user_id INT UNSIGNED NOT NULL,
buy_date DATE
);
INSERT INTO buy_log VALUES (1, '2009-01-01');
INSERT INTO buy_log VALUES (2, '2009-01-01');
INSERT INTO buy_log VALUES (3, '2009-01-01');
INSERT INTO buy_log VALUES (1, '2009-02-01');
INSERT INTO buy_log VALUES (2, '2009-02-01');
INSERT INTO buy_log VALUES (3, '2009-02-01');
INSERT INTO buy_log VALUES (1, '2009-03-01');
INSERT INTO buy_log VALUES (1, '2009-04-01');
INSERT INTO buy_log VALUES (3, '2009-04-01');
僅建立單獨索引
ALTER TABLE buy_log ADD KEY (user_id);
獲取最近三次購買記錄
分析一下該語句執行過程,使用了外部檔案排序。因為取出了user_id=1
的購買記錄,但它們並未按照buy_date
排序,所以要進行一次檔案排序。檔案排序是一個極其耗時的操作,需要很多次IO操作,尤其是在大表中應該儘量規避。
建立聯合索引
ALTER TABLE buy_log ADD KEY (user_id, buy_date);
這次可選的索引變成了兩個,而且使用了第二個索引,並且沒有進行檔案排序。
對於獨立索引和聯合索引共同存在的情況,如果你的查詢條件只用一個獨立索引就可以完成,MySQL會優先選擇獨立索引,因為這樣一個B+樹資料頁中可能包含更多符合條件的資料。
BUT WHY???? 我的MySQL大腦缺氧了嗎?
覆蓋索引
因為輔助索引中儲存的是(索引列1, 索引列2, 主鍵列1, 主鍵列2)
,如果通過查詢輔助索引,並且SELECT需要展示的結果列都在這個輔助索引裡,那麼就不用再去查詢聚集索引了。
比如:
SELECT 主鍵列1 FROM t WHERE 索引列1 = 'xxx';
覆蓋索引的另一個好處是,MySQL優化器遇到count(*)
操作會去統計輔助索引,因為相比統計聚集索引(統計整個表),它更小,需要讀取的磁碟塊更少。
一般情況下,對於輔助索引(a,b)
,條件中只查詢b
時,優化器不會考慮使用輔助索引,因為沒什麼意義。但當進行統計時,也會用到,同樣的原因,因為這個索引更小嘛~
優化器選擇不使用索引的情況
比如這條語句,優化器可能選擇不使用索引,就算你已經建立了orderid
上的輔助索引和(orderid, productid)
上的聯合輔助索引。
原因是查詢要返回所有欄位,覆蓋索引並不能生效,所以輔助索引還需要一次讀取書籤並再次查詢聚集索引的操作,這會把原本順序的磁碟操作打亂成一些離散的磁碟操作,即從讀輔助索引跳到讀聚集索引,再跳回輔助索引,再跳回聚集索引,這會徒增磁碟搜尋時間,所以當你要查詢相當大一批資料(一般是表的20%)時,MySQL會選擇直接選擇全表掃描,而放棄使用輔助索引。
現在的伺服器大都是SSD,最垃圾的伺服器也上SSD了,所以完全沒有磁碟搜尋時間,這時可以考慮使用如下語句強制使用索引。
索引提示
如果MySQL不能很好的選擇索引(一般情況下不存在),或者選擇索引所消耗的時間已經大於查詢時間(一般在分析範圍查詢時),可以考慮使用索引提示,告訴MySQL該使用什麼索引。
- USE INDEX(x) :告訴MySQL可以使用索引x,但具體使用哪個還是MySQL來決定
- FORCE INDEX(x) :強制使用索引x
Multi-Range-Read MRR優化
Multi-Range-Read主要用於優化上面所說的優化器不使用索引的情況。MySQL5.6開始支援。
不使用索引的原因很明瞭了,因為會InnoDB的輔助索引特性會導致產生隨機的磁碟訪問。
MRR優化對於這種情況做了如下優化:
- 先將讀取到的輔助索引鍵值放到快取中,這時快取中的資料按照輔助索引排序
- 將快取中的資料按照Rowid排序
- 根據Rowid排序的順序訪問聚集索引
MRR優化還會將聯合輔助索引拆分查詢。比如有如下索引(key_part1, key_part2)
之前,SQL優化器會使用索引,將key_part1
在指定範圍內的都取出,而不管key_part2
是否滿足,這樣就會有一些無用資料被取出。MRR會將查詢條件拆分為(1000, 10000), (1001, 10000)...(1999, 10000)這樣的鍵值對,然後根據這些條件進行查詢。
Index Condition Pushdown ICP優化
MySQL 5.6開始支援。即把進行索引搜尋時,將where條件的過濾下放到儲存引擎層(如果可以的話),這樣儲存引擎與SQL伺服器之間的資料傳輸和轉換就會減少了。
比如表中有聯合所有(zip_code, last_name, first_name)
如果不支援ICP的時候,可以使用聯合索引提取出所有zipcode=95054
的記錄,但是對於後面兩個模糊查詢,索引無能為力,需要在SQL層處理,這無形之間增加了SQL伺服器層面的壓力以及引擎層向SQL伺服器層遞交資料(需要轉換)的壓力。
開啟了ICP後,會在引擎層面過濾這些資料,即取到zipcode=95054
的索引項時就對另外兩個條件進行測試,如果不滿足就放棄它。前提是隻能對索引覆蓋到的列進行測試。
雜湊演算法
未完...