《高性能MySQL》讀后感——B-Tree索引

當人們談論索引的時候,如果沒有特別指明類型,多半說的是B-Tree索引,它使用B-Tree數據結構來存儲數據。實際上很多存儲引擎使用的是B+Tree,即每個葉子節點都包含指向下一個葉子節點的指針,從而方便葉子節點的范圍遍歷。
??常用的存儲引擎以不同的方式使用B-Tree索引,性能也各有不同,各有優劣。MyISAM和InnoDB都使用B+Tree。例如:

  • MyISAM使用前綴壓縮技術使得索引更小,InnoDB則按照原數據格式進行存儲。
  • MyISAM索引通過數據的物理位置引用被索引的行,InnoDB則根據主鍵引用被索引的行。

B-Tree通常意味著所有的值都是按順序存儲的,井且每一個葉子頁到根的距離相同。下面圖1、圖2展示了B-Tree索引的抽象表示,大致反映了InnoDB索引是如何工作的。MyISAM使用的結構有所不同,但基本思想是類似的。

圖1 建立在B-Tree結構(技術上是B+Tree)上的索引
圖2 建立在B-Tree結構(技術上是B+Tree)上的索引

B-Tree索引能夠加快訪問數據的速度,因為存儲引擎不再需要進行全表掃描來獲取需要的數據,取而代之的是從索引的根節點(圖示并未畫出)開始進行搜索。根節點的槽中存放了指向子節點的指針,存儲引擎根據這些指針向下層查找。通過比較節點頁的值和要查找的值可以找到合適的指針進入下層子節點,這些指針實際上定義了子節點頁中值的上限和下限。最終存儲引擎要么是找到對應的值,要么該記錄不存在。
??葉子節點比較特別,它們的指針指向的是被索引的數據,而不是其他的節點頁(不同引擎的“指針”類型不同)。圖1和圖2中僅繪制了一個節點和其對應的葉子節點,其實在根節點和葉子節點之間可能有很多層節點頁。樹的深度和表的大小直接相關。
??B-Tree對索引列是順序組織存儲的,所以很適合查找范圍數據。例如,在一個基于文本域的索引樹上,按字母順序傳遞連續的值進行查找是非常合適的,所以像“找出所有以I到K開頭的名字”這樣的查找效率會非常高。
假設有如下數據表:

CREATE TABLE `People` (
  `last_name` varchar(32) NOT NULL,
  `first_name` varchar(32) NOT NULL,
  `dob` date NOT NULL,
  `gender` enum('m','f') NOT NULL,
  KEY `last_name` (`last_name`,`first_name`,`dob`)
)

對于表中的每一行數據,索引中包含了last_name、first_name和dob列的值,下圖顯示了該索引是如何組織數據的存儲的。

建立在B-Tree結構(技術上是B+Tree)索引樹中的部分條目示例

請注意,索引對多個值進行排序的依據是CREATE TABLE語句中定義索引時列的順序。看一下最后兩個條目,兩個人的姓和名都一樣,則根據他們的出生日期來排列順序。B-Tree索引適用于全鍵值、鍵值范圍或鍵前綴查找。其中鍵前綴查找只適用于根據最左前綴的查找。

全值匹配

全值匹配指的是和索引中的所有列進行匹配,例如前面提到的索引可用于查找姓名為Cuba Allen、出生于1960-01-01的人。

匹配最左前綴

前面提到的索引可用于查找所有姓為Allen的人,即只使用索引的第一列。匹配列前綴也可以只匹配第一列值的開頭部分。例如前面提到的索引可用于查找所有以A開頭的姓的人。

mysql> explain select * from People where last_name = 'Allen';
+----+-------------+--------+------+---------------+-----------+---------+-------+------+-------------+
| id | select_type | table  | type | possible_keys | key       | key_len | ref   | rows | Extra       |
+----+-------------+--------+------+---------------+-----------+---------+-------+------+-------------+
|  1 | SIMPLE      | People | ref  | last_name     | last_name | 98      | const |    1 | Using where |
+----+-------------+--------+------+---------------+-----------+---------+-------+------+-------------+

mysql> explain select * from People where last_name like 'A%';
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+
| id | select_type | table  | type  | possible_keys | key       | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+
|  1 | SIMPLE      | People | range | last_name     | last_name | 98      | NULL |    1 | Using where |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+

如下例子,匹配列前綴不是第一列值的開頭部分,不會用到索引。

mysql> explain select * from People where last_name like '%ll%';
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | People | ALL  | NULL          | NULL | NULL    | NULL |    4 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+

如下例子,當使用中間列first_name查詢時,不會用到索引。

mysql> explain select * from People where first_name = 'Cuba';
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | People | ALL  | NULL          | NULL | NULL    | NULL |    4 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+------+-------------+
匹配范圍值

例如前面提到的索引可用于查找姓在Allen和Barrymore之間的人,這里也只使用了索引的第一列。其實也是匹配最左前綴。

精確匹配到某一列并范圍匹配另外一列

