面試官:limit 100w,10為什么慢?如何優化?
在 MySQL 中,limit X,Y 的查詢中,X 值越大,那么查詢速度也就越慢,例如以下示例:
- limit 0,10:查詢時間大概在 20 毫秒左右。
- limit 1000000,10:查詢時間可能是 15 秒左右(1秒等于 1000 毫秒),甚至更長時間。
所以,可以看出,limit 中 X 值越大,那么查詢速度都越慢。
這個問題呢其實就是 MySQL 中典型的深度分頁問題。那問題來了,為什么 limit 越往后查詢越慢?如何優化查詢速度呢?
為什么limit越來越慢?
在數據庫查詢中,當使用 LIMIT x, y 分頁查詢時,如果 x 值越大,查詢速度可能會變慢。這主要是因為數據庫需要掃描和跳過 x 條記錄才能返回 y 條結果。隨著 x 的增加,需要掃描和跳過的記錄數也增加,從而導致性能下降。
例如 limit 1000000,10 需要掃描 1000010 行數據,然后丟掉前面的 1000000 行記錄,所以查詢速度就會很慢。
優化手段
對于 MySQL 深度分頁比較典型的優化手段有以下兩種:
- 起始 ID 定位法:使用最后查詢的 ID 作為起始查詢的 ID。
- 索引覆蓋+子查詢。
1.起始ID定位法
起始 ID 定位法指的是 limit 查詢時,指定起始 ID。而這個起始 ID 是上一次查詢的最后一條 ID。例如上一次查詢的最后一條數據的 ID 為 6800000,那我們就從 6800001 開始掃描表,直接跳過前面的 6800000 條數據,這樣查詢的效率就高了,具體實現 SQL 如下:
select name, age, gender
from person
where id > 6800000 -- 核心實現 SQL
order by id limit 10;
其中 id 字段為表的主鍵字段。
為什么起始ID查詢效率高呢?
因此這種查詢是以上一次查詢的最后 ID 作為起始 ID 進行查詢的,而上次的 ID 已經定位到具體的位置了,所以只需要遍歷 B+ 樹葉子節點的雙向鏈表(主鍵索引的底層數據結構)就可以查詢到后面的數據了,所以查詢效率就比較高,如下圖所示:
如果上次查詢結果為 9,之后再查詢時,只需要從 9 之后再遍歷 N 條數據就能查詢出結果了,所以效率就很高。
優缺點分析
這種查詢方式,只適合一頁一頁的數據查詢,例如手機 APP 中刷新聞時那種瀑布流方式。
但如果用戶是跳著分頁的,例如查詢完第 1 頁之后,直接查詢第 250 頁,那么這種實現方式就不行了。
2.索引覆蓋+子查詢
此時我們為了查詢效率,可以使用索引覆蓋加子查詢的方式,具體實現如下。
假設,我們未優化前的 SQL 如下:
select name, age, gender
from person
order by createtime desc
limit 1000000,10;
在以上 SQL 中,createtime 字段創建了索引,但查詢效率依然很慢,因為它要取出 100w 完整的數據,并需要讀取大量的索引頁,和進行頻繁的回表查詢,所以執行效率會很低。
此時,我們可以做以下優化:
SELECT p1.name, p1.age, p1.gender
FROM person p1
JOIN (
SELECT id FROM person ORDER BY createtime desc LIMIT 1000000, 10
) AS p2 ON p1.id = p2.id;
相比于優化前的 SQL,優化后的 SQL 將不需要頻繁回表查詢了,因為子查詢中只查詢主鍵 ID,這時可以使用索引覆蓋來實現。那么子查詢就可以先查詢出一小部分主鍵 ID,再進行查詢,這樣就可以大大提升查詢的效率了。
索引覆蓋(Index Coverage)是一種數據庫查詢優化技術,它指的是在執行查詢時,數據庫引擎可以直接從索引中獲取所有需要的數據,而不需要再回表(訪問主鍵索引或者表中的實際數據行)來獲取額外的信息。這種方式可以減少磁盤 I/O 操作,從而提高查詢性能。