在 【精華】洞悉MySQL底層架構:遊走在緩衝與磁盤之間 這篇文章中,咱們介紹了索引樹的頁面怎麼加載到內存中,如何淘汰,等底層細節。這篇文章咱們從比較宏觀的角度來看MySQL中關鍵字的原理。本文,咱們主要探索order by語句的底層原理。閱讀完本文,您將瞭解到:html
order by語句有哪些排序模式,以及每種排序模式的優缺點;mysql
order by語句會用到哪些排序算法,在什麼場景下會選擇哪一種排序算法;web
如何查看和分析sql的order by優化手段(執行計劃 + OPTIMIZER_TRACE日誌);算法
如何優化order by語句的執行效率?(思想:減少行大小,儘可能走索引,可以走覆蓋索引最佳,可適當增長sort buffer內存大小)sql
這裏咱們從數據結構的維度來看數據和索引,也就是都當成B+樹的的,咱們須要數據的時候再從存儲引擎的B+樹中讀取。數據庫
如下是咱們本文做爲演示例子的表,假設咱們有以下表:編程
索引以下:後端
對應的idx_d索引結構以下(這裏咱們作了一些誇張的手法,讓一個頁數據變小,爲了展示在索引樹中的查找流程):微信
一、如何跟蹤執行優化
爲了方便分析sql的執行流程,咱們能夠在當前session中開啓 optimizer_trace:網絡
SET optimizer_trace='enabled=on';
而後執行sql,執行完以後,就能夠經過如下堆棧信息查看執行詳情了:
SELECT * FROM information_schema.OPTIMIZER_TRACE\G;
如下是
1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 100,2;
的執行結果,其中符合a=3的有8457條記錄,針對order by重點關注如下屬性:
1"filesort_priority_queue_optimization": { // 是否啓用優先級隊列
2 "limit": 102, // 排序後須要取的行數,這裏爲 limit 100,2,也就是100+2=102
3 "rows_estimate": 24576, // 估計參與排序的行數
4 "row_size": 123, // 行大小
5 "memory_available": 32768, // 可用內存大小,即設置的sort buffer大小
6 "chosen": true // 是否啓用優先級隊列
7},
8...
9"filesort_summary": {
10 "rows": 103, // 排序過程當中會持有的行數
11 "examined_rows": 8457, // 參與排序的行數,InnoDB層返回的行數
12 "number_of_tmp_files": 0, // 外部排序時,使用的臨時文件數量
13 "sort_buffer_size": 13496, // 內存排序使用的內存大小
14 "sort_mode": "sort_key, additional_fields" // 排序模式
15}
1.一、排序模式
其中 sort_mode有以下幾種形式:
sort_key, rowid
:代表排序緩衝區元組包含排序鍵值和原始錶行的行id,排序後須要使用行id進行回表,這種算法也稱爲original filesort algorithm
(回表排序算法);sort_key, additional_fields
:代表排序緩衝區元組包含排序鍵值和查詢所須要的列,排序後直接從緩衝區元組取數據,無需回表,這種算法也稱爲modified filesort algorithm
(不回表排序);sort_key, packed_additional_fields
:相似上一種形式,可是附加的列(如varchar類型)緊密地打包在一塊兒,而不是使用固定長度的編碼。
如何選擇排序模式
選擇哪一種排序模式,與max_length_for_sort_data
這個屬性有關,這個屬性默認值大小爲1024字節:
若是查詢列和排序列佔用的大小超過這個值,那麼會轉而使用
sort_key, rowid
模式;若是不超過,那麼全部列都會放入sort buffer中,使用
sort_key, additional_fields
或者sort_key, packed_additional_fields
模式;若是查詢的記錄太多,那麼會使用
sort_key, packed_additional_fields
對可變列進行壓縮。
1.二、排序算法
基於參與排序的數據量的不一樣,能夠選擇不一樣的排序算法:
若是排序取的結果很小,小於內存,那麼會使用
優先級隊列
進行堆排序;例如,如下只取了前面10條記錄,會經過優先級隊列進行排序:
1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
若是排序limit n, m,n太大了,也就是說須要取排序很後面的數據,那麼會使用sort buffer進行
快速排序
:以下,表中a=1的數據有三條,可是因爲須要limit到很後面的記錄,MySQL會對比優先級隊列排序和快速排序的開銷,選擇一個比較合適的排序算法,這裏最終放棄了優先級隊列,轉而使用sort buffer進行快速排序:
1select a, b, c, d from t20 force index(idx_abc) where a=1 order by d limit 300,2;
若是參與排序的數據sort buffer裝不下了,那麼咱們會一批一批的給sort buffer進行內存快速排序,結果放入排序臨時文件,最終使對全部排好序的臨時文件進行
歸併排序
,獲得最終的結果;以下,a=3的記錄超過了sort buffer,咱們要查找的數據是排序後1000行起,sort buffer裝不下1000行數據了,最終MySQL選擇使用sort buffer進行分批快排,把最終結果進行歸併排序:
1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;
二、order by走索引避免排序
執行以下sql:
1select a, b, c, d from t20 force index(idx_d) where d like 't%' order by d limit 2;
咱們看一下執行計劃:
發現Extra列爲:Using index condition
,也就是這裏只走了索引。
執行流程以下圖所示:
經過idx_d索引進行range_scan查找,掃描到4條記錄,而後order by繼續走索引,已經排好序,直接取前面兩條,而後去彙集索引查詢完整記錄,返回最終須要的字段做爲查詢結果。這個過程只須要藉助索引。
如何查看和修改sort buffer大小?
咱們看一下當前的sort buffer大小:
能夠發現,這裏默認配置了sort buffer大小爲512k。
咱們能夠設置這個屬性的大小:
SET GLOBAL sort_buffer_size = 32*1024;
或者
SET sort_buffer_size = 32*1024;
下面咱們統一把sort buffer設置爲32k
1SET sort_buffer_size = 32*1024;
三、排序算法案例
3.一、使用優先級隊列進行堆排序
若是排序取的結果很小,而且小於sort buffer,那麼會使用優先級隊列進行堆排序;
例如,如下只取了前面10條記錄:
1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
a=3的總記錄數:8520
。查看執行計劃:
發現這裏where條件用到了索引,order by limit用到了排序。咱們進一步看看執行的optimizer_trace日誌:
1"filesort_priority_queue_optimization": {
2 "limit": 10,
3 "rows_estimate": 27033,
4 "row_size": 123,
5 "memory_available": 32768,
6 "chosen": true // 使用優先級隊列進行排序
7},
8"filesort_execution": [
9],
10"filesort_summary": {
11 "rows": 11,
12 "examined_rows": 8520,
13 "number_of_tmp_files": 0,
14 "sort_buffer_size": 1448,
15 "sort_mode": "sort_key, additional_fields"
16}
發現這裏是用到了優先級隊列進行排序。排序模式是:sort_key, additional_fields,即先回表查詢完整記錄,把排序須要查找的全部字段都放入sort buffer進行排序。
因此這個執行流程以下圖所示:
經過where條件a=3掃描到8520條記錄;
回表查找記錄;
把8520條記錄中須要的字段放入sort buffer中;
在sort buffer中進行堆排序;
在排序好的結果中取limit 10前10條,寫入net buffer,準備發送給客戶端。
3.二、內部快速排序
若是排序limit n, m,n太大了,也就是說須要取排序很後面的數據,那麼會使用sort buffer進行快速排序。MySQL會對比優先級隊列排序和歸併排序的開銷,選擇一個比較合適的排序算法。
如何衡量到底是使用優先級隊列仍是內存快速排序?
通常來講,快速排序算法效率高於堆排序,可是堆排序實現的優先級隊列,無需排序完全部的元素,就能夠獲得order by limit的結果。
MySQL源碼中聲明瞭快速排序速度是堆排序的3倍,在實際排序的時候,會根據待排序數量大小進行切換算法。若是數據量太大的時候,會轉而使用快速排序。
有以下SQL:
1select a, b, c, d from t20 force index(idx_abc) where a=1 order by d limit 300,2;
咱們把sort buffer設置爲32k:
1SET sort_buffer_size = 32*1024;
其中a=1的記錄有3條。查看執行計劃:
能夠發現,這裏where條件用到了索引,order by limit 用到了排序。咱們進一步看看執行的optimizer_trace日誌:
1"filesort_priority_queue_optimization": {
2 "limit": 302,
3 "rows_estimate": 27033,
4 "row_size": 123,
5 "memory_available": 32768,
6 "strip_additional_fields": {
7 "row_size": 57,
8 "sort_merge_cost": 33783,
9 "priority_queue_cost": 61158,
10 "chosen": false // 對比發現快速排序開銷成本比優先級隊列更低,這裏不適用優先級隊列
11 }
12},
13"filesort_execution": [
14],
15"filesort_summary": {
16 "rows": 3,
17 "examined_rows": 3,
18 "number_of_tmp_files": 0,
19 "sort_buffer_size": 32720,
20 "sort_mode": "<sort_key, packed_additional_fields>"
21}
能夠發現這裏最終放棄了優先級隊列,轉而使用sort buffer進行快速排序。
因此這個執行流程以下圖所示:
經過where條件a=1掃描到3條記錄;
回表查找記錄;
把3條記錄中須要的字段放入
sort buffer
中;在sort buffer中進行
快速排序
;在排序好的結果中取limit 300, 2第300、301條記錄,寫入net buffer,準備發送給客戶端。
3.三、外部歸併排序
當參與排序的數據太多,一次性放不進去sort buffer的時候,那麼咱們會一批一批的給sort buffer進行內存排序,結果放入排序臨時文件,最終使對全部排好序的臨時文件進行歸併排序,獲得最終的結果。
有以下sql:
1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 1000,10;
其中a=3的記錄有8520條。執行計劃以下:
能夠發現,這裏where用到了索引,order by limit用到了排序。進一步查看執行的optimizer_trace日誌:
1"filesort_priority_queue_optimization": {
2 "limit": 1010,
3 "rows_estimate": 27033,
4 "row_size": 123,
5 "memory_available": 32768,
6 "strip_additional_fields": {
7 "row_size": 57,
8 "chosen": false,
9 "cause": "not_enough_space" // sort buffer空間不夠,沒法使用優先級隊列進行排序了
10 }
11},
12"filesort_execution": [
13],
14"filesort_summary": {
15 "rows": 8520,
16 "examined_rows": 8520,
17 "number_of_tmp_files": 24, // 用到了24個外部文件進行排序
18 "sort_buffer_size": 32720,
19 "sort_mode": "<sort_key, packed_additional_fields>"
20}
咱們能夠看到,因爲limit 1000,要返回排序後1000行之後的記錄,顯然sort buffer已經不能支撐這麼大的優先級隊列了,因此轉而使用sort buffer內存排序,而這裏須要在sort buffer中分批執行快速排序,獲得多個排序好的外部臨時文件,最終執行歸併排序。(外部臨時文件的位置由tmpdir參數指定)
其流程以下圖所示:
四、排序模式案例
4.一、sort_key, additional_fields模式
sort_key, additional_fields
,排序緩衝區元組包含排序鍵值和查詢所須要的列(先回表取須要的數據,存入排序緩衝區中),排序後直接從緩衝區元組取數據,無需再次回表。
上面 2.3.一、2.3.2節的例子都是這種排序模式,就不繼續舉例了。
4.二、
模式
sort_key, packed_additional_fields
:相似上一種形式,可是附加的列(如varchar類型)緊密地打包在一塊兒,而不是使用固定長度的編碼。
上面2.3.3節的例子就是這種排序模式,因爲參與排序的總記錄大小太大了,所以須要對附加列進行緊密地打包操做,以節省內存。
4.三、
模式
前面咱們提到,選擇哪一種排序模式,與max_length_for_sort_data
[2]這個屬性有關,max_length_for_sort_data
規定了排序行的最大大小,這個屬性默認值大小爲1024字節:
也就是說若是查詢列和排序列佔用的大小小於這個值,這個時候會走sort_key, additional_fields
或者sort_key, packed_additional_fields
算法,不然,那麼會轉而使用sort_key, rowid
模式。
如今咱們特地把這個值設置小一點,模擬sort_key, rowid
模式:
1SET max_length_for_sort_data = 100;
這個時候執行sql:
1select a, b, c, d from t20 force index(idx_abc) where a=3 order by d limit 10;
這個時候再查看sql執行的optimizer_trace日誌:
1"filesort_priority_queue_optimization": {
2 "limit": 10,
3 "rows_estimate": 27033,
4 "row_size": 49,
5 "memory_available": 32768,
6 "chosen": true
7},
8"filesort_execution": [
9],
10"filesort_summary": {
11 "rows": 11,
12 "examined_rows": 8520,
13 "number_of_tmp_files": 0,
14 "sort_buffer_size": 632,
15 "sort_mode": "<sort_key, rowid>"
16}
能夠發現這個時候切換到了sort_key, rowid
模式,在這個模式下,執行流程以下:
where條件a=3掃描到8520條記錄;
回表查找記錄;
找到這8520條記錄的
id
和d
字段,放入sort buffer中進行堆排序;排序完成後,取前面10條;
取這10條的id回表查詢須要的a,b,c,d字段值;
依次返回結果給到客戶端。
能夠發現,正由於行記錄太大了,因此sort buffer中只存了須要排序的字段和主鍵id,以時間換取空間,最終排序完成,再次從彙集索引中查找到全部須要的字段返回給客戶端,很明顯,這裏多了一次回表操做的磁盤讀,總體效率上是稍微低一點的。
五、order by優化總結
根據以上的介紹,咱們能夠總結出如下的order by語句的相關優化手段:
order by字段儘可能使用固定長度的字段類型,由於排序字段不支持壓縮;
order by字段若是須要用可變長度,應儘可能控制長度,道理同上;
查詢中儘可能不用用select *,避免查詢過多,致使order by的時候sort buffer內存不夠致使外部排序,或者行大小超過了
max_length_for_sort_data
致使走了sort_key, rowid
排序模式,使得產生了更多的磁盤讀,影響性能;嘗試給排序字段和相關條件加上聯合索引,可以用到覆蓋索引最佳。
這篇文章的內容就差很少介紹到這裏了,可以閱讀到這裏的朋友真的是頗有耐心,爲你點個贊。
本文爲arthinking基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,若是你發現了有何錯漏之處,煩請高擡貴手幫忙指正,萬分感激。
你們能夠關注個人博客:itzhai.com
獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。
若是您以爲讀完本文有所收穫的話,能夠關注個人帳號,或者點個贊吧,碼字不易,您的支持就是我寫做的最大動力,再次感謝!
關注個人公衆號,及時獲取最新的文章。
更多文章
JVM系列專題:公衆號發送 JVM
References
[1]: 滴滴雲. MySQL 全表 COUNT(*) 簡述. zhihu.com. Retrieved from https://zhuanlan.zhihu.com/p/54378839
[2]: MySQL. 8.2.1.14 ORDER BY Optimization. Retrieved from https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html
[3]: MySQL:排序(filesort)詳細解析. Retrieved from https://www.jianshu.com/p/069428a6594e
[4]: MYSQL實現ORDER BY LIMIT的方法以及優先隊列(堆排序). Retrieved from http://blog.itpub.net/7728585/viewspace-2130920/
·END·
訪問IT宅(itzhai.com)查看個人博客更多文章
掃碼關注及時獲取新內容↓↓↓
Java後端技術架構 · 技術專題 · 經驗分享
碼字不易,若有收穫,點個「贊」哦~
本文分享自微信公衆號 - Java架構雜談(itread)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。