從官方文檔中探索MySQL分頁的幾種方式及分頁優化

概覽

相比於Oracle,SQL Server 等數據庫,MySQL分頁的方式簡單得多了,官方自帶了分頁語法 limit 語句:html

select * from test_t LIMIT {[offset,] row_count | row_count OFFSET offset}

例如:要獲取第12行到第21行的記錄能夠這樣寫:mysql

select * from test_t limit 11,10;

或者sql

select * from test_t limit 10 offset 11;

固然簡單的用法能夠這樣使用,可是若是遇到數據量比較大的狀況下和分頁在中間或後面部分的話,這樣使用會有性能問題。等會再看一下優化後的另一種分頁方式。數據庫

從官網中探索

想着 limit 語句既然是官方語言,那樣官方文檔中必定會有的介紹MySQL分頁的方式和優化的相關的建議吧,後面我看了一下其實與想象中的有挺大差異的,官方文檔不是很全面🤐segmentfault

關於MySQL 分頁方式的官方文檔解讀

先來看看官網文檔中是怎麼樣描述 limit語句的 :app

The LIMIT clause can be used to constrain the number of rows returned by the SELECT statement. LIMIT takes one or two numeric arguments, which must both be nonnegative integer constants, with these exceptions:
Within prepared statements, LIMIT parameters can be specified using ? placeholder markers.
Within stored programs, LIMIT parameters can be specified using integer-valued routine parameters or local variables.
來自:https://dev.mysql.com/doc/refman/5.6/en/limit-optimization.htmlide

翻譯成中文,大概意思就是:「limit 語句能夠用到 select 語句中去限制返回結果的行數。limit 子語句能夠帶一個或兩個參數,這些參數都必須是非負整數常量,但有如下例外:一個是預執行語句中能夠用?替代,另一個是在存儲程序中用表達式替代」性能

簡單來講,就是 limit 是用來限制查詢返回行數的(這裏舉例的是 select 語句中limit的用法,在update 等語句中也有相關 limit 的介紹,大概相似)。測試

好吧,講了等會沒講🤣fetch

接着,在官網文檔中舉例了 limit 的一些簡單用法示例,還有一些其餘說明。
select語句中的limit說明
能夠重點關注下後面紅框部分的兩句描述。

第一句爲:爲了兼容PostgreSQL,MySQL 也支持 limit offset 這種方式的語句

這裏說的是,MySQL 分頁 除了能夠這樣寫

select * from test_t limit 11,10

也能夠這樣寫

select * from test_t limit 10 offset 11

offset 這種方式,我以前還真沒見過MySQL這樣用的😂,又獲得一個冷知識。

至於爲何須要兼容 PostgreSQL ,我搜了一下網上資料,估計是 當時PostgreSQL 很火,可讓用戶沒有什麼成本就遷移到MySQL上

第二句爲:若是LIMIT出如今帶圓括號的查詢表達式中,而且也應用在外部查詢中,則查詢的結果未準肯定義,而且可能在未來的MySQL版本中更改。

這裏說的就是,例如這樣的 SQL 結果有可能之後會發生變化:

SELECT * FROM d_comment WHERE  id IN (SELECT id FROM `d_app_info` ORDER BY id LIMIT 1,10) ORDER BY id LIMIT 0,10

挺懷疑它這裏的描述的,這樣的用法不是很正常嗎?🙄會有什麼影響嗎?。我這裏的用的是MySQL 5.6(2013年)版本的文檔,因而我切換到的MySQL 5.8(2020年)的版本,發現這裏的描述仍是不變🙃。過了7年這裏的描述仍是沒變,這之後都應該不會變化了吧。。。

可是當我執行上面的 SQL的時候,papa打臉了
打臉了
提示這個錯誤:

This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'

因而又去搜索了一下,發如今官方文檔中就說明了,就不能這樣使用
mysql 子查詢不支持limit說明

來自:https://dev.mysql.com/doc/refman/8.0/en/subquery-restrictions.html

