算法的藝術:MySQL order by對各類排序算法的巧用

【精華】洞悉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進行排序。

因此這個執行流程以下圖所示:

  1. 經過where條件a=3掃描到8520條記錄;

  2. 回表查找記錄;

  3. 把8520條記錄中須要的字段放入sort buffer中;

  4. 在sort buffer中進行堆排序;

  5. 在排序好的結果中取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進行快速排序。

因此這個執行流程以下圖所示:

  1. 經過where條件a=1掃描到3條記錄;

  2. 回表查找記錄;

  3. 把3條記錄中須要的字段放入sort buffer中;

  4. 在sort buffer中進行快速排序

  5. 在排序好的結果中取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條。執行計劃以下:

image-20200614171147989

能夠發現,這裏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模式,在這個模式下,執行流程以下:

  1. where條件a=3掃描到8520條記錄;

  2. 回表查找記錄;

  3. 找到這8520條記錄的idd字段,放入sort buffer中進行堆排序;

  4. 排序完成後,取前面10條;

  5. 取這10條的id回表查詢須要的a,b,c,d字段值;

  6. 依次返回結果給到客戶端。


能夠發現,正由於行記錄太大了,因此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後端技術架構 · 技術專題 · 經驗分享

blog: itzhai.com


碼字不易,若有收穫,點個「贊」哦~




本文分享自微信公衆號 - Java架構雜談(itread)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索