MySQL的orderby是如何工做的

 

一、           概念

在你開發應用的時候,必定會常常碰到須要根據指定的字段排序來顯示結果的需求。仍是以咱們前面舉例用過的市民表爲例,假設你要查詢城市是「杭州」的全部人名字,而且按照姓名排序返回前1000我的的姓名、年齡。算法

 

假設這個表的部分定義是這樣的:session

 

CREATE TABLE `t` (性能

  `id` int(11) NOT NULL,優化

  `city` varchar(16) NOT NULL,線程

  `name` varchar(16) NOT NULL,設計

  `age` int(11) NOT NULL,3d

  `addr` varchar(128) DEFAULT NULL,orm

  PRIMARY KEY (`id`),blog

  KEY `city` (`city`)排序

) ENGINE=InnoDB;

這時,你的SQL語句能夠這麼寫:

 

select city,name,age from t where city='杭州' order by name limit 1000  ;

這個語句看上去邏輯很清晰,可是你瞭解它的執行流程嗎?今天,我就和你聊聊這個語句是怎麼執行的,以及有什麼參數會影響執行的行爲。

 

二、           全字段排序

爲避免全表掃描,咱們須要在city字段加上索引。

 

在city字段上建立索引以後,咱們用explain命令來看看這個語句的執行狀況。

Extra這個字段中的「Using filesort」表示的就是須要排序,MySQL會給每一個線程分配一塊內存用於排序,稱爲sort_buffer

 

爲了說明這個SQL查詢語句的執行過程,咱們先來看一下city這個索引的示意圖。

 

從圖中能夠看到,知足city='杭州’條件的行,是從ID_X到ID_(X+N)的這些記錄。

 

一般狀況下,這個語句執行流程以下所示 :

 

初始化sort_buffer,肯定放入name、city、age這三個字段;

 

從索引city找到第一個知足city='杭州’條件的主鍵id,也就是圖中的ID_X;

 

到主鍵id索引取出整行,取name、city、age三個字段的值,存入sort_buffer中;

 

從索引city取下一個記錄的主鍵id;

 

重複步驟三、4直到city的值不知足查詢條件爲止,對應的主鍵id也就是圖中的ID_Y;

 

對sort_buffer中的數據按照字段name作快速排序;

 

按照排序結果取前1000行返回給客戶端。

 

咱們暫且把這個排序過程,稱爲全字段排序,執行流程的示意圖以下所示,下一篇文章中咱們還會用到這個排序。

 

全字段排序

圖中「按name排序」這個動做,可能在內存中完成,也可能須要使用外部排序,這取決於排序所需的內存和參數sort_buffer_size。

 

sort_buffer_size,就是MySQL爲排序開闢的內存(sort_buffer)的大小。若是要排序的數據量小於sort_buffer_size,排序就在內存中完成。但若是排序數據量太大,內存放不下,則不得不利用磁盤臨時文件輔助排序。

 

能夠用下面介紹的方法,來肯定一個排序語句是否使用了臨時文件。

 

/* 打開optimizer_trace,只對本線程有效 */

SET optimizer_trace='enabled=on';

 

/* @a保存Innodb_rows_read的初始值 */

select VARIABLE_VALUE into @a from  performance_schema.session_status where variable_name = 'Innodb_rows_read';

 

/* 執行語句 */

select city, name,age from t where city='杭州' order by name limit 1000;

 

/* 查看 OPTIMIZER_TRACE 輸出 */

SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G

 

/* @b保存Innodb_rows_read的當前值 */

select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';

 

/* 計算Innodb_rows_read差值 */

select @b-@a;

 

這個方法是經過查看 OPTIMIZER_TRACE 的結果來確認的,能夠從 number_of_tmp_files中看到是否使用了臨時文件。

 

 

number_of_tmp_files表示的是,排序過程當中使用的臨時文件數。你必定奇怪,爲何須要12個文件?內存放不下時,就須要使用外部排序,外部排序通常使用歸併排序算法。能夠這麼簡單理解,MySQL將須要排序的數據分紅12份,每一份單獨排序後存在這些臨時文件中。而後把這12個有序文件再合併成一個有序的大文件。

 

