全字段排序和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,就會使用這個算法。