四種不同的分頁解決方案,你學(xué)會了嗎?
”使用mysql limit 分頁就行了,分頁查詢用得著四種寫法嗎? "
這可能是很多人的想法。的確mysql limit offset是可以勝任分頁的,但是另外三種辦法在其他場景表現(xiàn)更好。
大家最熟悉的就是如下的分頁截圖,返回總頁數(shù)、支持頁數(shù)跳轉(zhuǎn)。
圖片
1 Limit Offset分頁
例如每頁10條,查詢第三頁 ,mysql limit 部分為:limit 20,10;
前段每次需要指定 每頁數(shù)量,當(dāng)前頁數(shù)。由后端拼接查詢SQL,構(gòu)建mysql limit 子句。
limit offset 分頁有幾個特性。
- 支持頁數(shù)跳轉(zhuǎn)。用戶選定第幾頁,就跳轉(zhuǎn)到對應(yīng)的頁面。
- 返回記錄總條數(shù)。用戶可以看到共幾頁,一共多少條數(shù)據(jù)。
limit offset 實現(xiàn)簡單,但是存在缺陷。當(dāng)出現(xiàn)深度分頁時,MySQL 需要掃描大量數(shù)據(jù)才能找到指定頁的數(shù)據(jù),造成慢查詢 ,增加增加數(shù)據(jù)庫的內(nèi)存和cpu負(fù)載, 如果這個深度分頁的QPS比較高,無疑最終會拖垮數(shù)據(jù)庫。在流量高峰期,如果深度分頁的慢查詢較多,毫無疑問,會增加其他SQL耗時,影響其他業(yè)務(wù)場景。
值得說明的是,分頁查詢必須指定排序方式。如果沒有指定排序方式,使用分頁很難保證數(shù)據(jù)不會出現(xiàn)重復(fù)。 如果實在沒有排序字段,可以使用主鍵ID。
我曾經(jīng)犯過類似錯誤,在使用ElasticSearch替換lucene 做檢索時,發(fā)現(xiàn)lucene和ElasticSearch返回的結(jié)果一直不一致,排查了很久,才意識到必須指定排序方式,否則使用分頁查詢會導(dǎo)致數(shù)據(jù)重復(fù)。
那么Limit Offset就沒有其他方式避免深度分頁嗎?答案是可以
2 Limit 指定主鍵Id過濾
如果在查詢條件上加上主鍵Id是不是就可以了呢?
改進前:
select * from students where xxxx查詢條件xxx order by id desc limit 1000,20;
改進后:
select * from students where xxxx查詢條件xxx AND id <lastMinId order by id desc limit 20;
改進后在原有的查詢條件上 指定了lastMinId,上一輪最小的Id。在查詢下一頁時,把上一頁的最小id 傳下去,這樣保證后續(xù)查到的列表都是小于lastMinId。從源頭上增加了查詢條件,減少了mysql的檢索范圍,每次都只獲取前二十條數(shù)據(jù)。
這樣就高枕無憂了嗎?當(dāng)然不
這種方式前提條件是排序方式可以指定主鍵Id,如果根據(jù)其他排序方式,就不能這樣做了。
這種方式還有其他應(yīng)用場景嗎?最佳的場景就是從下游批量獲取大量數(shù)據(jù)時,可以根據(jù)主鍵id進行排序,每次選擇最大的N條,或最小的N條。
每次查詢都更新主鍵id范圍,這樣就能避免深度分頁,查詢?nèi)康臄?shù)據(jù)。
3 HasMore 滾動查詢
有的業(yè)務(wù)場景例如用戶App端的購買記錄頁,用戶只能每頁滾動查詢購買記錄,無需知道購買訂單總數(shù)。針對這個場景,有什么優(yōu)化呢?
在之前的limit Offset分頁時,需要返回記錄總數(shù),前端也要確定查詢總頁數(shù)。滾動分頁查詢則無需獲取總頁數(shù),無需查詢總數(shù)。減少了一次select count(*)的查詢。
只需要在每一次分頁查詢時,每頁數(shù)量+1 即可。例如每頁10條,可以指定11條,如果真查出來11條,hasMore=true,上游需要繼續(xù)查,否則hasMore=false,上游無需再分頁查詢。
4 ElasticSearch 分頁查詢
ES 比較適用于檢索條件復(fù)雜、實時性要求比較低的查詢場景。例如B端的各類復(fù)雜查詢條件檢索場景以及 C端用戶關(guān)鍵詞訂單列表搜索等場景。查詢耗時基本在100ms以上、甚至1s以上。
值得一提的是需要mysql數(shù)據(jù)異構(gòu)到ES,ES加載進索引也有1s左右延遲,數(shù)據(jù)從產(chǎn)生到ES索引延遲比較高。
ElasticSearch 支持分頁查詢,和Mysql Limit offset 類似。同時也強烈建議,使用分頁查詢時,指定排序方式。
SearchRequest searchRequest = new SearchRequest(index);
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//計算出記錄起始下標(biāo)
int from = (pageNum - 1) * pageSize;
// 起始記錄下標(biāo),從0開始
sourceBuilder.from(from);
//每頁顯示的記錄數(shù)
sourceBuilder.size(pageSize);
和mysql類似,ES也有深度分頁的查詢壓力,默認(rèn)的最大查詢深度max_result_window=1W, 閾值可以修改。在低頻的B端查詢場景,可以根據(jù)需要適當(dāng)調(diào)整閾值。
以上4種分頁查詢方式?jīng)]有最好,需要針對不同的場景選擇最合適的。