Mysql學習之order by的工作原理


在你開發應用的時候,一定會經常碰到需要根據指定的字段排序來顯示結果的需求。假設你要查詢城市是“杭州”的所有人名字,並且按照姓名排序返回前 1000 個人的姓名、年齡。

查詢語句為:

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

全字段排序

為避免全表掃描,我們需要在 city 字段加上索引。

通常情況下,這個語句執行流程如下所示 :

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

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

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

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

  5. 重復步驟 3、4 直到 city 的值不滿足查詢條件為止,對應的主鍵 id 也就是圖中的 ID_Y;

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

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

我們暫且把這個排序過程,稱為全字段排序,執行流程的示意圖如下所示

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

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

這個算法有一個問題,就是如果查詢要返回的字段很多的話,那么 sort_buffer 里面要放的字段數太多,這樣內存里能夠同時放下的行數很少,要分成很多個臨時文件,排序的性能會很差。

rowid 排序

如果 MySQL 認為排序的單行長度太大會怎么做呢?

接下來,我來修改一個參數,讓 MySQL 采用另外一種算法。

SET max_length_for_sort_data = 16;

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

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

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

  1. 初始化 sort_buffer,確定放入兩個字段,即 name 和 id;

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

  3. 到主鍵 id 索引取出整行,取 name、id 這兩個字段,存入 sort_buffer 中;

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

  5. 重復步驟 3、4 直到不滿足 city='杭州’條件為止,也就是圖中的 ID_Y;

  6. 對 sort_buffer 中的數據按照字段 name 進行排序;

  7. 遍歷排序結果,取前 1000 行,並按照 id 的值回到原表中取出 city、name 和 age 三個字段返回給客戶端。

這個執行流程的示意圖如下:

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

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

全字段排序 VS rowid 排序

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

對於 InnoDB 表來說,rowid 排序會要求回表多造成磁盤讀,因此不會被優先選擇。

 聯合索引

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

所以,我們可以在這個市民表上創建一個 city 和 name 的聯合索引

在這個索引里面,我們依然可以用樹搜索的方式定位到第一個滿足 city='杭州’的記錄,並且額外確保了,接下來按順序取“下一條記錄”的遍歷過程中,只要 city 的值是杭州,name 的值就一定是有序的。

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

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

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

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

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

 

可以看到,這個查詢過程不需要臨時表,也不需要排序。接下來,我們用 explain 的結果來印證一下。

從圖中可以看到,Extra 字段中沒有 Using filesort 了,也就是不需要排序了。而且由於 (city,name) 這個聯合索引本身有序,所以這個查詢也不用把 4000 行全都讀一遍,只要找到滿足條件的前 1000 條記錄就可以退出了。也就是說,在我們這個例子里,只需要掃描 1000 次。

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

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

針對這個查詢,我們可以創建一個 city、name 和 age 的聯合索引

這樣整個查詢語句的執行流程就變成了:

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

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

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

 

當然,這里並不是說要為了每個查詢能用上覆蓋索引,就要把語句中涉及的字段都建上聯合索引,畢竟索引還是有維護代價的。這是一個需要權衡的決定。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM