MySQL排序速度慢而且可能不穩定

  • 2019 年 11 月 12 日
  • 筆記

一、具體現象

有一個功能,按照演算法得出的權重值,分頁展示一批列表數據,權重值越大越靠前。研發同學回饋查詢速度慢且排序不穩定。

排序不穩定的具體現象,有不少記錄存在相同權重值,某條記錄(假設id=100)第一頁出現了,翻到第二頁可能還有它(採用的limit控制哪一頁)。

第1頁數據

第2頁數據

一個主表A,左連接兩個表B、C,根據C的權重欄位排序。具體SQL如下

二、問題分析

查看SQL語句的執行計劃(EXPLAIN),發現有Using filesort的字樣。

趕緊搜索一下MySQL說明文檔,第一條是排序優化

文檔中有這麼一句話「如果索引不能滿足ORDERBY子句,MySQL將執行文件排序(filesort)操作,讀取數據行並對其進行排序。文件排序構成查詢執行中的額外排序階段。」

顯然,利用索引實現有序,比採用filesort更高效。filesort並不一定都通過磁碟排序,數據量不大的時候是在記憶體里完成。速度不夠快的原因找到了。

filesort的時候可能在記憶體中出現堆排序列或快速排序兩種方式,具體使用哪一種排序方式是優化器決定的,基本原則如下

  • 快速排序演算法:大量排序
  • 堆排序演算法:排序量不大

快速排序和堆排序是不穩定的排序演算法,對於重複值不能保證順序。Order by排序不穩定的原因也定位到了

了解一下filesort的原理

(1)根據表的索引或者全表掃描,讀取所有滿足條件的記錄。

(2)對於每一行,存儲一對值到緩衝區(排序列,行記錄指針),一個是排序的索引列的值,即order by用到的列值,和指向該行數據的行指針,緩衝區的大小為sort_buffer_size大小。

(3)當緩衝區滿後,運行一個快速排序(qsort)來將緩衝區中數據排序,並將排序完的數據存儲到一個臨時文件,並保存一個存儲塊的指針,當然如果緩衝區不滿,則不會重建臨時文件了。

(4)重複以上步驟,直到將所有行讀完,並建立相應的有序的臨時文件。

(5)對塊級進行排序,這個類似歸併排序演算法,只通過兩個臨時文件的指針來不斷交換數據,最終達到兩個文件,都是有序的。

(6)重複5直到所有的數據都排序完畢。

(7)採取順序讀的方式,將每行數據讀入記憶體(這裡讀取數據時並不是一行一行讀),並取出數據傳到客戶端,讀取快取大小由read_rnd_buffer_size來指定。

三、怎麼優化

1、利用索引達到排序目的(針對例子的優化)

針對文章開始的例子,優化原則是Use of Indexes to Satisfy ORDER BY(讓ORDER BY用上索引),即提升查詢效率,又保證穩定性(索引B+樹葉子結點的順序是唯一且一定的)

MySQL的文檔列出若干具體的case,把最主要整理出來如下。

MySQL文檔中有這麼一句話 「該查詢連接了許多表,並且ORDER BY中的列並非全部來自用於檢索行的第一個非恆定表。」,滿足這類型的SQL也不能利用索引排序。這就是文章開頭的例子。另外,使用別名,如果跟表的列名衝突可能導致索引排序失效。

看到有些文章寫到下面這條語句ORDER BY不能利用索引

這個說法顯然與MySQL官方文檔不一致。我覺得,這個語句能不能使用索引,跟資料庫引擎根據開銷決定是否檢索的階段使用索引有關。

2、優化filesort

如果確實沒辦法利用索引,可以想辦法優化filesort排序。

如果結果集太大記憶體裝不下,filesort將根據需要使用臨時磁碟文件。磁碟io速度你懂的!MySQL官方建議可以調大排序快取參數sort_buffer_size,MySQL 8.0還對快取利用率做了優化,調大一點也不浪費。以前版本的MySQL可以求助DBA。

可以這樣優化的典型SQL 語句如下

3、其他

有些ORDER BY甚至連filesort都不能用,對這類優化感覺有點超綱了,把原文貼一下

「對於不使用filesort的慢排序查詢,請嘗試將「max_length_for_sort_data」參數調低到適合觸發filesort的值(此參數的值設置得過高的一個表現是磁碟IO高和CPU負載低)」

四、實際效果

文章開頭例子的一種場景,我們巧妙利用了索引排序,達到很好的效果。

另外一個場景仍然使用filesort的排序方式

當然更好的做法是接入ES之類的搜索引擎