圖中幾個值的的意思:

咱們的示例表中有4000條知足city='杭州’的記錄,因此你能夠看到 examined_rows=4000,表示參與排序的行數是4000行。

 

sort_mode 裏面的packed_additional_fields的意思是,排序過程對字符串作了「緊湊」處理。即便name字段的定義是varchar(16)在排序過程當中仍是要按照實際長度來分配空間的。

 

同時,最後一個查詢語句select @b-@a 的返回結果是4000,表示整個執行過程只掃描了4000行。

 

這裏須要注意的是,爲了不對結論形成干擾,我把internal_tmp_disk_storage_engine設置成MyISAM。不然,select @b-@a的結果會顯示爲4001。

 

這是由於查詢OPTIMIZER_TRACE這個表時,須要用到臨時表,而internal_tmp_disk_storage_engine的默認值是InnoDB。若是使用的是InnoDB引擎的話,把數據從臨時表取出來的時候,會讓Innodb_rows_read的值加1。

 

一、           rowid排序

在上面這個算法過程裏面,只對原表的數據讀了一遍,剩下的操做都是在sort_buffer和臨時文件中執行的。但這個算法有一個問題,就是若是查詢要返回的字段不少的話,那麼sort_buffer裏面要放的字段數太多,這樣內存裏可以同時放下的行數不多,要分紅不少個臨時文件,排序的性能會不好。

 

因此若是單行很大,這個方法效率不夠好。

 

若是MySQL認爲排序的單行長度太大會怎樣處理呢?

 

SET max_length_for_sort_data = 16;

修改這個參數,讓MySQL採用另一種算法。

 

max_length_for_sort_data,是MySQL中專門控制用於排序的行數據的長度的一個參數。它的意思是,若是單行的長度超過這個值,MySQL就認爲單行太大,要換一個算法。

 

city、name、age 這三個字段的定義總長度是36,我把max_length_for_sort_data設置爲16,咱們再來看看計算過程有什麼改變。

新的算法放入sort_buffer的字段,只有要排序的列(即name字段)和主鍵id。

 

但這時,排序的結果就由於少了city和age字段的值,不能直接返回了,整個執行流程就變成以下所示的樣子:

 

初始化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排序。

對比圖3的全字段排序流程圖你會發現,rowid排序多訪問了一次表t的主鍵索引,就是步驟7。

 

須要說明的是,最後的「結果集」是一個邏輯概念,實際上MySQL服務端從排序後的sort_buffer中依次取出id,而後到原表查到city、name和age這三個字段的結果,不須要在服務端再耗費內存存儲結果,是直接返回給客戶端的。

 

根據這個說明過程和圖示,你能夠想一下,這個時候執行select @b-@a,結果會是多少呢?

 

如今,咱們就來看看結果有什麼不一樣。

 

首先,圖中的examined_rows的值仍是4000,表示用於排序的數據是4000行。可是select @b-@a這個語句的值變成5000了。

 

由於這時候除了排序過程外,在排序完成後,還要根據id去原表取值。因爲語句是limit 1000,所以會多讀1000行。

 

 

從OPTIMIZER_TRACE的結果中,你還能看到另外兩個信息也變了。

 

sort_mode變成了<sort_key, rowid>,表示參與排序的只有name和id這兩個字段。

number_of_tmp_files變成10了,是由於這時候參與排序的行數雖然仍然是4000行,可是每一行都變小了,所以須要排序的總數據量就變小了,須要的臨時文件也相應地變少了。

 

四、全字段排序和rowid排序對比

若是MySQL實在是擔憂排序內存過小,會影響排序效率,纔會採用rowid排序算法,這樣排序過程當中一次能夠排序更多行,可是須要再回到原表去取數據。

 

