原文出處,刪除與知識無關的作者個人經歷和感想部分
葉不聞《寫在教師節:分頁場景(limit,offset)為什么會慢》
鏈接:https://juejin.im/post/5c4db295e51d4503834d9c43
邏輯算子部分引用了
叄金《SQL優化器執行過程之邏輯算子》
鏈接:http://www.imooc.com/article/278660
問題分析
select * from table where status = xx limit 10 offset 100000;
在分頁場景下,即使有索引,limit請求也會非常慢,在數據量只有10萬的情況下,單機大概2-3秒
索引
我們知道MySQL的索引是b+樹。如果是一個二叉樹,因為無法知道前100個數在樹上的分布情況,所以無法利用二分查找的特性。在b+樹中,可以通過葉子節點組成的鏈表,以O(n)
的復雜度查找到第100大的數,但即使是O(n),也不至於這么慢,是否還有其他原因
通過查閱資料得知,InnoDB的索引分為兩種
- 聚簇索引:包含主鍵索引和對應的實際數據,索引的葉子節點就是數據節點,找到索引也就找到了數據
- 輔助索引:可以理解為二級節點,其葉子節點還是索引節點,包含了主鍵id,還需要查詢一遍數據
由於MySQL的分層的原因,即使前100000個會扔掉,MySQL也會通過二級索引上的主鍵id,去聚簇索引上查一遍數據,這可是100000次隨機IO,自然慢成哈士奇
分層
了解這個之前需要先了解一個概念,邏輯算子。簡單的介紹一下查詢計划中的一些邏輯算子
- DataSource:數據源,也就是我們SQL語句中的表。
select name from table1
中的table1 - Join:連接,如
select * from table1 table2 where table1.name = table2.name
就是把兩個表做Join。連接條件是最簡單的等值連接,當然還有其他我們熟知的inner join
,left join
,right join
等等 - Selection:選擇,如
select name from table1 where id = 1
中的where后的過濾條件 - Aggregation:分組,如
select sum(score) from table1 group by name
中的group by。按照某些列進行分組,分組后可以進行一些聚合操作,比如Max、Min、Sum、Count、Average等等 - Projection:投影,指搜索的列,如
select name from table1 where id = 1
中的列name - Sort:排序,如
select * from table1 order by id
里面的order by。無序的數據通過這個算子處理后,輸出有序的數據 - Apply:子查詢,如
select * from (select id,name from table1) as t
中的(select id,name from table1) as t
。可以進行嵌套查詢。
選擇、投影、連接就是最基本的算子,其中 Join 有內連接,左外右外連接等多種連接方式
select b from t1, t2 where t1.c = t2.c and t1.a > 5
變成邏輯查詢計划之后
- t1 t2 對應的 DataSource,負責將數據撈上來
- 上面接個 Join 算子,將兩個表的結果按 t1.c = t2.c連接
- 再按 t1.a > 5 做一個 Selection 過濾
- 最后將 b 列投影
下圖是未經優化的表示,所以說不是mysql不想把limit傳遞給引擎層,而是因為划分了邏輯算子,所以導致無法直到具體算子包含了多少符合條件的數據
ps:SELECT執行順序
SQL語句執行順序 | MySQL執行順序 | |
---|---|---|
1 | select distinct | from |
2 | from | on |
3 | join | join |
4 | on | where |
5 | where | group by |
6 | group by | having+聚合函數 |
7 | having | select distinct |
8 | union | union |
9 | order by | order by |
10 | limit | limit |
解決方法
解決方法有2種
- 根據業務實際需求,看能否替換為下一頁,上一頁的功能,特別在移動端。把limit替換成
>id
的方式。該id再調用時,需要返回給前端。但是這種有些業務場景不適用
select * from table where status = xx id > 100000 limit 10;
- 【推薦】嵌套子查詢,先查找數據的主鍵值,因為主鍵在輔助索引上就有,所以不用回歸到聚簇索引的磁盤去拉取。再通過這些已經被limit出來的10個主鍵id,去查詢聚簇索引。這樣只會十次隨機IO。在業務確實需要用分頁的情況下,使用該方案可以大幅度提高性能。通常能滿足性能要求
select xxx from in (select id from table where status = xx limit 10 offset 100000);