Mysql優化(十)什么是 MySQL 的 回表 ?怎么減少回表的次數?


索引優化

索引結構

要搞明白這個問題,需要大家首先明白 MySQL 中索引存儲的數據結構。這個其實很多小伙伴可能也都聽說過,B+Tree 嘛!

B+Tree 是什么?那你得先明白什么是 B-Tree,來看如下一張圖:

img

前面是 B-Tree,后面是 B+Tree,兩者的區別在於:

  1. B-Tree 中,所有節點都會帶有指向具體記錄的指針;B+Tree 中只有葉子結點會帶有指向具體記錄的指針。
  2. B-Tree 中不同的葉子之間沒有連在一起;B+Tree 中所有的葉子結點通過指針連接在一起。
  3. B-Tree 中可能在非葉子結點就拿到了指向具體記錄的指針,搜索效率不穩定;B+Tree 中,一定要到葉子結點中才可以獲取到具體記錄的指針,搜索效率穩定。

基於上面兩點分析,我們可以得出如下結論:

  1. B+Tree 中,由於非葉子結點不帶有指向具體記錄的指針,所以非葉子結點中可以存儲更多的索引項,這樣就可以有效降低樹的高度,進而提高搜索的效率。
  2. B+Tree 中,葉子結點通過指針連接在一起,這樣如果有范圍掃描的需求,那么實現起來將非常容易,而對於 B-Tree,范圍掃描則需要不停的在葉子結點和非葉子結點之間移動。

對於第一點,一個 B+Tree 可以存多少條數據呢?以主鍵索引的 B+Tree 為例(二級索引存儲數據量的計算原理類似,但是葉子節點和非葉子節點上存儲的數據格式略有差異),我們可以簡單算一下。

計算機在存儲數據的時候,最小存儲單元是扇區,一個扇區的大小是 512 字節,而文件系統(例如 XFS/EXT4)最小單元是塊,一個塊的大小是 4KB。InnoDB 引擎存儲數據的時候,是以頁為單位的,每個數據頁的大小默認是 16KB,即四個塊。

基於這樣的知識儲備,我們可以大致算一下一個 B+Tree 能存多少數據。

假設數據庫中一條記錄是 1KB,那么一個頁就可以存 16 條數據(葉子結點);對於非葉子結點存儲的則是主鍵值+指針,在 InnoDB 中,一個指針的大小是 6 個字節,假設我們的主鍵是 bigint ,那么主鍵占 8 個字節,當然還有其他一些頭信息也會占用字節我們這里就不考慮了,我們大概算一下,小伙伴們心里有數即可:

16*1024/(8+6)=1170

即一個非葉子結點可以指向 1170 個頁,那么一個三層的 B+Tree 可以存儲的數據量為:

1170*1170*16=21902400

可以存儲 2100萬 條數據。

在 InnoDB 存儲引擎中,B+Tree 的高度一般為 2-4 層,這就可以滿足千萬級的數據的存儲,查找數據的時候,一次頁的查找代表一次 IO,那我們通過主鍵索引查詢的時候,其實最多只需要 2-4 次 IO 操作就可以了。

大家先搞明白這個 B+Tree。

兩類索引

大家知道,MySQL 中的索引有很多中不同的分類方式,可以按照數據結構分,可以按照邏輯角度分,也可以按照物理存儲分,其中,按照物理存儲方式,可以分為聚簇索引和非聚簇索引。

我們日常所說的主鍵索引,其實就是聚簇索引(Clustered Index);主鍵索引之外,其他的都稱之為非主鍵索引,非主鍵索引也被稱為二級索引(Secondary Index),或者叫作輔助索引。

對於主鍵索引和非主鍵索引,使用的數據結構都是 B+Tree,唯一的區別在於葉子結點中存儲的內容不同:

  • 主鍵索引的葉子結點存儲的是一行完整的數據
  • 非主鍵索引的葉子結點存儲的則是主鍵值。葉子結點不包含行記錄的全部數據;非主鍵的葉子結點中,除了用來排序的key還包含一個bookmark;該書簽存儲了聚集索引的key。

這就是兩者最大的區別。