若是MySQL認爲內存足夠大,會優先選擇全字段排序,把須要的字段都放到sort_buffer中,這樣排序後就會直接從內存裏面返回查詢結果了,不用再回到原表去取數據。

 

MySQL設計思想:若是內存夠,就要多利用內存,儘可能減小磁盤訪問。

 

對於InnoDB表來講,rowid排序會要求回表多形成磁盤讀,所以不會被優先選擇。

 

看到這裏, MySQL作排序是一個成本比較高的操做。那麼你會問,是否是全部的order by都須要排序操做呢?若是不排序就能獲得正確的結果,那對系統的消耗會小不少,語句的執行時間也會變得更短。

 

其實,並非全部的order by語句,都須要排序操做的。從上面分析的執行過程,咱們能夠看到,MySQL之因此須要生成臨時表,而且在臨時表上作排序操做,其緣由是原來的數據都是無序的。

 

若是可以保證從city這個索引上取出來的行,自然就是按照name遞增排序的話,是否是就能夠不用再排序了呢?

 

確實是這樣的。

 

因此,咱們能夠在這個市民表上建立一個city和name的聯合索引,對應的SQL語句是:

 

alter table t add index city_user(city, name);

做爲與city索引的對比,咱們來看看這個索引的示意圖。

 

在這個索引裏面,咱們依然能夠用樹搜索的方式定位到第一個知足city='杭州’的記錄,而且額外確保了,接下來按順序取「下一條記錄」的遍歷過程當中,只要city的值是杭州,name的值就必定是有序的。

 

這樣整個查詢過程的流程就變成了:

 

從索引(city,name)找到第一個知足city='杭州’條件的主鍵id;

 

到主鍵id索引取出整行,取name、city、age三個字段的值,做爲結果集的一部分直接返回;

 

從索引(city,name)取下一個記錄主鍵id;

 

重複步驟二、3,直到查到第1000條記錄,或者是不知足city='杭州’條件時循環結束。

能夠看到,這個查詢過程不須要臨時表,也不須要排序。接下來,咱們用explain的結果來印證一下。

 

從圖中能夠看到,Extra字段中沒有Using filesort,也就是不須要排序了。並且因爲(city,name)這個聯合索引自己有序,因此這個查詢也不用把4000行全都讀一遍,只要找到知足條件的前1000條記錄就能夠退出了。也就是說,在咱們這個例子裏,只須要掃描1000次。

 

覆蓋索引是指,索引上的信息足夠知足查詢請求,不須要再回到主鍵索引上去取數據。

 

按照覆蓋索引的概念,咱們能夠再優化一下這個查詢語句的執行流程。

 

針對這個查詢,咱們能夠建立一個city、name和age的聯合索引,對應的SQL語句就是:

 

alter table t add index city_user_age(city, name, age);

這時,對於city字段的值相同的行來講,仍是按照name字段的值遞增排序的,此時的查詢語句也就再也不須要排序了。這樣整個查詢語句的執行流程就變成了:

 

從索引(city,name,age)找到第一個知足city='杭州’條件的記錄,取出其中的city、name和age這三個字段的值,做爲結果集的一部分直接返回;

 

從索引(city,name,age)取下一個記錄,一樣取出這三個字段的值,做爲結果集的一部分直接返回;

 

重複執行步驟2,直到查到第1000條記錄,或者是不知足city='杭州’條件時循環結束。

 

能夠看到,Extra字段裏面多了「Using index」,表示的就是使用了覆蓋索引,性能上會快不少。

固然,這裏並非說要爲了每一個查詢能用上覆蓋索引,就要把語句中涉及的字段都建上聯合索引,畢竟索引仍是有維護代價的。這是一個須要權衡的決定。

 

五、小結

在開發系統的時候,你老是不可避免地會使用到order by語句。你內心要清楚每一個語句的排序邏輯是怎麼實現的,還要可以分析出在最壞狀況下,每一個語句的執行對系統資源的消耗,這樣才能作到下筆若有神,不犯低級錯誤。

相關文章
相關標籤/搜索