前面提到的索引也可用于查找所有姓為Allen,并且名字是字母C開頭(比如Cuba、Carl等)的人。即第一列last_name全匹配,第二列first_name范圍匹配。

mysql> explain select * from People where last_name = 'Allen' and first_name like 'C%';
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+
| id | select_type | table  | type  | possible_keys | key       | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+
|  1 | SIMPLE      | People | range | last_name     | last_name | 196     | NULL |    1 | Using where |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+
只訪問索引的查詢

B-Tree通常可以支持“只訪問索引的查詢”,即查詢只需要訪問索引,而無須訪問數據行。用Explain查看Extra可以看到Using index,如下所示。

mysql> explain select dob from People where last_name = "Allen" and first_name = 'Cu%';
+----+-------------+--------+------+---------------+-----------+---------+-------------+------+--------------------------+
| id | select_type | table  | type | possible_keys | key       | key_len | ref         | rows | Extra                    |
+----+-------------+--------+------+---------------+-----------+---------+-------------+------+--------------------------+
|  1 | SIMPLE      | People | ref  | last_name     | last_name | 196     | const,const |    1 | Using where; Using index |
+----+-------------+--------+------+---------------+-----------+---------+-------------+------+--------------------------+

因為索引樹中的節點是有序的,所以除了按值查找之外,索引還可以用于查詢中的ORDER BY操作(按順序查找)。如果B-Tree可以按照某種方式查找到值,那么也可以按照這種方式用于排序。所以,如果ORDER BY子句滿足前面列出的幾種查詢類型,則這個索引也可以滿足對應的排序需求。
以第一列last_name ORDER BY,可以使用索引,如下所示。

mysql> explain select dob from People order by last_name;
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+
| id | select_type | table  | type  | possible_keys | key       | key_len | ref  | rows | Extra       |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+
|  1 | SIMPLE      | People | index | NULL          | last_name | 199     | NULL |    4 | Using index |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-------------+

以第一列first_name ORDER BY,出現Using filesort,重新到數據行獲取first_name排序,如下所示。

mysql> explain select dob from People order by first_name;
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-----------------------------+
| id | select_type | table  | type  | possible_keys | key       | key_len | ref  | rows | Extra                       |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-----------------------------+
|  1 | SIMPLE      | People | index | NULL          | last_name | 199     | NULL |    4 | Using index; Using filesort |
+----+-------------+--------+-------+---------------+-----------+---------+------+------+-----------------------------+

下面是一些關于B-Tree索引的限制:

  • 如果不是按照索引的最左列開始查找,則無法使用索引。例如上面例子中的索引不能用于查找名字為Bill的人,也無法查找某個特定生日的人,因為這兩列都不是最左數據列。類似地,也無法查找姓氏以某個字母結尾的人。
  • 不能跳過索引中的列。也就是說,前面所述的索引無法用于查找姓為Smith并且在某個特定日期出生的人。如果不指定名(first_name),則MySQL只能使用索引的第一列。
  • 如果查詢中有某個列的范圍(like between > <都算范圍查詢)查詢,則其右邊所有列都無法使用索引優化查找。例如有查詢WHERE last_name='Smith’AND first_name like '%J%'AND dob=’1976-12-23',這個查詢只能使用索引的前兩列,因為這里的like是一個范圍條件(但是服務器可以把其余列用于其他目的),并且first_name不是最左開始查找,索引也不能用first_name。如果范圍查詢列值的數量有限,那么可以通過使用多個等于條件來代替范圍條件。

所以前面提到的索引列的順序是多么的重要:這些限制都和索引列的順序有關。在優化性能的時候,可能需要使用相同的列但順序不同的索引來滿足不同類型的查詢需求。

索引的優點

最常見的B-Tree索引,按照順序存儲數據,所以MySQL可以用來做ORDER BY和GROUP BY操作。因為數據是有序的,所以B-Tree也就會將相關的列值存儲在一起。最后,因為索引中存儲了實際的列值,所以某些查詢只使用索引就能夠完成全部查詢。總結下來索引有如下三個優點:

  • 索引大大減少了服務器需要掃描的數據量。
  • 索引可以幫助服務器避免排序和臨時表。
  • 索引可以將隨機I/O變為順序I/O。
三星索引理論

Lahdenmaki和Leach的三星索引理論:

  • 一星:索引將相關的記錄放到一起。
  • 二星:索引中的數據順序和查找中的排列順序一致。
  • 三星:索引中的列包含了查詢中需要的全部列。

表shop結構:

mysql> desc shop;
+----------+---------------+------+-----+---------+----------------+
| Field    | Type          | Null | Key | Default | Extra          |
+----------+---------------+------+-----+---------+----------------+
| id       | int(11)       | NO   | PRI | NULL    | auto_increment |
| shop_id  | int(11)       | NO   | MUL | NULL    |                |
| goods_id | int(11)       | NO   |     | NULL    |                |
| pay_type | tinyint(1)    | NO   | MUL | NULL    |                |
| price    | decimal(10,2) | NO   | MUL | NULL    |                |
| comment  | varchar(4000) | YES  | MUL | NULL    |                |
+----------+---------------+------+-----+---------+----------------+

