在寫 select 語句的時候,使用 limit, offset 可能就像是我們吃飯喝水一樣自然了。
剛開始工作的時候也經(jīng)常聽前輩們教導:使用 limit, offset,當 offset 變大的時候執(zhí)行效率會越來越低。
相信在前輩們的言傳身教,和自己的實戰(zhàn)過程中,大家也都知道了為什么會這樣。
因為 select 在執(zhí)行過程中,對于存儲引擎返回的記錄,經(jīng)過 server 層的 WHERE 條件篩選之后,符合條件的前 offset條記錄,會被直接無情的拋棄,直到符合條件的第 offset + 1 條記錄,才開始發(fā)送給客戶端,發(fā)送了 limit 條記錄之后,查詢結(jié)束。
雖然知道了是什么,也知道了為什么,但是我也一直好奇底層是怎么實現(xiàn)的,所以今天我們來扒一扒它的廬山真面目。
1. 語法回顧
先來簡單的回顧一下 select 語句中 limit, offset 的語法,MySQL 支持 3 種形式:
- LIMIT limit: 因為沒有指定 offset,所以 offset = 0,表示讀取符合 WHERE 條件的第 1 ~ limit 條記錄。
- LIMIT offset, limit: 我們常用的就是這種了。
- LIMIT limit OFFSET offset: 這種不常用。
offset 和 limit 的值都不能為負數(shù),在源碼里這兩個屬性定義的是無符號整數(shù),并且在解析階段就做了限制,如果為負數(shù),直接報語法錯誤了。
2. 語法解析階段
在讀取數(shù)據(jù)的過程中,對于符合條件的前 offset 條記錄,會直接忽略,不發(fā)送給客戶端,從符合條件的第 offset + 1 條記錄開始,發(fā)送 limit 條記錄給客戶端。
所以,server 層實際上需要從存儲引擎讀取 offset + limit 條記錄,源碼里也是這么實現(xiàn)的,語法解析階段,在驗證了 offset 和 limit 都是大于等于 0 的整數(shù)之后,就把 offset + limit 的計算結(jié)果保存到一個叫做 select_limit_cnt 的屬性里,offset 也會保存到一個叫做 offset_limit_cnt 的屬性里。
3. 發(fā)送數(shù)據(jù)階段
來到發(fā)送數(shù)據(jù)階段,此時的記錄已經(jīng)通過了 WHERE 條件的篩選,接下來就是判斷這條記錄是不是要發(fā)送給客戶端。
第 1 步
因為 offset 已經(jīng)保存到 offset_limit_cnt 中了,先來判斷 offset_limit_cnt 是否大于 0,如果大于 0,這條記錄就會被拋棄了,不發(fā)送給客戶端;如果等于 0,記錄就具備了發(fā)送給客戶端的資格了,然后接著進入第 2 步。
在拋棄記錄之前,還會干一件事:對一個叫做 send_records 的屬性進行加 1 操作,就是假裝這條記錄已經(jīng)發(fā)送了(為什么這樣干?第 2 步會用到這個屬性)。
offset_limit_cnt 是保證不會小于 0 的,所以在這一步只需要判斷是大于 0 還是等于 0 就可以了。
第 2 步
來到這一步,記錄就具備了發(fā)送給客戶端的資格了,至于要不要發(fā),就看客戶端想不想要它了,而客戶端想不想要它,取決于 select_limit_cnt。
所以,在這一步要判斷已發(fā)送記錄數(shù)量(send_records)和需要發(fā)送記的錄數(shù)量(select_limit_cnt)之間的關(guān)系,如果已發(fā)送記錄數(shù)量大于等于需要發(fā)送的記錄數(shù)量,則結(jié)束查詢,否則就接著進入第 3 步。
第 3 步
在這里,記錄愉快的等待著被發(fā)送給客戶端。
是的,還要愉快的等著,因為要排隊,畢竟運輸也是需要成本的,不能來一條記錄,就發(fā)一趟車,要等一輛車裝滿之后,才會發(fā)車的。這里的車指的是網(wǎng)絡(luò)緩沖區(qū),以后也會寫文章介紹,敬請期待。
4. 最佳實踐
既然在 offset 變大之后,使用 limit, offset 效率越來越低,那應該怎么辦呢?對于實戰(zhàn)經(jīng)驗豐富的小伙伴來說,這是相當簡單了,但是以防萬一剛看到本文的小伙伴是剛剛開始用 SQL 寫 Bug,所以還是要大概的寫一下的。
以一個 SQL 為例:
select * from t2where i1 > 90000000 limit 8888, 10
為了取到 10 條記錄,要先找到 8888 條記錄,然后取到需要的 10 條,前面 8888 條記錄都白找了,太浪費了,可以這樣修改一下:
select * from t2 where i1 > 90000000 and id > LAST_MAX_IDlimit 10
LAST_MAX_ID 是上一次執(zhí)行 SQL 時讀取到的主鍵 ID 的最大值,如果是第一次執(zhí)行語句,LAST_MAX_ID = 0。
不過這種方案也有個問題,不支持跳著翻頁,只支持順序翻頁(就是每次都點下一頁的這種)。
如果要支持跳著翻頁,怎么辦?
只用 MySQL 這把錘子顯然有點不夠用了,還要再找一把錘子(Redis),可以把符合條件的記錄的主鍵 ID 都讀取出來,存入到 Redis 的有序集合(zset)中,用 zset 相應的函數(shù)讀取到某一頁應該展示的數(shù)據(jù)對應的那些主鍵 ID,然后用這些主鍵 ID 去 MySQL 中查詢對應的數(shù)據(jù),從而用兩把錘子間接的實現(xiàn)了分頁功能。
當然,這個方案也是有適用場景的,比如,這個方案明顯就不適用于這些場景:符合條件的記錄非常非常多導致存主鍵 ID 到 Redis 要占用很大的內(nèi)存、記錄更新頻繁導致存主鍵 ID 的緩存經(jīng)常被清除。如果碰到更復雜的場景,就要結(jié)合業(yè)務(wù)具體情況具體分析了
以上就是本文的全部內(nèi)容了,如果本文對你有所幫助,還請幫忙 轉(zhuǎn)發(fā)、點贊,謝謝 ^_^