MySQL-排序相關原理分析
全欄位排序和rowId排序
建表語句如下:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`city` varchar(16) NOT NULL,
`name` varchar(16) NOT NULL,
`age` int(11) NOT NULL,
`addr` varchar(128) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `city` (`city`)
) ENGINE=InnoDB;
sql語句如下:
select city,name,age from t where city='杭州' order by name limit 1000 ;
相關概念定義
sort_buffer:MySQL會給每個執行緒分配一塊記憶體區域用於排序,這塊區域叫sort_buffer。如果待排序的資料足夠存放在sort_buffer中,那麼就會直接用這塊區域進行排序,演算法為快速排序;如果待排序的資料超過了sort_buffer大小,會使用磁碟臨時檔案來輔助排序,演算法為歸併排序。
全欄位排序:sort_buffer中儲存的待排序資料,包括需要返回的所有欄位,比如,上面sql語句中的city,name,age,雖然只用name來排序,但是還是冗餘存放了city和age的資料,排序完直接返回即可。
rowId排序:sort_buffer中儲存的待排序資料,只包括待排序欄位和對應行的主鍵id,比如,上面sql語句,如果使用rowId排序,那麼sort_buffer中只會儲存name和rowID欄位,等到排序完畢,需要回表查詢出來需要返回的其他欄位資料。
什麼時候選擇全欄位排序?什麼時候選擇rowID排序?
當MySQL判斷,當待處理表為InnoDB磁碟表時,會優先使用全欄位排序,目的是為了減少rowID排序最後需要再次回表查詢需要返回的欄位的操作開銷,但是全欄位排序如果需要冗餘的單行資料量太大時,就不會選擇全欄位排序,而選擇rowID排序。
- 如何判斷單行資料是否過大?MySQL中會使用max_length_for_sort_data來判斷。
為什麼單行資料量大,就需要切換演算法?
如果單行資料量太大,記憶體中能儲存下的行數就會變少,就需要使用更多的磁碟臨時檔案來儲存,排序的效能會比較差。
記憶體臨時表和磁碟臨時表
看這個業務:
有一張單詞表,我們需要隨機顯示三個單詞給使用者。
建表語句和生成資料儲存過程:
mysql> CREATE TABLE `words` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`word` varchar(64) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=0;
while i<10000 do
insert into words(word) values(concat(char(97+(i div 1000)), char(97+(i % 1000 div 100)), char(97+(i % 100 div 10)), char(97+(i % 10))));
set i=i+1;
end while;
end;;
delimiter ;
call idata();
SQL語句:
mysql> select word from words order by rand() limit 3;
注:rand() 這個函式會返回一個0~1之間的隨機小數值。
當需要使用這個隨機值來排序時,就需要使用臨時表來儲存這個隨機資料。
記憶體臨時表
執行過程如下:
- 首先生成Memory引擎的記憶體臨時表,在主鍵索引中,依次取出所有的word值,呼叫rand函式生成 一個隨機值,把word和隨機值儲存到臨時表中。
- 然後針對這個臨時表開始排序。使用sort_buffer並且使用rowId演算法。
- 這裡為什麼使用rowID演算法了呢?因為上面提到過的全欄位排序會被優先選擇,前提是待排序的表是磁碟表;現在的待排序表為Memory引擎的記憶體表,雖然使用rowID,但是最後的回表查詢都是在記憶體中完成的,開銷大大降低,MySQL當然會選擇可以一次排序更多行的rowId演算法。
磁碟臨時表
MySQL中有一個引數,tmp_table_size 這個引數限制了記憶體臨時表的大小,預設值是 16M。如果臨時表大於16M,就會使用磁碟臨時表來儲存臨時資料,預設是InnoDB引擎表。關於預設的InnoDB引擎表的排序過程,在上面的全欄位排序和rowId排序中已經介紹過了。
新的排序演算法
上面介紹過,InnoDB磁碟表,要麼使用基於全記憶體的快排,要麼基於輔助的磁碟臨時檔案的歸併排序,其實在MySQL5.6之後,還引入了一種新的排序演算法,優先佇列排序演算法。
為什麼需要這種演算法?
考慮剛才的SQL語句
mysql> select word from words order by rand() limit 3;
無論是使用快排還是歸併排序,他們都是基於所有的資料進行排序。
但分析上面的sql語句,其實我們只需要排序後的前面三條資料,並且後面的排序資料在計算上來說是浪費資源的。有沒有一種演算法,可以通過排序,只得到我們需要的最小的三條或者最大的三條資料,並且儘量不使用磁碟臨時檔案呢?
優先佇列排序演算法
優先佇列排序演算法,如果執行上面的sql語句,會先從表中順序取最開始的3條資料,儲存到一個最大堆中(最大堆:堆頭永遠是容器內資料的最大值)。然後遍歷後面的所有資料,判斷當前取值和堆最大值比較,如果比堆最大值小,就把新資料入堆,並且重新排序堆中的順序,保持堆頭為最大值。
經過這種排序以後,堆中就是我們需要的前三個最小值了。
示意圖如下:
什麼時候會選擇這種演算法?
當存在limit字句時,並且limit需要的維護的最大堆的大小小於 sort_buffer,就會使用這個演算法。