1. 程式人生 > >MySQL InnoDB儲存引擎體系架構 —— 索引高階

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)

覆蓋所有的概念和意義比較微妙,大家多體會體會。

這就是我今天為大家分享的內容,如果有錯誤的地方,希望指出。如果有其他疑惑,也可以可以留言關注我的微信公眾號或者加筆者微信,隨時交流技術,後續我也會繼續為大家帶來後端技術乾貨,敬請期待,謝謝大家。