在編寫SQL 語句時經常會用到 order by 進行排序,那麼排序過程是什麼樣的?爲何有些排序執行比較快,有些排序執行很慢?又該如何去優化?算法
索引排序指的是在經過索引查詢時就完成了排序,從而不須要再單獨進行排序,效率高。索引排序是經過聯合索引實現的。由於聯合索引是從最左邊的列開始起按大小順序進行排序,以下圖。sql
好比如今查詢條件是 where sex=1 order by name,那麼查詢過程就是會找到知足 sex=1 的記錄,而符合這條的全部記錄必定是按照 name 排序的,因此也不須要額外進行排序。而若是是 where sex >1 order by name,那麼根據 sex>1 獲得的記錄 sex 值並非固定值,因此獲得的記錄是按照 sex,其次纔是 name 進行排列的。也就沒有實現索引排列。數據庫
額外排序就是須要額外進行排序,也就是在 explain 的 extra 列中出現 using filesort。能夠分別兩種方式來看待。數組
MySQL 爲每一個線程各維護了一塊內存區域 sort_buffer ,用於進行排序。sort_buffer 的大小能夠經過 sort_buffer_size 來設置。若是加載的記錄字段總長度(多是全字段也多是 rowid排序的字段)小於 sort_buffer_size 便使用 sort_buffer 排序;若是超過則使用 sort_buffer + 臨時文件進行排序。函數
MySQL 會使用臨時文件搭配 Sort_Buffer 進行排序。主要是使用歸併算法來得出最終排序後的結果。
優化
臨時文件種類:spa
臨時表種類由參數 tmp_table_size 與臨時表大小決定,若是內存臨時表大小超過 tmp_table_size ,那麼就會轉成磁盤臨時表。由於磁盤臨時表在磁盤上,因此使用內存臨時表的效率是大於磁盤臨時表的。線程
一、內存臨時表 blog
二、磁盤臨時表 磁盤臨時表默認使用的是 InnoDB,若是想要切換執行引擎,能夠修改參數 internal_tmp_disk_storage_engine。 排序
執行方式是由 max_length_for_sort_data 參數與用於排序的單條記錄字段長度決定的,若是用於排序的單條記錄字段長度 <= max_length_for_sort_data ,就使用全字段排序;反之則使用 rowid 排序。
全字段排序就是將查詢的全部字段所有加載進來進行排序。
優勢:查詢快,執行過程簡單
缺點:須要的空間大。
例子(不考慮臨時文件):select city,name,age from t where city='杭州' order by name limit 1000 ; city 有索引
一、初始化 sort_buffer,肯定放入兩個字段,即 name 和 id;
二、從索引 city 找到第一個知足 city='杭州’條件的主鍵 id,也就是圖中的 ID_X;
三、到主鍵 id 索引取出整行,取 name、id 這兩個字段,存入 sort_buffer 中;
四、從索引 city 取下一個記錄的主鍵 id;
五、重複步驟 三、4 直到不知足 city='杭州’條件爲止,也就是圖中的 ID_Y;
六、對 sort_buffer 中的數據按照字段 name 進行排序;
七、遍歷排序結果,取前 1000 行,並按照 id 的值回到原表中取出 city、name 和 age 三個字段返回給客戶端。
rowid 表示位置信息,若是整張表有主鍵那麼 rowid 就是主鍵,若是沒有主鍵就會自動建立一個 6 字節的惟一標識。因此 rowid 排序就表示只加載用於排序的字段以及 rowid ,而後進行排序,而後根據排序好的 rowid 去表中回表查詢所要的結果。
缺點:會產生更屢次數的回表查詢,查詢可能會慢一些。
優勢:所需的空間更小。
例子(不考慮臨時文件):select city,name,age from t where city='杭州' order by name limit 1000 ; city 有索引
一、初始化 sort_buffer,肯定放入兩個字段,即 name 和 id;
二、從索引 city 找到第一個知足 city='杭州’條件的主鍵 id,也就是圖中的 ID_X;
三、到主鍵 id 索引取出整行,取 name、id 這兩個字段,存入 sort_buffer 中;
四、從索引 city 取下一個記錄的主鍵 id;
五、重複步驟 三、4 直到不知足 city='杭州’條件爲止,也就是圖中的 ID_Y;
六、對 sort_buffer 中的數據按照字段 name 進行排序;
七、遍歷排序結果,取前 1000 行,並按照 id 的值回到原表中取出 city、name 和 age 三個字段返回給客戶端。
select word from words order by rand() limit 3; 表數據有10000行 SQL是從10000行記錄中隨機獲取3條記錄返回。
分析: 這裏查詢的字段只有一個,因此使用全字段查詢。 加上記錄數過多,可是單條記錄的字段長度不長,因此會使用 sort_buffer + 內存臨時表。因此總結來看這條語句會使用 全字段查詢 + sort_buffer + 內存臨時表 來排序。
執行過程:
一、從緩衝池依次讀取記錄,每次讀取後都調用 rand() 函數生成一個 0-1 的數存入內存臨時表,W 是 word 值,R 是 rand() 生成的隨機數。到這掃描了 10000 行。
二、初始化 sort_buffer,從內存臨時表中將 rowid(這張表自動生成的) 以及 排序數據 R 存入 sort_buffer。到這由於要遍歷內存臨時表因此又掃描了 10000 行。
三、在 sort_buffer 中根據 R 排好序,而後選擇前三個記錄的 rowid 逐條去內存臨時表中查到 word 值返回。到這由於取了三個數據去內存臨時表去查找因此又掃描了 3 行。總共 20003 行。
經過上面的例子能夠看出當要從表中隨機獲取幾條記錄使用 rand() 函數是很是消耗資源的,同時觸發了 Using temporary(使用了臨時表) 和 Using filesort(使用了額外排序)。而且進行了 20003 行記錄的掃描,很是消耗資源。因此咱們能夠本身去計算一個隨機值,避免使用 rand() 函數。
查詢隨機的一條記錄:
一、取得整個表的行數,並記爲 C。
二、取得 Y = floor(C * rand())。floor 函數在這裏的做用,就是取整數部分。
三、再用 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優先進行排序的。能夠優化成下面三步:
一、執行 select * from t where city=「杭州」 order by name limit 100; 這個語句是不須要排序的,客戶端用一個長度爲 100 的內存數組 A 保存結果。
二、執行 select * from t where city=「蘇州」 order by name limit 100; 用相同的方法,假設結果被存進了內存數組 B。
三、如今 A 和 B 是兩個有序數組,而後你能夠用歸併排序的思想,獲得 name 最小的前 100 值,就是咱們須要的結果了。
若是將 " limit 100" 改爲 " limit 10000,100 "。能夠優化成下面三步:
一、select id,name from t where city="杭州" order by name limit 10100;
二、select id,name from t where city="蘇州" order by name limit 10100。
三、用歸併排序的方法取得按 name 順序第 10001~10100 的 name、id 的值,而後拿着這 100 個 id 到數據庫中去查出全部記錄。
優化整體上就是圍繞 「儘可能不使用額外排序,避免使用臨時表」 的原則。
一、儘可能使用索引完成排序,若是該查詢語句執行的頻率比較高,能夠爲其建立一個聯合索引。而若是使用的頻率很低,那麼就不須要去建立,由於索引的維護須要成本。
二、若是須要額外去排序,那麼能夠適當調整 sort_buffer_size(sort_buffer) 和 tmp_table_size(內存臨時表) ,使排序只在內存中執行。
三、若是 sort_buffer 空間設置足夠大,也能夠適當調整 max_length_for_sort_data 的值,使用全字段排序。
四、對於一些比較耗時的函數能夠自定義算法去實現,避免計算過程在 MySQL 中實現。