在編寫SQL 語句時常常會用到 order by 進行排序,那么排序過程是什么樣的?為什么有些排序執行比較快,有些排序執行很慢?又該如何去優化?
索引排序
索引排序指的是在通過索引查詢時就完成了排序,從而不需要再單獨進行排序,效率高。索引排序是通過聯合索引實現的。因為聯合索引是從最左邊的列開始起按大小順序進行排序,如下圖。
比如現在查詢條件是 where sex=1 order by name,那么查詢過程就是會找到滿足 sex=1 的記錄,而符合這條的所有記錄一定是按照 name 排序的,所以也不需要額外進行排序。而如果是 where sex >1 order by name,那么根據 sex>1 得到的記錄 sex 值並不是固定值,所以得到的記錄是按照 sex,其次才是 name 進行排列的。也就沒有實現索引排列。
額外排序
額外排序就是需要額外進行排序。可以分別兩種方式來看待。
按執行位置划分
1、Sort_Buffer
MySQL 為每個線程各維護了一塊內存區域 sort_buffer ,用於進行排序。sort_buffer 的大小可以通過 sort_buffer_size 來設置。如果加載的記錄字段總長度小於 sort_buffer_size 便使用 sort_buffer 排序;如果超過則使用 sort_buffer + 臨時文件進行排序。
2、Sort_Buffer + 臨時文件
MySQL 會使用臨時文件搭配 Sort_Buffer 進行排序。主要是使用歸並算法來得出最終排序后的結果。
臨時文件種類:
臨時表種類由參數 tmp_table_size 與臨時表大小決定,如果內存臨時表大小超過 tmp_table_size ,那么就會轉成磁盤臨時表。因為磁盤臨時表在磁盤上,所以使用內存臨時表的效率是大於磁盤臨時表的。
1、內存臨時表
2、磁盤臨時表 磁盤臨時表默認使用的是 InnoDB,如果想要切換執行引擎,可以修改參數 internal_tmp_disk_storage_engine。
按執行方式划分
執行方式是由 max_length_for_sort_data 參數與用於排序的單條記錄字段長度決定的,如果用於排序的單條記錄字段長度 <= max_length_for_sort_data ,就使用全字段排序;反之則使用 rowid 排序。
1、全字段排序
全字段排序就是將查詢的所有字段全部加載進來進行排序。
優點:查詢快,執行過程簡單
缺點:需要的空間大。
例子(不考慮臨時文件):select city,name,age from t where city='杭州' order by name limit 1000 ; city 有索引
1、初始化 sort_buffer,確定放入兩個字段,即 name 和 id;
2、從索引 city 找到第一個滿足 city='杭州’條件的主鍵 id,也就是圖中的 ID_X;
3、到主鍵 id 索引取出整行,取 name、id 這兩個字段,存入 sort_buffer 中;
4、從索引 city 取下一個記錄的主鍵 id;
5、重復步驟 3、4 直到不滿足 city='杭州’條件為止,也就是圖中的 ID_Y;
6、對 sort_buffer 中的數據按照字段 name 進行排序;
7、遍歷排序結果,取前 1000 行,並按照 id 的值回到原表中取出 city、name 和 age 三個字段返回給客戶端。
2、rowid 排序
rowid 表示位置信息,如果整張表有主鍵那么 rowid 就是主鍵,如果沒有主鍵就會自動創建一個 6 字節的唯一標識。所以 rowid 排序就表示只加載用於排序的字段以及 rowid ,然后進行排序,然后根據排序好的 rowid 去表中回表查詢所要的結果。
缺點:會產生更多次數的回表查詢,查詢可能會慢一些。
優點:所需的空間更小。
例子(不考慮臨時文件):select city,name,age from t where city='杭州' order by name limit 1000 ; city 有索引
1、初始化 sort_buffer,確定放入兩個字段,即 name 和 id;
2、從索引 city 找到第一個滿足 city='杭州’條件的主鍵 id,也就是圖中的 ID_X;
3、到主鍵 id 索引取出整行,取 name、id 這兩個字段,存入 sort_buffer 中;
4、從索引 city 取下一個記錄的主鍵 id;
5、重復步驟 3、4 直到不滿足 city='杭州’條件為止,也就是圖中的 ID_Y;
6、對 sort_buffer 中的數據按照字段 name 進行排序;
7、遍歷排序結果,取前 1000 行,並按照 id 的值回到原表中取出 city、name 和 age 三個字段返回給客戶端。
執行案例分析
rand() 執行
select word from words order by rand() limit 3; 表數據有10000行 SQL是從10000行記錄中隨機獲取3條記錄返回。
分析: 這里查詢的字段只有一個,所以使用全字段查詢。 加上記錄數過多,但是單條記錄的字段長度不長,所以會使用 sort_buffer + 內存臨時表。所以總結來看這條語句會使用 全字段查詢 + sort_buffer + 內存臨時表 來排序。
執行過程:
1、從緩沖池依次讀取記錄,每次讀取后都調用 rand() 函數生成一個 0-1 的數存入內存臨時表,W 是 word 值,R 是 rand() 生成的隨機數。到這掃描了 10000 行。
2、初始化 sort_buffer,從內存臨時表中將 rowid(這張表自動生成的) 以及 排序數據 R 存入 sort_buffer。到這因為要遍歷內存臨時表所以又掃描了 10000 行。
3、在 sort_buffer 中根據 R 排好序,然后選擇前三個記錄的 rowid 逐條去內存臨時表中查到 word 值返回。到這因為取了三個數據去內存臨時表去查找所以又掃描了 3 行。總共 20003 行。
rand() 優化
通過上面的例子可以看出當要從表中隨機獲取幾條記錄使用 rand() 函數是非常消耗資源的,同時觸發了 Using temporary 和 Using filesort。並且進行了 20003 行記錄的掃描,非常消耗資源。所以我們可以自己去計算一個隨機值,避免使用 rand() 函數。
查詢隨機的一條記錄:
1、取得整個表的行數,並記為 C。
2、取得 Y = floor(C * rand())。floor 函數在這里的作用,就是取整數部分。
3、再用 limit Y,1 取得一行。
select count(*) into @C from t;
set @Y = floor(@C * rand());
set @sql = concat("select * from t limit ", @Y, ",1");
prepare stmt from @sql;
execute stmt;
DEALLOCATE prepare stmt;
如果查詢多條,只要將第二步執行多次,然后依次執行就可以了。
使用這樣的方式就可以避免 MySQL 去使用臨時表以及 filesort 排序,提高執行效率。
優先隊列排序算法
在 5.6 中對排序算法進行一些優化,之前使用的是搭配臨時表使用 歸並排序算法,這種方式會對所有的記錄都進行排序,消耗了不必要的資源例如有一個 20000 行記錄的表,執行 select word from words order by rand() limit 3;
因為這條語句只取三條記錄,對這剩余的 19993 行進行排序比較浪費CPU資源且耗時。所以在 5.6 中提出了使用 優先隊列排序算法。還是以這個例子為例,因為查詢的字段只有一個,且查詢的行數很多,所以還是使用 全字段查詢 + sort_buffer + 內存臨時表 。
過程:先讀取前三行記錄並為其分別通過 rand() 函數為其設置一個0-1的隨機數,取這三條記錄的 rowid、隨機數組成一個堆,然后依次設置隨機數並與當前堆中的隨機數比較。如果這個隨機數比堆中某個記錄的隨機數小,就替換,然后移除,如果沒有小的就直接移除,取下一個。最后根據堆中的 rowid 去臨時表中讀取對應的 word 值返回。
失效場景:因為要拿指定的記錄數的排序數據以及rowid去挨個比較,所以如果需要返回的記錄數過多,導致所有的字段長度超過了設置的 sort_buffer_size ,那么此算法就會失效。
索引排序案例
問題:有 (city,name) 聯合索引,select * from t where city in (“杭州”," 蘇州 ") order by name limit 100; 這個 SQL 語句是否需要排序?有什么方案可以避免排序?
答案:需要排序。因為city 的條件有兩個,總體上來看就是以 city優先進行排序的。可以優化成下面三步:
1、執行 select * from t where city=“杭州” order by name limit 100; 這個語句是不需要排序的,客戶端用一個長度為 100 的內存數組 A 保存結果。
2、執行 select * from t where city=“蘇州” order by name limit 100; 用相同的方法,假設結果被存進了內存數組 B。
3、現在 A 和 B 是兩個有序數組,然后你可以用歸並排序的思想,得到 name 最小的前 100 值,就是我們需要的結果了。
如果將 " limit 100" 改成 " limit 10000,100 "。可以優化成下面三步:
1、select id,name from t where city="杭州" order by name limit 10100;
2、select id,name from t where city="蘇州" order by name limit 10100。
3、用歸並排序的方法取得按 name 順序第 10001~10100 的 name、id 的值,然后拿着這 100 個 id 到數據庫中去查出所有記錄。
優化總結
優化總體上就是圍繞 “盡量不使用額外排序,避免使用臨時表” 的原則。
1、盡量使用索引完成排序,如果該查詢語句執行的頻率比較高,可以為其創建一個聯合索引。而如果使用的頻率很低,那么就不需要去創建,因為索引的維護需要成本。
2、如果需要額外去排序,那么可以適當調整 sort_buffer_size(sort_buffer) 和 tmp_table_size(內存臨時表) ,使排序只在內存中執行。
3、如果 sort_buffer 空間設置足夠大,也可以適當調整 max_length_for_sort_data 的值,使用全字段排序。
4、對於一些比較耗時的函數可以自定義算法去實現,避免計算過程在 MySQL 中實現。