MySQL實踐之排序分析

一、 前言

本文描述了團隊在工作中遇到的一個MySQL分頁查詢問題,順帶講解相關知識點,為后來者鑒。本文重點不是"怎樣"優化表結構和SQL語句,而是探索不同查詢方式"為什么"會有顯著差異,涉及下列知識點:

  • MySQL 延遲關聯
  • MySQL Optimizer Trace使用
  • MySQL 排序原理

二、 問題

工作中用到了一張表,字段比較多,每行大概500字節,總行數大概80萬。場景中,需要根據某個非索引字段排序,然后進行分頁讀取。現把脫敏之后的表結構奉上(只修改了字段名),通過簡單腳本插入80萬行模擬數據即可以測試了。

2.1 表結構

CREATE TABLE `t` (
 `a0` varchar(16) NOT NULL,
 `a1` bigint(20) NOT NULL,
 `a2` decimal(27,9) NOT NULL DEFAULT '0', `a3` decimal(27,9) NOT NULL DEFAULT '0',
 `a4` decimal(27,9) NOT NULL DEFAULT '0', `a5` decimal(27,9) NOT NULL DEFAULT '0',
 `a6` decimal(27,9) NOT NULL DEFAULT '0', `a7` decimal(27,9) NOT NULL DEFAULT '0',
 `a8` decimal(18,9) NOT NULL DEFAULT '0', `a9` decimal(18,9) NOT NULL DEFAULT '0',
 `b1` decimal(27,9) NOT NULL DEFAULT '0', `b2` decimal(27,9) NOT NULL DEFAULT '0',
 `b3` decimal(18,9) NOT NULL DEFAULT '0', `b4` decimal(18,9) NOT NULL DEFAULT '0',
 `b5` decimal(18,9) NOT NULL DEFAULT '0', `b6` decimal(18,9) NOT NULL DEFAULT '0',
 `b7` decimal(18,9) NOT NULL DEFAULT '0', `b8` decimal(18,9) NOT NULL DEFAULT '0',
 `b9` decimal(18,9) NOT NULL DEFAULT '0', `c1` decimal(18,9) NOT NULL DEFAULT '0',
 `c2` decimal(18,9) NOT NULL DEFAULT '0', `c3` decimal(18,9) NOT NULL DEFAULT '0',
 `c4` decimal(18,9) NOT NULL DEFAULT '0', `c5` decimal(18,9) NOT NULL DEFAULT '0',
 `c6` decimal(18,9) NOT NULL DEFAULT '0', `c7` int(11) NOT NULL DEFAULT '0',
 `c8` int(11) NOT NULL DEFAULT '0', `c9` int(11) NOT NULL DEFAULT '0',
 `d1` int(11) NOT NULL DEFAULT '0', `d2` int(11) NOT NULL DEFAULT '0',
 `d3` decimal(18,9) NOT NULL DEFAULT '0', `d4` decimal(18,9) NOT NULL DEFAULT '0',
 `d5` decimal(18,9) NOT NULL DEFAULT '0', `d6` decimal(18,9) NOT NULL DEFAULT '0',
 `d7` decimal(18,9) NOT NULL DEFAULT '0', `d8` decimal(18,9) NOT NULL DEFAULT '0',
 `d9` int(11) NOT NULL DEFAULT '0', `e1` decimal(27,9) NOT NULL DEFAULT '0',
 `e2` decimal(27,9) NOT NULL DEFAULT '0', `e3` decimal(27,9) NOT NULL DEFAULT '0',
 `e4` decimal(18,9) NOT NULL DEFAULT '0', `e5` decimal(18,9) NOT NULL DEFAULT '0',
 `e6` decimal(18,9) NOT NULL DEFAULT '0', `e7` decimal(18,9) NOT NULL DEFAULT '0',
 `e8` decimal(18,9) NOT NULL DEFAULT '0', `e9` decimal(18,9) NOT NULL DEFAULT '0',
 `f1` decimal(18,9) NOT NULL DEFAULT '0', `f2` decimal(18,9) NOT NULL DEFAULT '0',
 PRIMARY KEY (`a0`,`a1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

查詢顯式用到的字段是a0和a1。再看一下表信息:

#  select * from information_schema.tables where table_name='t'\G
SELECT * FROM tables where table_name='t'\G
*************************** 1. row ***************************
  TABLE_CATALOG: def
   TABLE_SCHEMA: test
     TABLE_NAME: t
     TABLE_TYPE: BASE TABLE
         ENGINE: InnoDB
        VERSION: 10
     ROW_FORMAT: Compact
     TABLE_ROWS: 799304
 AVG_ROW_LENGTH: 482
    DATA_LENGTH: 385875968
MAX_DATA_LENGTH: 0
   INDEX_LENGTH: 0
      DATA_FREE: 5242880
 AUTO_INCREMENT: NULL
    CREATE_TIME: 2017-01-13 09:34:59
    UPDATE_TIME: NULL
     CHECK_TIME: NULL
TABLE_COLLATION: utf8_general_ci
       CHECKSUM: NULL
 CREATE_OPTIONS:
  TABLE_COMMENT:

2.2 兩種查詢

最初,使用最直接的查詢語句,耗時6.67秒:

SQL_1:   SELECT * FROM t ORDER BY a1 DESC LIMIT 100000,1;

隨后,使用"延遲關聯",耗時0.90秒:

SQL_2:   SELECT * FROM t INNER JOIN (SELECT a0, a1 FROM t ORDER BY a1 DESC LIMIT 100000,1) AS A USING (a0, a1);

延遲關聯,先根據條件查詢需要的主鍵,再根據主鍵關聯原表獲得需要的數據。

問題: 為什么SQL_2比SQL_1執行快那么多?

三、背景知識

工欲善其事,必先利其器。MySQL查詢分析的利器,就似乎其自帶的"MySQL Optimizer Trace"。 另外,還必須對MySQL的查詢執行過程有一個基本了解,特別是排序過程。

3.1 Optimizer Trace 使用簡介

Optimizer Trace 是MySQL 5.6.3里新加的一個特性,可以把MySQL Optimizer的決策和執行過程輸出成文本,結果為JSON格式,兼顧了程序分析和閱讀的便利。

【使用方法】

  1. 啟用Optimizer Trace,它默認是關閉的。
    SET optimizer_trace="enabled=on";
  2. 設置Trace使用的內存,默認內存比較小,有時候不夠用:
    SET optimizer_trace_max_mem_size=1024000;
  3. 執行SQL語句
    SQL_1: SELECT * FROM t ORDER BY a1 DESC LIMIT 100000,1;
    SQL_2: SELECT * FROM t INNER JOIN (SELECT a0, a1 FROM t ORDER BY a1 DESC LIMIT 100000,1) AS A USING (a0, a1);
  4. 查看Trace輸出
    select trace from information_schema.optimizer_trace\G

Trace輸出分為3大部分,如下,分別對應到mysql中的三個函數: JOIN::prepare(), JOIN::optimize(), JOIN::exec()

{
    trace: {
        steps: [
            { join_preparation: {} },   <--> JOIN::prepare()
            { join_optimization: {} },  <--> JOIN::optiomize()
            { join_execution: {}    }   <--> JOIN::exec()
        ]
    }
} 

【系統參數】
追蹤行為完全由OPTIMIZER_TRACE系列參數控制,關于這些參數的詳細說明參考MySQL在線文檔。

#  show variables like '%optimizer_trace%';
+------------------------------+----------------------------------------------------------------------------+
| Variable_name                | Value                                                                      |
+------------------------------+----------------------------------------------------------------------------+
| optimizer_trace              | enabled=off,one_line=off                                                   |
| optimizer_trace_features     | greedy_search=on,range_optimizer=on,dynamic_range=on,repeated_subselect=on |
| optimizer_trace_limit        | 1                                                                          |
| optimizer_trace_max_mem_size | 16384                                                                      |
| optimizer_trace_offset       | -1                                                                         |
+------------------------------+----------------------------------------------------------------------------+

一般只要關心optimizer_trace/optimizer_trace_max_mem_size這兩個參數。optimizer_trace可以開啟和關閉追蹤,結果打印方式等,optimizer_trace_max_mem_size決定追蹤工具最多可以使用多少內容。

optimizer_trace    enabled=on  啟用追蹤
                   enabled=off 不啟用追蹤
                   one_line=on TRACE輸出在一行里面,便于程序處理
                   one_line=off TRACE輸出在多行,便于閱讀
optimizer_trace_max_mem_size   追蹤時最多允許使用多少內存,內存太小可能輸出不完整。

這些參數都是基于SESSION的,并且optimizer_trace默認情況下沒有開啟,使用起來很安全。隨后的章節會有詳細實用案例,此處不再贅述。

3.2 MySQL排序簡介

對于MySQL排序有篇文章寫的不錯:MySQL排序內部原理探秘, 也可以閱讀MySQL源代碼的filesort.cc。這里挑選幾個重點列舉下:

  1. MySQL會把需要排序的數據從磁盤讀取到"Sort_buffer"。放到Sort_buffer的字段由 max_length_for_sort_data 決定:
    a) 字段總長度>max_length_for_sort_data,讀取"排序字段+RowID"。這種方式稱為回表模式,記為:< sort_key,rowid>
    b) 字段總長度<=max_length_for_sort_data,讀取"排序字段+SELECT字段+WHERE字段"。這種方式稱為不回表模式,記為:<sort_key,additional_fields>
  2. Sort_buffer有大小限制,在我們的場景中,是8MB。
  3. 數據量超過Sort_buffer時,會初步排序,然后寫入外部臨時表。
  4. 若使用了臨時表,通過"多路歸并排序"逐步合并,直到最后輸出有序結果。
  5. 使用了臨時表時,查詢性能一般來說會急劇下降。
  6. 對于帶Order By+Limit的語句,會進行"優先隊列"評估,如果適用,可以只取若干元素,加速排序。

優先隊列評估 是非常重要的一個步驟,對TopN查詢性能提升很大,過程簡要描述如下:

* 估算數據表的數據總量上限 N
* 計算需要返回的數據總量 M = (LIMIT + OFFSET)
* 計算Sort_buffer容量 X
* Case 1: 如果 X > N
    Case 1.1: 如果 M < N / PQ_slowness,啟用優先隊列
    Case 1.2: 否則,不啟用優先隊列,而是直接用快速排序。
* Case 2: 如果 X > M + 1,啟用優先隊列
* Case 3: 如果當前是不回表模式,嘗試去除非排序字段重新計算Sort_buffer容量 Y,
    Case 3.1 如果 Y > M + 1,估算啟用優先隊列+回表模式的代價 C1,估算使用臨時表多路歸并+不回表模式的代價 C2
        Case 3.1.1: 如果 C1 < C2,啟用優先隊列,并修改為回表模式;
        Case 3.1.2: 否則不啟用優先隊列。
* Final: 各條件不滿足,不啟用優先隊列。
圖3-1 優先隊列評估過程

四、 觀察案例中SQL的執行

準備測試環境:

#  SHOW variables like '%sort%';
+--------------------------------+---------------------+
| Variable_name                  | Value               |
+--------------------------------+---------------------+
| max_length_for_sort_data       | 1024                |
| sort_buffer_size               | 8388608             |
+--------------------------------+---------------------+

#  SET optimizer_trace="enabled=on";
#  SET optimizer_trace_max_mem_size=1000000;

#  SELECT trace FROM information_schema.optimizer_trace\G

注意兩個重要變量

max_length_for_sort_data: 1024
sort_buffer_size: 8388608

4.1 SQL_1 執行過程

SELECT * FROM t ORDER BY a1 DESC LIMIT 100000,1;

圖4-1 SQL_1 執行過程

解析

* ORDER BY a1, 排序字段a1無索引,使用全文掃描
* SELECT *, 需要讀取所有字段
* SELECT字段+排序字段+WHERE字段長度=454,454 < max_length_for_sort_data(1024),使用不回表模式。
* 查詢中存在Order By+Limit,需進行優先隊列評估
    Case 1 不滿足: 1850799*454 > 8388608
    Case 2 不滿足: 100001*454 > 8388608
    Case 3.1 滿足: 74*(100001+1) < 8388608
        計算PQ代價 C1 = 1.18e9
        計算外排代價 C2 = 3.04e6
        C1 > C2,優先隊列得不償失,不啟用
* 執行結果:無優先隊列,不回表模式,使用了46個臨時表

4.2 SQL_2 執行過程

SELECT * FROM t INNER JOIN (SELECT a0, a1 FROM t ORDER BY a1 DESC LIMIT 100000,1) AS A USING (a0, a1);

圖4-2 SQL_2 執行過程

解析

*首先執行子查詢 SELECT a0, a1 FROM t ORDER BY a1 DESC LIMIT 100000,1
    SELECT a0,a1, 只需要讀取a0和a1兩個字段
    ORDER BY a1, 排序字段a1無索引,使用全文掃描
    SELECT字段+排序字段+WHERE字段長度=66,66 < max_length_for_sort_data(1024),使用不回表模式。
    查詢中存在 Order By+Limit,進行優先隊列評估
        Case 1 不滿足: 1850799*66 > 8388608
        Case 2 可滿足: 100001*66 > 8388608
        啟用優先隊列
    執行結果:啟用優先隊列,不回表模式,沒有使用臨時表
    執行完畢,結果存儲在臨時表A里面
* 臨時表A只有1行,連表查詢時,可以使用 t 的主鍵,非常快速。

4.3 小結

  • 全表掃描時,SQL_1需要讀取所有字段(大約500字節),SQL_2只需要讀取2個字段(小于100字節)。
  • SQL1需要使用外部排序,臨時表數量又比較多(46個),所以比較慢。
  • SQL2可以啟用優先隊列優化,排序用數據全部存放在sort_buffer,加速明顯。

五、場景擴展

5.1 Limit 大小對執行過程的影響

稍微修改下SQL語句,把 LIMIT 100000,1 修改為 LIMIT 600000,1,再看執行過程。

5.1.1 SQL_1 執行過程

SELECT * FROM t ORDER BY a1 DESC LIMIT 600000,1;
1 row in set (8.82 sec)

圖5-1 Limit 600000 時 SQL_1 執行過程

解析
這次的執行結果和4.1完全一樣,還是"無優先隊列+不會表模式",但決策依據稍有不同:(600001+1)*74 > 8388608,Case 3.1不滿足,而之前是Case 3.1.1不滿足。

5.1.2 SQL_2 執行過程

SELECT * FROM t INNER JOIN (SELECT a0, a1 FROM t ORDER BY a1 DESC LIMIT 600000,1) AS A USING (a0, a1);
1 row in set (1.05 sec)
圖5-2 Limit 600000 時 SQL_2 執行過程

解析

  1. 這次查詢比4.2慢,因為Case 2和Case 3.1都不滿足,未啟用優先隊列,使用"無優先隊列+不回表模式"。
  2. 但是和5.1.1相比,子查詢的數據總量變少,只使用了8個臨時表,還是更快。

5.2 回表排序的影響

以上測試,都是不回表模式,如果是回表模式,會怎么樣呢?先把關鍵參數設置更小,迫使MySQL使用回表模式:SET max_length_for_sort_data=100;

5.2.1 SQL_1 執行過程

SELECT * FROM t ORDER BY a1 DESC LIMIT 600000,1;
1 row in set (4.16 sec)

解析

  1. 因為(SELECT字段+排序字段+WHERE字段)(454字節) > max_length_for_sort_data(100),這次使用了回表模式,但還是無法啟用優先隊列優化。
  2. 因為排序時只需要讀取排序字段和rowid,臨時表數量減少到8個。

5.2.2 SQL_2 執行過程

SELECT * FROM t INNER JOIN (SELECT a0, a1 FROM t ORDER BY a1 DESC LIMIT 600000,1) AS A USING (a0, a1);
1 row in set (1.05 sec)

解析
由于(SELECT字段+排序字段+WHERE字段)(66字節) < max_length_for_sort_data(100),依然還是"無優先隊列+不回表模式"",使用了8個臨時表。

六、參考資料

  1. MySQL: The Optimizer Trace
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,362評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,013評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,346評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,421評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,146評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,534評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,585評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,767評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,318評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,074評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,258評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,828評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,486評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,916評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,156評論 1 290
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,993評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,234評論 2 375

推薦閱讀更多精彩內容