須要改寫成這樣,嵌多一層子查詢

SELECT id,dt FROM d_comment WHERE  id IN (  SELECT * FROM (SELECT id FROM `d_app_info` ORDER BY app_name,id LIMIT 1,10) t2  ) ORDER BY id LIMIT 0,10;

或者用多表關聯查詢來解決(這種方式在分頁數量多的時候性能更好些)

SELECT a.id,a.dt FROM d_comment a , (SELECT id FROM `d_app_info` ORDER BY app_name,id LIMIT 1,10) b WHERE a.id=b.id ORDER BY a.id ;

而後,我與外層沒有 limit 的 SQL 執行對比了一下:

SELECT id,dt FROM d_comment WHERE  id IN (  SELECT * FROM (SELECT id FROM `d_app_info` ORDER BY app_name,id LIMIT 1,10) t2  ) ORDER BY id ;

外層帶limit的
外層不帶limit的
結果是同樣的,與預想的同樣,這裏的應該是不須要管了🙃,除了注意一下要嵌套多一層

關於MySQL limit 優化相關的官方文檔解讀

想着官方文檔上也會有分頁相關優化的介紹,因而在官方文檔的搜索欄輸入 limit 或 page 上搜索了一下,發現與 limit 優化相關的只有如下這個:
官方文檔中關於limit 的優化

LIMIT Query Optimization
If you need only a specified number of rows from a result set, use a LIMIT clause in the query, rather than fetching the whole result set and throwing away the extra data.
MySQL sometimes optimizes a query that has a LIMIT row_count clause and no HAVING clause:
引用自:https://dev.mysql.com/doc/refman/5.6/en/limit-optimization.html

官方文檔,這部分主要介紹的在包含limit row_count 這種語句的各類類型查詢的狀況下,MySQL會作了哪些優化。其中大部分是用不到的,這裏重點介紹一下2種比較有趣的特色。

一,查詢總行數的另外一種方法

查詢分頁的總行數,咱們通常狀況下是使用的, select count(*) 去實現的。文檔中介紹了另一種方式,經過 select FOUND_ROWS(); 語句,不過這種方式的須要修改獲取limit 數據的SQL 。例如:若是在SQL中同時執行這兩條語句,就能夠分別獲取到分頁的數據和分頁總行數。

# 這個SQL 比平時的分頁SQL 多了 SQL_CALC_FOUND_ROWS 修飾
SELECT SQL_CALC_FOUND_ROWS  id,`name` FROM `d_common_all_select_info` ORDER BY `name` LIMIT 0,10;
# 這條SQL返回的爲總行數,不過統計方式與select count(*) 有些不一樣,另外須要與上面的SQL 同時執行
SELECT FOUND_ROWS();

二,查詢的order by 的順序問題

If multiple rows have identical values in the ORDER BY columns, the server is free to return those rows in any order, and may do so differently depending on the overall execution plan. In other words, the sort order of those rows is nondeterministic with respect to the nonordered columns.
One factor that affects the execution plan is LIMIT, so an ORDER BY query with and without LIMIT may return rows in different orders

翻譯過來並簡單來講就是,若是order by 中包含相同的值,則order by出來的結果不必定每次同樣,它返回的順序與整體執行計劃有關。例如,帶limit 和不帶limit的order by 語句,返回順序不同的。

因此若是order by 的列包含相同值的時候,保證每次都是相同的結果,最好在最後的加上id列(這裏假設自增id列名爲id)。例如:日常要order by dt 的,而後dt中會包含相同值的最好修改成 order by dt,id
帶limit 和不帶limit的order by結果

MySQL 分頁優化的兩種方法

通過屢次在官方文檔中查找,沒有發現其餘關於 MySQL 分頁優化的描述,可是若是在表數據量大的時候,直接使用 limit offset, row_num 這種方式去查詢的會很慢的。因此這裏再介紹一下MySQL 分頁查詢的另外一種方式,並與原方式進行一些對比。

