問題背景
MySql(InnoDB)中的訂單表需要按時間順序分頁查詢,且主鍵不是時間次元遞增,訂單表在百萬以上規模,此時如何高效地實作該需求?
方案1
一般情況下,大家的分頁都會采用 MySql裡邊的 limit offset, pageSize的用法來實作分頁查詢
select * from order where user_id = xxx order by created_time, id limit offset, pageSize
因為created_time可能重複,是以order by時應加上id,保證順序的确定性
說明:該方案在表規模較小的時候,不會暴露出問題,當order表增長到十萬級,并且查詢後面幾頁的時候,執行速度明顯變慢,可能降到100ms的量級,如果資料量增長到百萬級,則耗時達到秒級,如果增長到千萬級,那耗時就變得完全不可接受了
分析:方案1為啥在大表中表現這麼差呢?
假設我們在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條記錄即可,整個過程非常高效
說明:上述方法确實可以解決漏掉資料或重複的問題,并且也有着不錯的性能,但缺點也比較明顯,查詢過于複雜,得分情況執行不同的SQL,并且分頁不穩定,中間查詢出來的記錄數可能小于pageSize(如果沒有重複項,那會多出一倍的結果為空的查詢),實際上後面還有資料
方案3
由于有了a>x or (a=x and b>y)這種等價于組合比較的文法,且能正确地使用索引,是以可以寫出高效且還算簡潔的SQL
select * from order
where user_id = xxx
and 【其它業務條件】
and (created_time > 'created_time of latest recode'
or (created_time = 'created_time of latest recode' and id > 'id of latest recode')
)
order by created_time, id limit pageSize
注意:
這裡也不允許created_time為null,因為null值參與>和=運算,結果一律為null,即條件不成立,相應結果查不出來。
如果存在為null的情況,則要作一些調整,如果前一批資料的最後一條記錄的created_time為null(null在索引中被視作極小值),則可以這樣改:
(created_time is not null or (created_time is null and id > 'id of latest recode'))
仍舊可以走索引,實作高效分頁查詢。
總結
方案1在小表的情況下,簡單友善,隻用傳頁碼和頁大小即可,還可以随機跳到指定頁,具有一定優勢
方案2和方案3在大表的情況下,有着優異的性能,以及穩定性,缺點是不能随機地跳轉頁面,需要傳入上一頁的排序字段。這個弊端在一定程度上可以規避,比如現在很多分頁都是一頁一頁地往下翻,比如微網誌、朋友圈動态等,或者是分批處理全表資料,不需要随機跳轉