MySQL最佳化案例系列-mysql分頁最佳化_Mysql

來源:互聯網
上載者:User

通常,我們會採用ORDER BY LIMIT start, offset 的方式來進行分頁查詢。例如下面這個SQL:

SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 100, 10;

或者像下面這個不帶任何條件的分頁SQL:

SELECT * FROM `t1` ORDER BY id DESC LIMIT 100, 10;

一般而言,分頁SQL的耗時隨著 start 值的增加而急劇增加,我們來看下面這2個不同起始值的分頁SQL執行耗時:

yejr@imysql.com> SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 500, 10;…10 rows in set (0.05 sec)yejr@imysql.com> SELECT * FROM `t1` WHERE ftype=6 ORDER BY id DESC LIMIT 935500, 10;…10 rows in set (2.39 sec)

可以看到,隨著分頁數量的增加,SQL查詢耗時也有數十倍增加,顯然不科學。今天我們就來分析下,如何能最佳化這個分頁方案。 一般滴,想要最佳化分頁的終極方案就是:沒有分頁,哈哈哈~~~,不要說我講廢話,確實如此,可以把分頁演算法交給Sphinx、Lucence等第三方解決方案,沒必要讓MySQL來做它不擅長的事情。 當然了,有小夥伴說,用第三方太麻煩了,我們就想用MySQL來做這個分頁,咋辦呢?莫急,且待我們慢慢分析,先看下錶DDL、資料量、查詢SQL的執行計畫等資訊:

yejr@imysql.com> SHOW CREATE TABLE `t1`;CREATE TABLE `t1` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT,... `ftype` tinyint(3) unsigned NOT NULL,... PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;yejr@imysql.com> select count(*) from t1;+----------+| count(*) |+----------+| 994584 |+----------+yejr@imysql.com> EXPLAIN SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 500, 10\G*************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 type: indexpossible_keys: NULL key: PRIMARY key_len: 4 ref: NULL rows: 510 Extra: Using whereyejr@imysql.com> EXPLAIN SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500, 10\G*************************** 1. row *************************** id: 1 select_type: SIMPLE table: t1 type: indexpossible_keys: NULL key: PRIMARY key_len: 4 ref: NULL rows: 935510 Extra: Using where

可以看到,雖然通過主鍵索引進行掃描了,但第二個SQL需要掃描的記錄數太大了,而且需要先掃描約935510條記錄,然後再根據排序結果取10條記錄,這肯定是非常慢了。 針對這種情況,我們的最佳化思路就比較清晰了,有兩點:

1、儘可能從索引中直接擷取資料,避免或減少直接掃描行資料的頻率
2、儘可能減少掃描的記錄數,也就是先確定起始的範圍,再往後取N條記錄即可

據此,我們有兩種相應的改寫方法:子查詢、表串連,即下面這樣的:

#採用子查詢的方式最佳化,在子查詢裡先從索引擷取到最大id,然後倒序排,再取10行結果集
#注意這裡採用了2次倒序排,因此在取LIMIT的start值時,比原來的值加了10,即935510,否則結果將和原來的不一致

yejr@imysql.com> EXPLAIN SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC\G*************************** 1. row *************************** id: 1 select_type: PRIMARY table: <derived2> type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 10 Extra: Using filesort*************************** 2. row *************************** id: 2 select_type: DERIVED table: t1 type: ALLpossible_keys: PRIMARY key: NULL key_len: NULL ref: NULL rows: 973192 Extra: Using where*************************** 3. row *************************** id: 3 select_type: SUBQUERY table: t1 type: indexpossible_keys: NULL key: PRIMARY key_len: 4 ref: NULL rows: 935511 Extra: Using where#採用INNER JOIN最佳化,JOIN子句裡也優先從索引擷取ID列表,然後直接關聯查詢獲得最終結果,這裡不需要加10yejr@imysql.com> EXPLAIN SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500,10) t2 USING (id)\G*************************** 1. row *************************** id: 1 select_type: PRIMARY table: <derived2> type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 935510 Extra: NULL*************************** 2. row *************************** id: 1 select_type: PRIMARY table: t1 type: eq_refpossible_keys: PRIMARY key: PRIMARY key_len: 4 ref: t2.id rows: 1 Extra: NULL*************************** 3. row *************************** id: 2 select_type: DERIVED table: t1 type: indexpossible_keys: NULL key: PRIMARY key_len: 4 ref: NULL rows: 973192 Extra: Using where

然後我們來對比下這2個最佳化後的新SQL執行時間:

