1. 程式人生 > >MySql大表分頁(附獨門祕技)

MySql大表分頁(附獨門祕技)

 

問題背景

MySql(InnoDB)中的訂單表需要按時間順序分頁查詢,且主鍵不是時間維度遞增,訂單表在百萬以上規模,此時如何高效地實現該需求?

注:本文並非主要講解如何建立索引,以下的分析均建立在有合適的索引的前提下

初步方案1

眾所周知,MySql中,有一個limit offset, pageSize的用法,可以實現分頁查詢

select * from order where user_id = xxx and 【其它業務條件】 order by created_time, id limit offset, pageSize

因為created_time可能重複,所以order by時應加上id,保證順序的確定性

點評

該方案在表規模較小的時候,不會暴露出問題,當order表增長到十萬級,並且查詢後面幾頁的時候,執行速度明顯變慢,可能降到100ms的量級,如果資料量增長到百萬級,則耗時達到秒級,如果增長到千萬級,那耗時就變得完全不可接受了(曾排查過這樣的線上慢SQL)

 


 深入分析

方案1為啥在大表中表現這麼差呢?我們可以來揣測一下MySql是怎麼執行這個查詢的

假設我們在user_id,created_time,以及【其它業務條件】建立了聯合索引,當我要查詢第100000條到100049條的記錄時,因為MySql的索引是b+ tree結構,不像陣列可以隨機定位到第N條記錄,它需要花不小的成本去找到N的位置,N越大,成本越大

拋開b+ tree的細節不講,我們還可以藉助統計表記錄總數的SQL來理解

select count(1) from order

如果能非常高效地定位第N條記錄,那麼上述統計也能非常高效的執行,但實際上,在大表中統計記錄總條數,也是非常慢的(本文是在InnoDB的場景下)

方案1低效的根本原因在於:定位到offset的成本過高,未能充分利用索引的有序性

 


方案2

索引(b+ tree)的特點在於,資料是有序的,雖然找到第N條記錄的效率比較低,但找到某一條資料在索引中的位置,其效率是很高的(索引本來就是解決這個問題的)

我們換一種思路,每次取50條記錄,第一次取的時候,指定從上次結束的位置繼續往後取50條,這樣,我們便可以利用上索引的有序性了

我們先看一個以id為序,進行分頁查詢的例子

select * from order where id > 'pre max id' order by id limit 50

第一次查詢不用帶條件,後續查詢則傳入前一次查詢的最大id,簡單分析可知,MySql在執行時,先定位到pre max id的位置(id是有序的,定位非常快),然後從這往後取50條記錄即可,整個過程非常高效

 

我們回到最開始的問題,“按時間順序分頁查詢,且主鍵不是時間維度遞增”,此時我們不能用id作為分頁的條件,因為按它去分頁,便不是按時間順序了,但也不能直接把id換成時間,因為時間可能會重複,我們來分析一下

id username created_time
xxx zhangsan 2019-01-01
ddd zhangsan 2019-02-03
yyy zhangsan 2019-02-03
abc zhangsan 2019-02-05
aaa zhangsan 2020-08-01

 

 

 

 

 

 

 

 

假如前一次分頁的最後一條記錄為id=ddd的這條(created_time為2019-02-03),下一次查詢使用created_time>2019-02-03作為條件時,則會把id=yyy的這條記錄漏掉,如果換成created_time>=2019-02-03也不行,id=ddd的這條記錄就又被查出來了

對於這個資料遺漏或重複的問題,我看到一種解決方案是這樣的:

分三種情況進行查詢

  1. 首次查詢,created_time>='xxxx-xx-xx',如果不要求以某時間開始,則無條件
    select * from order where user_id = xxx and 【其它業務條件】 and created_time >= 'xxxx-xx-xx' order by created_time, id limit pageSize
  2. 如果上次查詢的記錄條數等於pageSize,則用created_time和id的組合條件來查詢,為了防止created_time在邊界位置發生重複時漏掉資料
    select * from order where user_id = xxx and 【其它業務條件】 and created_time = 'created_time of latest recored' and id > 'id of latest recored' order by created_time, id limit pageSize
  3. 如果上次查詢的記錄數小於pageSize,並且上次查詢是第二種查詢,則僅用created_time來查詢,
    select * from order where user_id = xxx and 【其它業務條件】 and created_time > 'created_time of latest recored' order by created_time, id limit pageSize

     