普通的limit offset,row_num 方式,會先從數據文件中查到offset + row_num 記錄,而後把前 offset 記錄拋棄掉返回的。例如:limit 1000,10 ,會從數據文件中查詢1010 行記錄,只返回的10記錄,前1000行記錄會被拋棄掉。爲了優化這種的大偏移的查詢的,主要有兩種方法:延遲關聯 和 書籤記錄。其中延遲關聯應用的業務場景更加普遍。

這裏使用一張測試表 page_test_t 來進行測試,表的行數以下(大約110萬行):

-- 查詢錶行數
SELECT TABLE_NAME, PARTITION_NAME, TABLE_ROWS, AVG_ROW_LENGTH, DATA_LENGTH FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_NAME='page_test_t' ;

測試表行數結果
這裏假設將 offset 設置爲 600000,取 10 行記錄來對比各個分頁方法之間的查詢速度

普通分頁

普通分頁,直接使用 limit offset,row_num 去查詢

SELECT SQL_NO_CACHE  * FROM `d_common_all_select_info` ORDER BY id  LIMIT 600000,10;

查詢10次,查詢結果與如下結果相差不超過0.01s,查詢時間耗時穩定在 0.46 s

普通分頁查詢耗時

延遲關聯

能夠先按照條件分頁查詢出主鍵,而後根據主鍵的再去關聯表,查詢出全部須要列的記錄數,這樣能夠避免掃描太多的數據頁。按照要求條件,能夠寫出這樣的例子 SQL:

SELECT SQL_NO_CACHE  a.* FROM `page_test_t` a,(SELECT id FROM page_test_t ORDER BY id LIMIT 600000,10) b WHERE a.id=b.id ORDER BY a.id ;

查詢10次,查詢結果與如下結果相差不超過0.01s,查詢時間耗時穩定在 0.18 s
延遲關聯查詢結果

書籤記錄

「書籤記錄」指咱們能夠用一個臨時變量來存儲上一次取數記錄的位置,而後在獲取下一頁的時候,能夠根據這個值,來獲取大於這個值的下一頁記錄(上一頁相似),直接從該值之後開始掃描。例如,假設咱們上一次獲取到了分頁 limit 599990,10 的記錄,最大的值的id爲 1690344(這裏的值做爲了一個書籤記錄),則咱們獲取 limit 600000,10 的記錄能夠這樣寫:

SELECT SQL_NO_CACHE  * FROM `page_test_t`  where id>1690344 ORDER BY id  LIMIT 10 ;

查詢10次,查詢結果與如下結果相差不超過0.01s,查詢時間耗時穩定在 0.004 s
書籤記錄查詢

小結

能夠看到,延遲關聯 比 普通分頁方式查詢速度快了近3倍,「書籤記錄」比普通分頁方式快了近100倍。不過「書籤記錄」只適合只有前翻頁,後翻頁這種類型的分頁交互,不能跳轉到任意頁碼,延遲關聯則能夠支持,因此通常狀況下,咱們均可以使用延遲關聯方式來替換原普通分頁的方式。

除了這兩種方法,還有例如使用預先計算的彙總表 或者關聯到另一張冗餘表,冗餘表中包含表的主鍵和須要作排序的數據列 等方法去優化分頁。 參考自:《高性能MySQL(第三版)》

總結

本文從SQL 的 limit 分頁語句出發,詳細介紹了 limit 的語法及簡要的歸納了MySQL 官方文檔對 limit 的優化,另外對比了兩種經常使用的分頁優化方法 延遲關聯「書籤記錄」

參考:
https://dev.mysql.com/doc/refman/5.6/en/limit-optimization.html
https://dev.mysql.com/doc/refman/5.6/en/select.html
https://www.liaoxuefeng.com/wiki/1177760294764384/1217864791925600
https://segmentfault.com/a/1190000017059239?utm_source=sf-related
MySQL中SQL_CALC_FOUND_ROWS的用法 https://www.cnblogs.com/chinesern/p/8260506.html

相關文章
相關標籤/搜索