yejr@imysql.com> SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) T ORDER BY id DESC;...rows in set (1.86 sec)#採用子查詢最佳化,從profiling的結果來看,相比原來的那個SQL快了:28.2%yejr@imysql.com> SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500,10) t2 USING (id);...10 rows in set (1.83 sec)#採用INNER JOIN最佳化,從profiling的結果來看,相比原來的那個SQL快了:30.8%

我們再來看一個不帶過濾條件的分頁SQL對比:

#原始SQLyejr@imysql.com> EXPLAIN SELECT * FROM `t1` ORDER BY id DESC LIMIT 935500, 10\G*************************** 1. row ***************************   id: 1 select_type: SIMPLE  table: t1   type: indexpossible_keys: NULL   key: PRIMARY  key_len: 4   ref: NULL   rows: 935510  Extra: NULLyejr@imysql.com> SELECT * FROM `t1` ORDER BY id DESC LIMIT 935500, 10;...10 rows in set (2.22 sec)#採用子查詢最佳化yejr@imysql.com> EXPLAIN SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC;*************************** 1. row ***************************   id: 1 select_type: PRIMARY  table: <derived2>   type: ALLpossible_keys: NULL   key: NULL  key_len: NULL   ref: NULL   rows: 10  Extra: Using filesort*************************** 2. row ***************************   id: 2 select_type: DERIVED  table: t1   type: ALLpossible_keys: PRIMARY   key: NULL  key_len: NULL   ref: NULL   rows: 973192  Extra: Using where*************************** 3. row ***************************   id: 3 select_type: SUBQUERY  table: t1   type: indexpossible_keys: NULL   key: PRIMARY  key_len: 4   ref: NULL   rows: 935511  Extra: Using indexyejr@imysql.com> SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC;…10 rows in set (2.01 sec)#採用子查詢最佳化,從profiling的結果來看,相比原來的那個SQL快了:10.6%#採用INNER JOIN最佳化yejr@imysql.com> EXPLAIN SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1`ORDER BY id DESC LIMIT 935500,10) t2 USING (id)\G*************************** 1. row ***************************   id: 1 select_type: PRIMARY  table:    type: ALLpossible_keys: NULL   key: NULL  key_len: NULL   ref: NULL   rows: 935510  Extra: NULL*************************** 2. row ***************************   id: 1 select_type: PRIMARY  table: t1   type: eq_refpossible_keys: PRIMARY   key: PRIMARY  key_len: 4   ref: t1.id   rows: 1  Extra: NULL*************************** 3. row ***************************   id: 2 select_type: DERIVED  table: t1   type: indexpossible_keys: NULL   key: PRIMARY  key_len: 4   ref: NULL   rows: 973192  Extra: Using indexyejr@imysql.com> SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1`ORDER BY id DESC LIMIT 935500,10) t2 USING (id);…10 rows in set (1.70 sec)#採用INNER JOIN最佳化,從profiling的結果來看,相比原來的那個SQL快了:30.2%

至此,我們看到採用子查詢或者INNER JOIN進行最佳化後,都有大幅度的提升,這個方法也同樣適用於較小的分頁,雖然LIMIT開始的 start 位置小了很多,SQL執行時間也快了很多,但採用這種方法後,帶WHERE條件的分頁分別能提高查詢效率:24.9%、156.5%,不帶WHERE條件的分頁分別提高查詢效率:554.5%、11.7%,各位可以自行進行測實驗證。單從提升比例說,還是挺可觀的,確保這些最佳化方法可以適用於各種分頁模式,就可以從一開始就是用。 我們來看下各種情境相應的提升比例是多少:

大分頁,帶WHERE 大分頁,不帶WHERE 大分頁平均提升比例 小分頁,帶WHERE 小分頁,不帶WHERE 總體平均提升比例
子查詢最佳化 28.20% 10.60% 19.40% 24.90% 554.40% 154.53%
INNER JOIN最佳化 30.80% 30.20% 30.50% 156.50% 11.70% 57.30%

結論:這樣看就和明顯了,尤其是針對大分頁的情況,因此我們優先推薦使用INNER JOIN方式最佳化分頁演算法。

上述每次測試都重啟mysqld執行個體,並且加了SQL_NO_CACHE,以保證每次都是直接資料檔案或索引檔案中讀取。如果資料經過預熱後,查詢效率會一定程度提升,但但上述相應的效率提升比例還是基本一致的。

2014/07/28後記更新:

其實如果是不帶任何條件的分頁,就沒必要用這麼麻煩的方法了,可以採用對主鍵採用範圍檢索的方法,例如參考這篇:Advance for MySQL Pagination

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.