點評

上述方法確實可以解決漏掉資料或重複的問題,並且也有著不錯的效能,但缺點也比較明顯,查詢過於複雜,得分情況執行不同的SQL,並且分頁不穩定,中間查詢出來的記錄數可能小於pageSize,實際上後面還有資料

 


進一步深入分析

我嘗試在網上找過資料,只找到了以id為分頁順序,然後用id>'pre max id'這種方式來查,而我們要以可重複的created_time為分頁順序,如何寫出簡潔高效的SQL呢?

 

如果要成為一個優秀的程式設計師,我覺得分析&解決新問題的能力,是必不可少的,即使在網上能找到解決方案,優秀的分析能力也有助於借鑑並結合自己的場景,優化出更好的個性化方案。

 

我們在(user_id,created_time)建立了索引,並且我們知道InnoDB的輔助索引是包含了主鍵的,且主鍵一定不會重複,這意味著在索引上,每條記錄的順序是完全確定的,不存在重複的情況

我們要分頁的順序跟此索引的順序是吻合的,只需要沿著索引,一批一批地取資料就可以了,這是一個對索引很直接的利用,為什麼現在我沒辦法做到?

如果我是MySql的設計人員,針對這種很常見很直接的需求,我怎麼去提供支援?還是說不支援?

我舉一個例子,像java中的基於排序的TreeSet,我猜它一定有floor和ceiling這樣的方法(返回Set中,大於或小於指定元素的第一個元素),這是基於排序的資料結構該有的東西,如果它沒有,那早被人噴瞭然後加上去了

回到索引的話題,這種直接的需求,它應該支援,否則說不過去,現在的問題變成了:用什麼語法來,來實現在組合索引上,基於組合(user_id,created_time,id的組合)順序的遍歷?

此時腦海裡便回想起以前用過的(a,b) in ((1,2),(3,4),(7,4))這樣的組合寫法,然後猜測它也支援大於小於這類比較,跑去MySql中驗證一下:

select (3,7)>(3,7),    (3,6)>(3,7),    (3,8)>(3,7),    (4,7)>(3,7),    (4,2)>(3,7);
返回:
0    0    1    1    1

如此一來,這問題就變得和id>'pre max id'這種一樣簡單了。

注:這種寫法後來在官方文件中找到了對應的資料,官方稱這類運算為“行比較”(row comparisons)

https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html#operator_greater-than

 方案3

由於有(a,b)>(1,2)這種語法,所以可以寫出簡潔又高效的SQL

select * from order where user_id = xxx and 【其它業務條件】 and (created_time, id) > (created_time and id of latest recode) order by created_time, id limit pageSize

此方式跟以id為序的分頁查詢是一樣的,首次查詢去掉組合條件即可,程式碼簡潔,同時又可以利用上組合索引,十分高效,耗時穩定,不會因為遍歷到末尾而效能降低

 


總結

方案1在小表的情況下,簡單方便,只用傳頁碼和頁大小即可,還可以隨機跳到指定頁,具有一定優勢

方案2和方案3在大表的情況下,有著優異的效能,以及穩定性,缺點是不能隨機地跳轉頁面,需要傳入上一頁的排序欄位。這個弊端在一定程度上可以規避,比如現在很多分頁都是一頁一頁地往下翻,比如微博、朋友圈動態等,或者是分批處理全表資料,不需要隨機跳轉

細心的同學可能發現,where條件裡還有【其它業務條件】,這樣還能正常走索引嗎?是否會發生全表掃描?這個問題其實是可以規避的,有空再寫一篇執行計劃並不完全可靠的案例。

題外話

方案3的寫法是我自己琢磨出來的,在網上也沒找到類似的資料,算獨門祕技吧,除此之外,我覺得同樣很有價值的是【進一步深入分析】中的思考過程,如果養成這種思考習慣,有利於創新,去解決別人沒遇到過的問題,在未知的領域,知道該從哪個方向去尋找答案;或者找到新的方法更好地去解決舊問題。

如果本文有幫助到你,或者覺得有價值,麻煩點個贊,這樣我會更有動力去更多地分享自己的經驗