MySQL InnoDB儲存引擎體系架構 —— 索引高階
眾所周知,在MySQL的InnoDB引擎,為了提高查詢速度,可以在欄位上新增索引,索引就像一本書的目錄,通過目錄來定位書中的內容在哪一頁。
InnoDB支援的索引有如下幾種:
- B+樹索引
- 全文索引
- 雜湊索引
筆者上一篇文章已MySQL InnoDB儲存引擎體系架構 —— 記憶體管理 經提到過,InnoDB的雜湊索引是自適應的,使用者無法對其進行干預,在此不再贅述,本文重點介紹B+樹索引。
一、資料結構——B+樹
相信大家在大學的資料結構的課程中都學過二分查詢、二叉樹和平衡二叉樹。在一組有序的資料中,利用二分查詢可以在log2N的複雜度中快速檢索資料,平衡二叉樹是在二叉查詢樹的基礎上演變而來,解決了二叉查詢樹在極端情況下轉化為單鏈表的問題。而B+樹呢?讓我們來看B+樹的結構
在B+樹中,資料都是按照從下到大的順序存放在葉子節點中,由上圖的B+樹可得出,這顆B+樹的高度為2,每頁可儲存4條資料,扇出為5,第一層是索引頁,第二層是資料頁。資料庫B+樹索引的本質就是B+樹在資料庫中的實現,並且B+樹的高度一般限制在2-4層,磁碟的IO操作只需要2-4次,所以在索引上查詢資料,速度很快。
二、B+樹索引
1、聚集索引
在InnoDB引擎中,都有一個聚集索引,一般是primary key,若使用者沒有顯示指定primary key,InnoDB會預設選擇表的第一個not null的unique索引為主鍵,若沒有,則會自動建立一個6位元組大小的_rowid作為主鍵。
上圖是一張聚集索引的示意圖,由上圖,我們可以看到,該樹分為兩層,同樣第一層是索引頁,第二層是資料頁,實實在在存放資料的地方。我們還可以得出,索引頁存放的並不是資料而是指向真實資料的一個偏移量,而真實資料存放在第二層的資料頁,所以如果一條SQL語句命中索引,只是命中了索引頁的資料,然後通過索引頁找到真實資料所在的頁。
聚集索引的儲存在物理上不是連續的,在邏輯上卻是連續的,這是因為頁與頁是通過雙向連結串列維護的,而每頁中行記錄也是通過雙向連結串列維護。為什麼要雙向連結串列??這是因為方便範圍查詢和排序,如過找到某個索引所在資料頁的偏移量,直接遍歷這個連結串列或者逆序遍歷這個連結串列,便可以方便的進行範圍查詢和逆序排序。比如
select * from table where id>10 and id<1000;
2、輔助索引
InnoDB的另一種索引,輔助索引,也叫二級索引或非聚集索引。對於輔助索引,葉子並不包含行記錄的全部資料,葉子節點除了包含鍵值外,還包含了一個被稱作“書籤”的東西,該書籤用來告訴InnoDB到哪裡可以找到所需的行的資料,所以書籤實際存放的是聚集索引,所以如果SQL命中了輔助索引,查詢流程分為兩步:
1、找到索引頁
2、通過索引頁找到資料頁,該資料頁包含聚集索引的的值
3、通過聚集索引找到行記錄
所以,輔助索引一般比聚集索引多一次IO。
一個很容易被DBA忽略的問題:如果一條SQL語句命中索引,B+樹索引不能找到一個給定查詢條件的具體行,只能找到被查詢資料行所在的頁,然後將這個資料讀入記憶體,然後再記憶體中遍歷所有行找到資料。另外,每一頁大小為16k,每一頁會包含多行,行與行之間是通過雙向連結串列組織的,所以範圍查詢或者順序倒序排序查詢時,只需遍歷連結串列就可以了。
三、索引管理
方便測試,我們建立一張表t,並新增索引
create table t(
a int primary key,
b varchar(500),
c int
);
alter table t add key idx_b (b(100));
alter table t add key idx_a_c (a,c);
alter table t add key idx_c (c);
表t,a欄位是主鍵,b欄位是字串長度500,在b欄位建立索引,索引名是idx_b,並且只對b的前100個字元建立索引,聯合s索引idx_a_c,和索引idx_c;
通過命令可以檢視某張表索引的建立情況
show index from t\G;
mysql> show index from t\G;
*************************** 1. row ***************************
Table: t
Non_unique: 0
Key_name: PRIMARY
Seq_in_index: 1
Column_name: a
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 2. row ***************************
Table: t
Non_unique: 1
Key_name: idx_b
Seq_in_index: 1
Column_name: b
Collation: A
Cardinality: 0
Sub_part: 100
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
*************************** 3. row ***************************
Table: t
Non_unique: 1
Key_name: idx_a_c
Seq_in_index: 1
Column_name: a
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null:
Index_type: BTREE
Comment:
Index_comment:
*************************** 4. row ***************************
Table: t
Non_unique: 1
Key_name: idx_a_c
Seq_in_index: 2
Column_name: c
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
*************************** 5. row ***************************
Table: t
Non_unique: 1
Key_name: idx_c
Seq_in_index: 1
Column_name: c
Collation: A
Cardinality: 0
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment:
Index_comment:
5 rows in set (0.01 sec)
我們來分析返回的資訊:
- table:索引所在的表名
- Non_unique:非唯一索引,我們可以看到primary key是0,代表非唯一索引
- Key_name:索引的名字
- Seq_in_index:索引中該列的位置,可以看索引idx_a_c就比較直觀
- Column_name:欄位名字
- Collation:一般都是A,此欄位不重要
- Cardinality:非常關鍵的一個欄位,在下面細講
- Sub_part:是否是列的部分被索引,b欄位長度500,我們只在b的前100長度上建立索引
- Packed:不重要
- Null:索引的列是否包含Null值
- Index_type:索引型別,都是BTREE
- Comment:註釋
- Index_comment:不重要
返回資料中,有一Cardinality欄位,優化器會根據這個欄位來選擇是否使用這個欄位,不過這個欄位並不是實時更新的,如果實時更新,代價比較大,如果要更新Cardinality欄位的值,可以使用如下命令
analyze table t\G;
Cardinality欄位代表什麼意思呢?表示索引中不重複記錄數量的預估值,Cardinality/count(*)的值儘可能接近1(幾乎沒有重複欄位),如果這個比值很小接近0,表示該索引中這個欄位的資料大部分都是重複的,那麼使用者可以考慮是否有必要建立這個索引。
那麼InnoDB何時更新Cardinality的值呢?
如果每次更新操作都對Cardinality進行更新統計,那麼代價是非常大的,因此InnoDB對Cardinality的更新策略如下:
- 表中1/16的資料已發生過變化
- start_modified_counter>2000000000 #20億
如果表中某一行資料頻繁的更新,表中資料量沒變,變化的只是這一行。
InnoDB如何統計Cardinality的值呢?
- 取得B+數葉子節點的數量,記作A
- 隨機取得8個葉子節點,統計每頁不同記錄得個數,記作p1,p2...p8
Cardinality = (p1+p2+..+p8)*A/8,因為是隨機取得8個葉子節點,所以暗示著每次計算出得Cardinality的值有可能不同。
讓我們老看一下,我們公司測服上的資料庫的Cardinality值
關於覆蓋索引:
- 就是select的資料列只用從索引中就能夠取得,不必從資料表中讀取,換句話說查詢列要被所使用的索引覆蓋。
- 如果一個索引包含了(或覆蓋了)滿足查詢語句中欄位與條件的資料就叫做覆蓋索引。
- 當發起一個被索引覆蓋的查詢(也叫作索引覆蓋查詢)時,在EXPLAIN的Extra列可以看到“Using index”的資訊
幾個例子如下,建表t,a是主鍵,b和c中新增聯合索引(b_c),並插入一些資料
create table t(
a int primary key auto_increment,
b int,
c int,
d int,
key b_c (b,c)
);
insert into t(b,c,d) values(1,1,1);
insert into t(b,c,d) values(2,2,2);
insert into t(b,c,d) values(3,3,3);
insert into t(b,c,d) values(4,4,4);
insert into t(b,c,d) values(5,5,5);
example1:我們看到,匹配到了主鍵,在Extra列中,出現Using index的字樣;
mysql> explain select a from t where a>1;
+----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
| 1 | SIMPLE | t | range | PRIMARY | PRIMARY | 4 | NULL | 4 | Using where; Using index |
+----+-------------+-------+-------+---------------+---------+---------+------+------+--------------------------+
example2:我們看到,匹配到了(b_c),覆蓋索引,key是b_c,在Extra列中,出現Using index的字樣
mysql> explain select b,c from t where b>1;
+----+-------------+-------+-------+---------------+------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+------+---------+------+------+--------------------------+
| 1 | SIMPLE | t | range | b_c | b_c | 5 | NULL | 4 | Using where; Using index |
+----+-------------+-------+-------+---------------+------+---------+------+------+--------------------------+
1 row in set (0.00 sec)
example3:雖然查詢條件是b,但是查詢到的欄位沒有b/c而是d,所以key是NULL,沒有用到索引;
mysql> explain select d from t where b>1;
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | t | ALL | b_c | NULL | NULL | NULL | 5 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
example4:返回欄位b c d,查詢條件是b,索引沒有完全覆蓋到返回的欄位。
mysql> explain select b,c,d from t where b>1;
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | t | ALL | b_c | NULL | NULL | NULL | 5 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
example5:沒有覆蓋到索引
mysql> explain select d from t where c>1;
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| 1 | SIMPLE | t | ALL | NULL | NULL | NULL | NULL | 5 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
example6:索引中就包含c列的值,只用到了覆蓋索引,Extra欄位有Using index的字樣;
mysql> explain select c from t where b>1;
+----+-------------+-------+-------+---------------+------+---------+------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+------+---------+------+------+--------------------------+
| 1 | SIMPLE | t | range | b_c | b_c | 5 | NULL | 4 | Using where; Using index |
+----+-------------+-------+-------+---------------+------+---------+------+------+--------------------------+
1 row in set (0.00 sec)
覆蓋所有的概念和意義比較微妙,大家多體會體會。
這就是我今天為大家分享的內容,如果有錯誤的地方,希望指出。如果有其他疑惑,也可以可以留言關注我的微信公眾號或者加筆者微信,隨時交流技術,後續我也會繼續為大家帶來後端技術乾貨,敬請期待,謝謝大家。