在Mysql中我們常常用order by來進行排序,使用limit來進行分頁,當需要先排序后分頁時我們往往使用類似的寫法select * from 表名 order by 排序字段 limt M,N。但是這種寫法卻隱藏着較深的使用陷阱。在排序字段有數據重復的情況下,會很容易出現排序結果與預期不一致的問題。
比如現在有一張user表,表結構及數據如下:


現在想根據創建時間升序查詢user表,並且分頁查詢,每頁2條,那很容易寫出sql為:select * from user order by create_time limit pageNo,2;
在執行查詢過程中會發現:
1、查詢第一頁數據時:

2、查詢第四頁數據時:

user表共有8條數據,有4頁數據,但是實際查詢過程中第一頁與第四頁竟然出現了相同的數據。
這是什么情況?難道上面的分頁SQL不是先將兩個表關聯查詢出來,然后再排好序,再取對應分頁的數據嗎???
上面的實際執行結果已經證明現實與想像往往是有差距的,實際SQL執行時並不是按照上述方式執行的。這里其實是Mysql會對Limit做優化,具體優化方式見官方文檔:https://dev.mysql.com/doc/refman/5.7/en/limit-optimization.html
這個是5.7版本的說明,提取幾個問題直接相關的點做下說明。

上面官方文檔里面有提到如果你將Limit row_count與order by混用,mysql會找到排序的row_count行后立馬返回,而不是排序整個查詢結果再返回。如果是通過索引排序,會非常快;如果是文件排序,所有匹配查詢的行(不帶Limit的)都會被選中,被選中的大多數或者全部會被排序,直到limit要求的row_count被找到了。如果limit要求的row_count行一旦被找到,Mysql就不會排序結果集中剩余的行了。
這里我們查看下對應SQL的執行計划:

可以確認是用的文件排序,表確實也沒有加額外的索引。所以我們可以確定這個SQL執行時是會找到limit要求的行后立馬返回查詢結果的。
不過就算它立馬返回,為什么分頁會不准呢?
官方文檔里面做了如下說明:

如果order by的字段有多個行都有相同的值,mysql是會隨機的順序返回查詢結果的,具體依賴對應的執行計划。也就是說如果排序的列是無序的,那么排序的結果行的順序也是不確定的。
基於這個我們就基本知道為什么分頁會不准了,因為我們排序的字段是create_time,正好又有幾個相同的值的行,在實際執行時返回結果對應的行的順序是不確定的。對應上面的情況,第一頁返回的name為8的數據行,可能正好排在前面,而第四頁查詢時name為8的數據行正好排在后面,所以第四頁又出現了。
那這種情況應該怎么解決呢?
官方給出了解決方案:

如果想在Limit存在或不存在的情況下,都保證排序結果相同,可以額外加一個排序條件。例如id字段是唯一的,可以考慮在排序字段中額外加個id排序去確保順序穩定。
所以上面的情況下可以在SQL再添加個排序字段,比如fund_flow的id字段,這樣分頁的問題就解決了。修改后的SQL可以像下面這樣:
SELECT * FROM user
ORDER BY create_time,id LIMIT 6,2;
再次測試問題解決!!
前言:在使用order by時,經常出現Using filesort,因此對於此類sql語句需盡力優化,使其盡量使用Using index。
0.准備
#1.創建test表。
drop table if exists test; create table test( id int primary key auto_increment, c1 varchar(10), c2 varchar(10), c3 varchar(10), c4 varchar(10), c5 varchar(10) ) ENGINE=INNODB default CHARSET=utf8; insert into test(c1,c2,c3,c4,c5) values('a1','a2','a3','a4','a5'); insert into test(c1,c2,c3,c4,c5) values('b1','b2','b3','b4','b5'); insert into test(c1,c2,c3,c4,c5) values('c1','c2','c3','c4','c5'); insert into test(c1,c2,c3,c4,c5) values('d1','d2','d3','d4','d5'); insert into test(c1,c2,c3,c4,c5) values('e1','e2','e3','e4','e5');
#2.創建索引。
1.根據Case分析order by的使用情況
Case 1:
分析:
①在c1,c2,c3,c4上創建了索引,直接在c1上使用范圍,導致了索引失效,全表掃描:type=ALL,ref=Null。因為此時c1主要用於排序,並不是查詢。
②使用c1進行排序,出現了Using filesort。
③解決方法:使用覆蓋索引。
Case 1.1:
分析:
排序時按照索引的順序,所以不會出現Using filesort。
Case 1.2:
分析:
出現了Using filesort。原因:排序用的c2,與索引的創建順序不一致,對比Case1.1可知,排序時少了c1(帶頭大哥),因此出現Using filesort。
Case 1.3:
分析:
出現了Using filesort。因為排序索引列與索引創建的順序相反,從而產生了重排,也就出現了Using filesort。
Case 2:
分析:
直接使用c2進行排序,出現Using filesort,因為不是從最左列索引開始排序的(沒有帶頭大哥)。
Case 2.1:
分析:
排序使用了索引順序(帶頭大哥在),因此不會出現Using filesort。
Case 2.2:
分析:
雖然排序的字段列與索引順序一樣,且order by默認升序,這里c2 desc變成了降序,導致與索引的排序方式不同,從而產生Using filesort。
總結:
①MySQL支持兩種方式的排序filesort和index,Using index是指MySQL掃描索引本身完成排序。index效率高,filesort效率低。
②order by滿足兩種情況會使用Using index。
#1.order by語句使用索引最左前列。
#2.使用where子句與order by子句條件列組合滿足索引最左前列。
③盡量在索引列上完成排序,遵循索引建立(索引創建的順序)時的最佳左前綴法則。
④如果order by的條件不在索引列上,就會產生Using filesort。
#1.filesort有兩種排序算法:雙路排序和單路排序。
雙路排序:在MySQL4.1之前使用雙路排序,就是兩次磁盤掃描,得到最終數據。讀取行指針和order by列,對他們進行排序,然后掃描已經排好序的列表,按照列表中的值重新從列表中讀取對應的數據輸出。即從磁盤讀取排序字段,在buffer進行排序,再從磁盤取其他字段。
如果使用雙路排序,取一批數據要對磁盤進行兩次掃描,眾所周知,I/O操作是很耗時的,因此在MySQL4.1以后,出現了改進的算法:單路排序。
單路排序:從磁盤中查詢所需的列,按照order by列在buffer中對它們進行排序,然后掃描排序后的列表進行輸出。它的效率更高一些,避免了第二次讀取數據,並且把隨機I/O變成了順序I/O,但是會使用更多的空間,因為它把每一行都保存在內存中了。
#2.單路排序出現的問題。
當讀取數據超過sort_buffer的容量時,就會導致多次讀取數據,並創建臨時表,最后多路合並,產生多次I/O,反而增加其I/O運算。
解決方式:
a.增加sort_buffer_size參數的設置。
b.增大max_length_for_sort_data參數的設置。
⑤提升order by速度的方式:
#1.在使用order by時,不要用select *,只查詢所需的字段。
因為當查詢字段過多時,會導致sort_buffer不夠,從而使用多路排序或進行多次I/O操作。
#2.嘗試提高sort_buffer_size。
#3.嘗試提高max_length_for_sort_data。
⑥附上一張從視頻中截取出來的總結圖。
⑦group by與order by很類似,其實質是先排序后分組,遵照索引創建順序的最佳左前綴法則。當無法使用索引列的時候,也要對sort_buffer_size和max_length_for_sort_data參數進行調整。注意where高於having,能寫在where中的限定條件就不要去having限定了。
by Shawn Chen,2018.6.26日,上午。