查詢下面的SQL:

select pay_type, price from shop where shop_id = 2 and goods_id = 2 order by price

沒有創建任何索引之前,Explain結果:

explain select pay_type, price from shop where shop_id = 2 and goods_id = 2 order by price\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: shop
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 20029
        Extra: Using where; Using filesort
第一步構建一星索引

根據where后面等值條件,或者范圍條件來構建索引,即index(shop_id,goods_id) ,如下所示,用explain查看上面SQL使用這個索引。SQL優化一般都說索引是為了能以最快的速度定位到想要的數據,即用空間來換時間,快速定位想要的數據后,也就過濾掉了不必要的數據,所以一星索引的核心就是利用索引來盡可能的過濾不必要的數據,減少數據處理的規模,對于RDBMS來說是極為關鍵的,比如說shop表有1000000行,shop_id的過濾度是10%,goods_id的過濾度是0.1%,如果沒有索引,數據庫不得不把表里所有的一百萬行數據都讀出來,做處理,但是如果有了這個一星索引,需要處理的數據被極大的縮小,只需要根據索引找到符合條件的索引葉子節點的范圍,讀取0.1%10%1000000=100 rows就可以了,哪怕我們樂觀的假定產生的都是邏輯IO, 而不是物理IO,單次的差別就已經很明顯,更別說是執行頻率很高的時候,我們線上很多爛SQL對DB造成了影響,一看機器邏輯讀都好幾百萬,基本上可以定位是SQL索引缺失,或者索引不合理造成的。

mysql> ALTER TABLE shop add index shop_id(`shop_id`, `goods_id`);

mysql> explain select pay_type, price from shop where shop_id = 2 and goods_id = 2 order by price\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: shop
         type: ref
possible_keys: shop_id
          key: shop_id
      key_len: 8
          ref: const,const
         rows: 19
        Extra: Using where; Using filesort
第二步構建二星索引

針對上面的case,我們構建索引如下index(shop_id, goods_id, price),用explain查看上面SQL已經沒有Using filesort基本的原理就是利用索引的有序性,把消除order by或者group by等需要排序的操作,因為大家都知道排序是非常消耗CPU資源的,大量的排序操作會把CPU搞得很高,即使CPU吃得消,如果數據量比較大,需要排序的數據放不下內存的sort buffer,只能悲劇的和外存換進換出,性能下降的就不是一點兩點,這時候利用索引避免排序的優勢就明顯的體現出來了。

mysql> ALTER TABLE shop drop index shop_id;

mysql> ALTER TABLE shop add index shop_id(`shop_id`, `goods_id`, `price`);

mysql> explain select pay_type, price from shop where shop_id = 2 and goods_id = 2 order by price\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: shop
         type: ref
possible_keys: shop_id
          key: shop_id
      key_len: 8
          ref: const,const
         rows: 19
        Extra: Using where
第三步構建三星索引

如下index(shop_id, goods_id, price,pay_type), 跟之前的二星索引的差別在于,在索引中額外添加了要查詢的列pay_type,這就是所謂的索引覆蓋,用explain查看上面SQL有Using index,即在索引的葉子節點就能夠讀到查詢SQL所需要的所有信息,而不需要回原表去查詢,在目前內存如此充足的情況下,很多時候,除了root節點和branch結構,甚至整個索引都是可以被放入內存的,這樣能大概率的避免,至少是減少物理IO。

mysql> ALTER TABLE shop drop index shop_id;

mysql> ALTER TABLE shop add index shop_id(`shop_id`, `goods_id`, `price`, `pay_type`);

mysql> explain select pay_type, price from shop where shop_id = 2 and goods_id = 2 order by price\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: shop
         type: ref
possible_keys: shop_id
          key: shop_id
      key_len: 8
          ref: const,const
         rows: 19
        Extra: Using where; Using index
注意:索引是最好的解決方案嗎?

索引并不總是最好的工具。索引其實是一種權衡,是一種拿空間來換時間的藝術。總的來說,只有當索引幫助存儲引擎快速查找到記錄帶來的好處大于其帶來的額外工作時,索引才是有效的。對于非常小的表,大部分情況下簡單的全表掃描更高效。對于中到大表,索引就非常有效。但對于特大型的表,建立和使用索引的代價將隨之增長。這種情況下,則需要一種技術可以直接區分出需要查詢的一組數據,而不是一條記錄一條記錄的匹配。例如可以使用分區技術。
??如果表的數量特別多,可以建立一個元數據信息表,用來查詢需要用到的某些特性。例如執行那些需要聚合多個應用分布在多個表的數據的查詢,則需要記錄“哪個用戶的信息存儲在哪個表中”的元數據,這樣在查詢時就可以直接忽略那些不包含指定用戶信息的表。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容