MySQL 優化 —— ORDER BY 優化

引言

本文翻譯自MySQL 官網:ORDER BY Optimization,MySQL 版本:5.7。

這一部分描述了MySQL何時會使用索引來滿足order by子句,filesort 操作會在索引不能生效的時候被用到,以及優化器對order by的執行計劃信息。

order by後面有沒有跟着limit,可能會返回不同的記錄順序。

一、使用索引來滿足 ORDER BY

某些情況,MySQL可能會使用索引來滿足order by子句並避免因執行 filesort 操作造成的額外的排序開銷。

即便order by子句並沒有完全與索引匹配,可能也會用到索引,只要索引所有未使用的部分和所有額外的order by 字段在where子句中都是常量。如果索引沒有包含查詢的所有字段,那麼只有當索引訪問比其他訪問方法開銷更小的時候纔會用到索引。

假設現在有一個索引覆蓋了key_part1, key_part2這兩個字段,下面的查詢可能會用索引來解決order by 的部分。優化器是否真會這麼做取決於如果有一個字段必須查詢而它又不在索引中的情況下,讀取索引是否比全表掃描更有效。

  • 在這個查詢中,兩個字段的複合索引可以使優化器避免排序。
SELECT * FROM t1
  ORDER BY key_part1, key_part2;

但是,查詢使用的是select *,這可能會查詢出比索引列(key_part1 , key_part2)更多的字段。這種情況,掃描整個索引並在表中記錄裏查找哪些不是索引列的開銷可能要比掃描整個表然後對結果集排序還要大。如果這樣,優化器通常就不會使用索引。如果select * 只查詢了索引列,那麼就會用到索引並避免排序操作。

如果表是一個InnoDB 表,那麼表的主鍵默認也是索引的一部分,那麼下面的查詢就可以使用索引來解決order by:

SELECT pk, key_part1, key_part2 FROM t1
  ORDER BY key_part1, key_part2;
  • 在這個查詢中key_part1是常量,所有通過索引訪問到的記錄都會按照key_part2 來排序,並且如果where子句有足夠的選擇性使得索引範圍掃描比全表掃描開銷更小的話,那麼覆蓋了(key_part1, key_part2)的複合索引就可以避免排序操作。
SELECT * FROM t1
  WHERE key_part1 = constant
  ORDER BY key_part2;
  • 下面的兩個查詢,和之前沒有desc的同一查詢,索引的使用情況類似。
SELECT * FROM t1
  ORDER BY key_part1 DESC, key_part2 DESC;
SELECT * FROM t1
  WHERE key_part1 = constant
  ORDER BY key_part2 DESC;
  • 下面的兩個查詢,key_part1 和一個常量進行比較。如果 WHERE 子句有足夠的可選擇性來使索引區間掃描比全表掃描更優,那麼索引就會被使用。
SELECT * FROM t1
  WHERE key_part1 > constant
  ORDER BY key_part1 ASC;

SELECT * FROM t1
  WHERE key_part1 < constant
  ORDER BY key_part1 DESC;
  • 下面的查詢, ORDER BY 沒有使用 key_part1,但是所有被查詢的行都有一個常量的 key_part1 的值,因此依然會使用索引:
SELECT * FROM t1
  WHERE key_part1 = constant1 AND key_part2 > constant2
  ORDER BY key_part2;

某些情況,MySQL 不會使用索引來處理 ORDER BY,即便仍然會在 WHERE 子句進行匹配操作時用到索引。例如下面這些:

  • 查詢中的 ORDER BY 使用了不同的 索引:
SELECT * FROM t1 ORDER BY key1, key2;
  • 查詢中的 ORDER BY 使用了不連續的索引部分:
SELECT * FROM t1 WHERE key2=constant ORDER BY key1_part1, key1_part3;
  • 查詢中的 ORDER BY 混用了 ASC 和 DESC:
SELECT * FROM t1 WHERE key2=constant ORDER BY key1_part1, key1_part3;
  • 用於查詢記錄的索引與 ORDER BY 中的索引不是同一個:
SELECT * FROM t1 WHERE key2=constant ORDER BY key1;
  • 查詢中的 ORDER BY 包含了一個表達式,這個表達式包含索引列名之外的其他項:
SELECT * FROM t1 ORDER BY ABS(key);
SELECT * FROM t1 ORDER BY -key;
  • 查詢連接了多張表,並且 ORDER BY 中的字段並不都來自於第一個非常量數據表(博主:如果所有的 ORDER BY 和 GROUP BY子句中的字段都來自同一個表,那麼這個表在連接時就作爲第一張表。這裏的意思是說,如果 ORDER BY 中的字段來自於不同的表,那麼就不會用到索引)
  • 查詢中包含不同的 ORDER BY 和 GROUP BY 表達式。
  • ORDER BY 子句中的字段只有字段的前綴有索引。這種情況,索引就不會完全參與排序操作。例如,有一個列聲明爲 CHAR(20),但是隻有前10個字節建立了索引,那麼索引就無法區分(博主:排序最本質的工作就是比較大小,這裏的區分就是比較的意思)剩餘的10個字節,這時就會使用 filesort 。
  • 索引沒有以一定的順序存儲記錄。例如,memory 存儲引擎中用到的 HASH 索引就是這種不會對記錄進行排序的索引。

另外,用於排序的索引究竟可不可用還會受到字段別名的影響。假設 t1 表的 t1.a 字段建立了索引。下面的語句中,查詢列表中有這個字段 a 。它代表 t1.a ,因爲 a 已經建立了索引,所以下面的語句就會用到索引:

SELECT a FROM t1 ORDER BY a;

下面的語句中,查詢列表依然有 a 列,但此 a 非彼 a,這個 a 是一個別名。它代表的是 ABS(a),因爲 索引是建立在 a 列上,所以 t1.a 上的索引不會生效:

SELECT ABS(a) AS a FROM t1 ORDER BY a;

下面的語句中, ORDER BY 對 a 進行排序,而在查詢列表中,並沒有叫 a 的列。但是 t1 表中有一個列叫做 a,因此 ORDER BY 指向的是 t1.a ,t1.a 上的索引就會生效。(當然,排序的結果可能與 ORDER BY ABS(a) 的排序完全不同)

SELECT ABS(a) AS b FROM t1 ORDER BY a;

 默認地,MySQL 會對 GROUP BY col1, col2, ... 進行排序,就好像你的查詢中依然包含 ORDER BY col1, col2, ... 一樣。如果你顯式地包含一個 ORDER BY  子句,包含了相同的字段列表,MySQL 會把它優化掉,排序依然會發生,且不會有性能犧牲。

如果一個查詢包含 GROUP BY 但是你想避免對結果排序的開銷。你可以通過指定 ORDER BY NULL 來抑制排序。例如:

INSERT INTO foo
SELECT a, COUNT(*) FROM bar GROUP BY a ORDER BY NULL;

優化器可能依然會用排序來實現分組操作。ORDER BY NULL 抑制了對結果的排序,分組操作之前不會有排序來決定結果。

注意:

GROUP BY 默認隱式排序(即 GROUP BY 的字段沒有 ASC 或 DESC)。然而,依賴隱式 GROUP BY 排序或顯式 GROUP BY (即使用聲明的 ASC 或 DESC 來對 GROUP BY 的字段排序)排序都是不推薦的。要排序,請用 ORDER BY 子句。

二、 使用 filesort 來滿足 ORDER BY

如果索引已經無法滿足 ORDER BY 子句,MySQL 會執行 filesort 操作,它的意思是——讀取表中數據然後排序。filesort 會在查詢執行的時候有額外的排序時間。

爲了獲取用於 filesort 操作的內存,優化器會預先分配一個固定大小爲 sort_buffer_size 個字節。每一個session 會話可以通過改變這個值來避免過度的內存消耗,或者在必要時分配更多內存。

如果結果集真的大到內存已經無法裝下,那麼 filesort 操作會在這種必要的時候使用臨時的磁盤文件。有些類型的查詢尤其適合完成內存內的 filesort 操作。例如,下面的查詢形式,優化器就可以使用 filesort 在內存中,而不是使用臨時文件來高效地處理 ORDER BY 操作:

SELECT ... FROM single_table ... ORDER BY non_index_column [DESC] LIMIT [M,]N;

下面的查詢在 web 應用中非常常見,它們用於讓一個很大的結果集展示很少的一些記錄。例如:

SELECT col1, ... FROM t1 ... ORDER BY name LIMIT 10;
SELECT col1, ... FROM t1 ... ORDER BY RAND() LIMIT 15;

三、影響 ORDER BY 優化

對於連 filesort 都無法生效的慢查詢,嘗試把 max_length_for_sort_data 系統變量調低到一個適合觸發 filesort 的值。(一個該值設置地太高的症狀是系統同時會出現高磁盤活動和低CPU 活動

爲了加快 ORDER BY 的速度,檢查一下你是否可以使用索引而不是額外的排序時間(博主:即使用 filesort)。如果無法做到,嘗試下面的優化策略:

  • 調大 sort_buffer_size 變量的值。理想狀態下,這個值應該大到足夠整個結果集可以容納在排序緩衝區(爲了避免磁盤寫入和合並次數),但是該值最小也必須要足夠容納15個元組。(合併最多15個臨時磁盤文件,每個文件必須有至少一個元組的內存空間)。
  • 考慮到存儲在排序緩衝區中的列值的大小受到 max_sort_length 系統變量的影響。例如,如果元組存儲長字符串字段的值,並且你調大了 max_sort_length 的值,那麼排序緩衝區元組的大小也會變大,並且你可能同時需要增大 sort_buffer_size 的大小。對於作爲字符串表達式結果計算的列值(例如那些需要一個字符串參數的函數),filesort 算法無法得知表達式的最大長度,所以就一定會給每個元組分配 max_sort_length 的字節數。如果要監控合併次數(合併臨時文件),可以檢查 Sort_merge_passes 狀態變量。
  • 調大 read_rnd_buffer_size 變量可以在同一時間讀取更多的記錄。
  • 改變 tmpdir 系統變量的值,使其指向一個具有大量空閒空間的專用文件系統。該變量值會列出幾個以循環方式使用的路徑。使用這個功能可以讓負載分散到多個目錄。在Unix 系統上以冒號(:)分隔路徑,在 Windows 系統上以分號(;)分隔路徑。路徑應該命名位於不同物理磁盤上的文件系統中的目錄,而不是同一磁盤上的不同分區。

四、ORDER BY 的執行計劃信息

使用 EXPLAIN ,你可以檢查MySQL 是否有用到索引來解決 ORDER BY 子句:

  • 如果在輸出的執行計劃中 Extra 列不包含 "Using filesort" ,那麼索引排序生效,filesort 不會執行。
  • 如果在輸出的執行計劃中 Extra 列包含 "Using filesort",那麼排序就沒有用到索引,filesort 就會執行。

另外,如果 filesort 執行了,優化器跟蹤輸出包括一個 filesort_summary 塊,例如:

"filesort_summary": {
  "rows": 100,
  "examined_rows": 100,
  "number_of_tmp_files": 0,
  "sort_buffer_size": 25192,
  "sort_mode": "<sort_key, packed_additional_fields>"
}

sort_mode 值提供了一些關於排序緩衝區中的元組的信息。

  • <sort_key, rowid> :它表示排序緩衝區元組是包含原錶行的排序鍵值和行ID的對。元組被排序鍵排序,記錄 ID 被用於從表中讀取數據。
  • <sort_key, additional_fields>:它表示排序緩衝區元組包含排序鍵值和查詢涉及到的字段。元組被排序鍵排序,字段值會直接從元組中讀取。
  • <sort_key, packed_additional_fields>:與前面的變體類似,但是附加的列是緊密地打包在一起的,而不是使用固定長度的編碼。

EXPLAIN 不會分辨優化器是否會執行內存中的 filesort 。內存 filesort 的使用可以在優化器的跟蹤報告中看到。

總結

關於 ORDER BY 的優化部分,的確是非常複雜,其中比較重要的是關於 ORDER BY 如何利用索引的具體條件。在某些場合下,ORDER BY 是不會使用到索引的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章