所以,當我們需要查詢的時候:

  1. 如果是通過主鍵索引來查詢數據,例如 select * from user where id=100,那么此時只需要搜索主鍵索引的 B+Tree 就可以找到數據。
  2. 如果是通過非主鍵索引來查詢數據,例如 select * from user where username='javaboy',那么此時需要先搜索 username 這一列索引的 B+Tree,搜索完成后得到主鍵的值,然后再去搜索主鍵索引的 B+Tree,就可以獲取到一行完整的數據。

對於第二種查詢方式而言,一共搜索了兩棵 B+Tree,第一次搜索 B+Tree 拿到主鍵值后再去搜索主鍵索引的 B+Tree,這個過程就是所謂的回表。

從上面的分析中我們也能看出,通過非主鍵索引查詢要掃描兩棵 B+Tree,而通過主鍵索引查詢只需要掃描一棵 B+Tree,所以如果條件允許,還是建議在查詢中優先選擇通過主鍵索引進行搜索。

眾所周知在InnoDB引用的是B+樹索引模型,這里對B+樹結構暫時不做過多闡述,很多文章都有描述,在第二問中我們對索引的種類划分為兩大類主鍵索引和非主鍵索引,那么問題就在於比較兩種索引的區別了,我們這里建立一張學生表,其中包含字段id設置主鍵索引、name設置普通索引、age(無處理),並向數據庫中插入4條數據:("小趙", 10)("小王", 11)("小李", 12)("小陳", 13)

