1. 程式人生 > 實用技巧 >MySQL-排序相關原理分析

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之間的隨機小數值。

當需要使用這個隨機值來排序時,就需要使用臨時表來儲存這個隨機資料。

記憶體臨時表

執行過程如下:

  1. 首先生成Memory引擎的記憶體臨時表,在主鍵索引中,依次取出所有的word值,呼叫rand函式生成 一個隨機值,把word和隨機值儲存到臨時表中。
  2. 然後針對這個臨時表開始排序。使用sort_buffer並且使用rowId演算法。
    1. 這裡為什麼使用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,就會使用這個演算法。