MySQL中的回表查詢與索引覆蓋:一次百萬級別分頁查詢使用Limit 從90秒到0.6毫秒的優化

事故現場

數據庫使用的MySQL,有一個日誌表,需要進行分頁查詢,於是很容易就想到了limit [offset偏移量] [count數量]這個查詢方式,當我們偏移量比較小時,似乎是沒什麼問題

SELECT	* FROM	t_log WHERE	type = 1 LIMIT 5, 50
查詢時間:0.45s

但是隨着offset的增加,就出現了查詢時間越來越長,但是每次查出的數據都只有50條,這就讓我特別不理解

SELECT	* FROM	t_log WHERE	type = 1 LIMIT 500000, 50
查詢時間:57.252s
SELECT	* FROM	t_log WHERE	type = 1 LIMIT 1000000, 50
查詢時間:89.15s

解決方案

查閱資料發現“limit”的工作方式是:
第一步.先查詢offset+count條數據;
第二步.再拋棄前offset條數據
但是全字段查詢肯定會有回表查詢操作,這就導致了進行百萬次的回表查詢,速度肯定會很慢,於是我的解決思路是,在“第一步”時不進行回表查詢,這樣會不會效率提高很多,於是把sql改成下面的等效查詢。

SELECT	*
FROM	t_log t
RIGHT JOIN (
	SELECT uid
	FROM t_log
	WHERE type = 1
	LIMIT 1000000,50
) tmp ON tmp.uid = t.uid
查詢時間:0.64

這下時間縮短了一百倍多,查出來的結果也是正確的,達到了我們要的效果,到此sql已經優化好了。

提到的“回表查詢”

上面說到“第一步”中進行了回表查詢,什麼是回表查詢呢?

InnoDB的索引

這裏我們不得不先解釋一下InnoDB的索引,要從InnoDB的索引實現說起。InnoDB有兩大類索引,一類是聚集索引(Clustered Index),一類是普通索引(Secondary Index)
InnoDB聚集索引和普通索引有什麼差異?有什麼區別呢?
InnoDB的聚集索引
每行數據是存在InnoDB聚集索引的葉子節點上的,因此InnoDB必須要有且只有一個聚集索引,下面聚集索引的生成規則:

  1. 如果表定義了PK(Primary Key,主鍵),那麼PK就是聚集索引。
  2. 如果表沒有定義PK,則第一個NOT NULL UNIQUE的列就是聚集索引。
  3. 否則InnoDB會另外創建一個隱藏的ROWID作爲聚集索引。

這種機制使得基於PK的查詢速度非常快,因爲直接定位的行記錄。
InnoDB普通索引
InnoDB普通索引的葉子節點存儲主鍵值。想拿到行數據,還得去聚集索引中掃描索引樹。
注意,不是存儲行記錄頭指針,MyISAM的索引葉子節點存儲記錄指針。

什麼是回表查詢

下面舉個例子解釋:
假設有個表user(id PK,name,code),id是聚集索引,code是普通索引。表中有幾條數據。

id name code -----
1 小明 AQ -----
4 小陳 DR -----
7 小紅 CY -----
9 小劉 FP -----

那麼兩種索引的B+樹索引就是如下圖這樣:
在這裏插入圖片描述
從普通索引無法直接定位行記錄,那普通索引的查詢過程是怎麼樣的呢?

例:select * from user where code = 'CY';

其查詢過程在通常情況下是需要掃描兩遍索引樹的,這裏的執行過程是這樣的:
在這裏插入圖片描述
如帶色的路徑:

  1. 第一遍先通過普通索引定位到主鍵值
  2. 然後第二遍再通過聚集索引定位到具體行記錄。
    這就是所謂的**回表查詢**,即先定位主鍵值,再根據主鍵值定位行記錄,性能相對於只掃描一遍聚集索引樹的性能要低一些。

怎麼優化回表查詢

那怎麼樣解決這個性能低的問題呢?這就涉及到一個概念---------覆蓋索引

覆蓋索引
覆蓋索引就是是一種避免回表查詢的優化策略。就是把所有需要查詢的字段都放到普通索引中,這樣普通索引查到的葉子結點(即上圖中的黑色方框)中已經能夠得到所需的所有字段,就不會再去聚集索引中再查詢。
實現覆蓋索引的方式
可分爲兩種:

  1. 第一種減少查詢字段只查詢縮影有的字段,例如我們上面提到的使用limit查詢,我們只查了id字段,這樣幾百萬條數據就不會回表查詢,外層查詢時只有50條數據去聚集索引裏進行了查詢。又如上面的user表 優化sql爲不查詢name字段。
例:select id,code from user where code = 'CY';
  1. 第二種方式就是修改表創建的索引,增加需要查詢的字段,如上面user表,把name也加到索引中,設置(name,code)兩個字段的聯合索引 。
    覆蓋索引的定義與注意事項

如果一個索引覆蓋(包含)了所有需要查詢的字段的值,這個索引就是覆蓋索引。因爲索引中已經包含了要查詢的字段的值,因此查詢的時候直接返回索引中的字段值就可以了,不需要再到表中查詢,避免了對主鍵索引的二次查詢,也就提高了查詢的效率。

要注意的是,不是所有類型的索引都可以成爲覆蓋索引的。因爲覆蓋索引必須要存儲索引的列值,而哈希索引、空間索引和全文索引等都不存儲索引列值,索引MySQL只能使用B-Tree索引做覆蓋索引。

另外,當發起一個被索引覆蓋的查詢(索引覆蓋查詢)時,在explain(執行計劃)的Extra列可以看到【Using Index】的信息。

覆蓋索引的優點

1.索引條目通常遠小於數據行的大小,因爲覆蓋索引只需要讀取索引,極大地減少了數據的訪問量。

2.索引是按照列值順序存儲的,對於IO密集的範圍查找會比隨機從磁盤讀取每一行數據的IO小很多。

3.一些存儲引擎比如MyISAM在內存中只緩存索引,數據則依賴操作系統來緩存,因此要訪問數據的話需要一次系統調用,使用覆蓋索引則避免了這一點。

4.由於InnoDB的聚簇索引,覆蓋索引對InnoDB引擎下的數據庫表特別有用。因爲InnoDB的二級索引在葉子節點中保存了行的主鍵值,如果二級索引能夠覆蓋查詢,就避免了對主鍵索引的二次查詢。

關注公衆號,每天進步一點點!
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章