create table `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主鍵',
  `name` varchar(32) COLLATE utf8_bin NOT NULL COMMENT '名稱',
  `age` int(3) unsigned NOT NULL DEFAULT '1' COMMENT '年齡',
  primary key (`id`),
  KEY `I_name` (`name`)
) ENGINE=InnoDB;

INSERT INTO student (name, age) VALUES("小趙", 10),("小王", 11),("小李", 12),("小陳", 13);

這里我們設置了主鍵為自增,那么此時數據庫里數據為

image-20220306184151569

每一個索引在 InnoDB 里面對應一棵B+樹,那么此時就存着兩棵B+樹。

img

可以發現區別在與葉子節點中主鍵索引存儲了整行數據,而非主鍵索引中存儲的值為主鍵id, 在我們執行如下sql后

SELECT age FROM student WHERE name = '小李';

流程為:

  1. name索引樹上找到名稱為小李的節點 id為 03
  2. id索引樹上找到id為 03的節點 獲取所有數據
  3. 從數據中獲取字段命為age的值返回 12

在流程中從非主鍵索引樹搜索回到主鍵索引樹搜索的過程稱為:回表,在本次查詢中因為查詢結果只存在主鍵索引樹中,我們必須回表才能查詢到結果,那么如何優化這個過程呢?引入正文覆蓋索引

覆蓋索引

就是把單列的非主鍵 索引 修改為 多字段 的聯合索引, 在一棵索引數上。 就找到了想要的數據, 不需要去主鍵索引樹上,再檢索一遍 這個現象,稱之為 索引覆蓋.

覆蓋索引(covering index ,或稱為索引覆蓋)即從非主鍵索引中就能查到的記錄,而不需要查詢主鍵索引中的記錄,避免了回表的產生減少了樹的搜索次數,顯著提升性能。

  • 如何使用是覆蓋索引?

之前我們已經建立了表student,那么現在出現的業務需求中要求根據名稱獲取學生的年齡,並且該搜索場景非常頻繁,那么先在我們刪除掉之前以字段name建立的普通索引,nameage兩個字段建立聯合索引,sql命令與建立后的索引樹結構如下

# 刪除之前的非主鍵索引
alter table student drop index I_name;
# 添加非主鍵索引
alter table student add index I_name_age(name, age);

img

那在我們再次執行如下sql后:

select age from student where name = '小李';

流程為:

  1. name,age聯合索引樹上找到名稱為小李的節點
  2. 此時節點索引(非主鍵索引)里包含信息age 直接返回 12
  • 如何確定數據庫成功使用了覆蓋索引呢?

當發起一個索引覆蓋查詢時,在explainextra列可以看到using index的信息:

img

這里我們很清楚的看到Extrausing index表明我們成功使用了覆蓋索引。

覆蓋索引避免了回表現象的產生,從而減少樹的搜索次數,顯著提升查詢性能,所以使用覆蓋索引是性能優化的一種手段。

  • 那么不用主鍵索引就一定需要回表嗎?

不一定!

如果查詢的列本身就存在於索引中,那么即使使用二級索引,一樣也是不需要回表的。

舉個例子,我有如下一張表:

image-20220306183637330

unameaddress 字段組成了一個復合索引,那么此時,雖然這是一個非主鍵索引,但是索引樹的葉子節點中除了保存主鍵值,也保存了 address 的值。

我們來看如下分析:

explain select uname,address from user where uname='javaboy';
image-20220306183854336

可以看到,此時使用到了 uname 索引,但是最后的 Extra 的值為 Using index,這就表示用到了索引覆蓋掃描(覆蓋索引),此時直接從索引中過濾不需要的記錄並返回命中的結果,這一步是在 MySQL 服務器層完成的,並且不需要回表。

哪些場景可以利用索引覆蓋來優化SQL?

  1. 全表count查詢優化

img

直接:

 select count(name) from user;
 不能利用索引覆蓋。

添加索引:

 alter table user add key(name);
 就能夠利用索引覆蓋提效。
  1. 列查詢回表優化

這個例子不再贅述,將單列索引(name)升級為聯合索引(name, sex),即可避免回表。

  1. 分頁查詢

將單列索引(name)升級為聯合索引(name, sex),也可以避免回表。

如何創建有效的索引

  1. 如果需要索引很長的字符串,此時需要考慮前綴索引

前綴索引即選擇所需字符串的一部分前綴作為索引,這時候,需要引入一個概念叫做索引選擇性索引選擇性是指不重復的索引值與數據表的記錄總數的比值,可以看出索引選擇性越高則查詢效率越高,當索引選擇性為1時,效率是最高的,但是在這種場景下,很明顯索引選擇性為1的話我們會付出比較高的代價,索引會很大,這時候我們就需要選擇字符串的一部分前綴作為索引,通常情況下一列的前綴作為索引選擇性也是很高的

如何選擇前綴

  • 計算該列完整列的選擇性,使得前綴選擇性接近於完整列的選擇性
  1. 使用多列索引

盡量不要為多列上創建單列索引,因為這樣的情況下最多只能使用一星索引,這樣的話,不如去創建一個全覆蓋索引,在多列上創建單列索引大部分情況下並不能提高 MySQL 的查詢性能,MySQL 5.0 中引入了合並索引,在一定程度上可以表內多個單列索引來定位指定的結果,但是 5.0 以前的版本,如果 where 中的多個條件是基於多個單列索引,那么 MySQL 是無法使用這些索引的,這種情況下,還不如使用 union

  1. 選擇合適的索引列順序

經驗是將選擇性最高的列放到索引最前列,可以在查詢的時候過濾出更少的結果集。

但這樣並不總是最好的,如果考慮到 group by 或者 order by 等情況,再比如考慮到一些特別場景下的 guest 賬號等數據情況,上面的經驗法則可能就不是最適用的

  1. 覆蓋索引

所謂覆蓋索引就是指索引中包含了查詢中的所有字段,這種情況下就不需要再進行回表查詢了

覆蓋索引對於 MyISAM 和 InnoDB 都非常有效,可以減少系統調用和數據拷貝等時間.

Tips:減少 select * 操作

  1. 使用索引掃描來做排序

MySQL 生成有序的結果有兩種方法:通過排序操作,或者按照索引順序掃描;使用排序操作需要占用大量的 CPU 和內存資源,而使用 index 性能是很好的,所以,當我們查詢有序結果時,盡量使用索引順序掃描來生成有序結果集。

怎樣保證使用索引順序掃描?

  • 索引 列 順序和 ORDER BY 順序一致
  • 所有列的排序方向一致
  • 如果關聯多表,那么只有當 ORDER BY 子句引用的字段全部為第一張表時,才能使用索引做排序,限制依然是需要滿足索引的最左前綴要求
  1. 壓縮索引

MyISAM 中使用了前綴壓縮技術,會減少索引的大小,可以在內存中存儲更多的索引,這部分優化默認也是只針對字符串的,但是可以自定義對整數做壓縮

這個優化在一定情況下性能比較好,但是對於某些情況可能會導致更慢,因為前綴壓縮決定了每個關鍵字都必須依賴於前面的值,所以無法使用二分查找等,只能順序掃描,所以如果查找的是逆序那么性能可能不佳

  1. 減少重復、冗余以及未使用的索引

MySQL 的唯一限制和主鍵限制都是通過索引實現的,所以不需要在同一列上增加主鍵、唯一限制再創建索引,這樣是重復索引

再舉個例子,如果已經創建了索引(A,B),那么再創建索引(A)的話,就屬於重復索引,因為 MySQL 索引是最左前綴,所以索引(A,B)本身就可以使用索引(A),但是創建索引(B)的話不屬於重復索引

盡量減少新增索引,而應該擴展已有的索引,因為新增索引可能會導致 INSERT、UPDATE、DELETE 等操作更慢

可以考慮刪除沒有使用到的索引,定位未使用的索引,有兩個辦法,在 Percona Server 或者 MariaDB 中打開 userstates 服務器變量,然后等服務器運行一段時間后,通過查詢 INFORMATION_SCHEMA.INDEX_STATISTICS 就可以查詢到每個索引的使用頻率

  1. 索引和鎖

InnoDB 支持行鎖和表鎖,默認使用行鎖,而 MyISAM 使用的是表鎖,所以使用索引可以讓查詢鎖定更少的行,這樣也會提升查詢的性能,如果查詢中鎖定了1000行,但實際只是用了100行,那么在 5.1 之前都需要提交事務之后才能釋放這些鎖,5.1 之后可以在服務器端過濾掉行之后就釋放鎖,不過依然會導致一些鎖沖突

  1. 減少索引和數據碎片
  • 首先我們需要了解一下為什么會產生碎片,比如 InnoDB 刪除數據時,這一段空間就會被留空,如果一段時間內大量刪除數據,就會導致留空的空間比實際的存儲空間還要大,這時候如果進行新的插入操作時,MySQL 會嘗試重新使用這部分空間,但是依然無法徹底占用,這樣就會產生碎片
  • 產生碎片帶來的后果當然是,降低查詢性能,因為這種情況會導致隨機磁盤訪問
  • 可以通過 OPTIMIZE TABLE 或者重新導入數據表來整理數據

什么是索引下推

假設有這么個需求,查詢表中“名字第一個字是張,性別男,年齡為10歲的所有記錄”。那么,查詢語句是這么寫的:

 select * from tuser where name like '張%' and age=10 and ismale=1;

根據前面說的“最左前綴原則”,該語句在搜索索引樹的時候,只能匹配到名字第一個字是‘張’的記錄(即記錄ID3),接下來是怎么處理的呢?

當然就是從ID3開始,逐個回表,到主鍵索引上找出相應的記錄,再比對ageismale這兩個字段的值是否符合。

但是!MySQL 5.6引入了索引下推優化,可以在索引遍歷過程中,對索引中包含的字段先做判斷,過濾掉不符合條件的記錄,減少回表字數
下面圖1、圖2分別展示這兩種情況。

img img

圖 1 中,在 (name,age) 索引里面我特意去掉了 age 的值,這個過程 InnoDB 並不會去看 age 的值,只是按順序把“name 第一個字是’’”的記錄一條條取出來回表。因此,需要回表 4 次。

圖 2 跟圖 1 的區別是,InnoDB 在 (name,age) 索引內部就判斷了 age 是否等於 10,對於不等於 10 的記錄,直接判斷並跳過。在我們的這個例子中,只需要對 ID4、ID5 這兩條記錄回表取數據判斷,就只需要回表 2 次。

如果沒有索引下推優化(或稱ICP優化),當進行索引查詢時,首先根據索引來查找記錄,然后再根據where條件來過濾記錄;在支持ICP優化后,MySQL會在取出索引的同時,判斷是否可以進行where條件過濾再進行索引查詢,也就是說提前執行where的部分過濾操作,在某些場景下,可以大大減少回表次數,從而提升整體性能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM