在日常開發工作中,你一定會經常遇到要根據指定字段進行排序的需求。
這時,你的SQL語句類似這樣。
select id,phone,code from evt_sms where phone like '13020%' order by id desc limit 10
這個SQL的邏輯是十分清晰明了,但其內部的執行原理你知多少。
接下來,本期文章將帶你打開order by的大門一探究竟。
本期所有結論都基於MySQL8.0.26版本
最新文章
MySQL統計總數就用count(*),別花里胡哨的《死磕MySQL系列 十》
一、常見的Extra幾個信息
在MySQL中想看一條SQL的性能不僅僅看是否用上了索引,還要看Extra中的內容,以下內容來自官方文檔,給你最准確的學習資料。
using index
根據索引樹可直接檢索列信息,無需額外的操作來讀取實際的行。
索引列即為查詢列,也為條件列。
using index condition
下面這條語句name為普通索引,age無索引。
select * from table where name = ? and age = ?
索引下推是在MySQL5.6及以后的版本出現的。
之前的查詢過程是,先根據name在存儲引擎中獲取數據,然后在根據age在server層進行過濾。
在有了索引下推之后,查詢過程是根據name、age在存儲引擎獲取數據,返回對應的數據,不再到server層進行過濾。
當你使用Explain分析SQL語句時,如果出現了using index condition那就是使用了索引下推,索引下推是在組合索引的情況出現幾率最大的。
using index for group_by
只查索引列,對索引列使用了group by
explain select phone from evt_sms where phone = "13054125874" group by phone;
using where
查詢的列被索引覆蓋,並且where篩選條件是索引列之一,但不是索引的前導列,Extra中為Using where; Using index, 意味着無法直接通過索引查找來查詢到符合條件的數據
查詢的列被索引覆蓋,並且where篩選條件是索引列前導列的一個范圍,同樣意味着無法直接通過索引查找查詢到符合條件的數據
zero limit
這個估計很少有小伙伴知道,就是你的SQL語句查詢數量為limit 0
using temporary
使用了臨時表,一般在使用group by、order by時會遇到。
這個也是本文即將要聊的話題。
using filesort
一般在使用group by、order by時會遇到,排序過程在內存中完成
Backward index scan
對索引列使用了降序操作
這里只列舉了最常見的幾個信息,MySQL官方文檔中對Extra的解析大概有37個,感興趣的可以去看看,后期咔咔也會逐步完善這塊內容。
二、文件排序
由於是在一些統計、排序的業務中會經常見到Extra中出現using filesort的信息。
在MySQL8.0.26版本中對一個沒有索引的列進行排序在Extra中顯示using filesort。在低版本中需要你進行試驗在什么情況下會出現。
在Extra中顯示的using filesort表示的就是排序,MySQL會給每個線程分配一塊內存用於排序,也被稱之為sort_buffer
。這期文章和下期文章會牽扯到很多名詞,記得自己整理一下哈!
再看這條語句
那么這條SQL執行的具體流程是什么呢?
1、初始化sort_buffer,放入字段phone、code字段
2、在phone的索引樹找到主鍵值
3、根據主鍵值到主鍵索引樹中檢索處phone、code對應字段的值,再存儲sort_buffer中
4、繼續從phone取下一個主鍵值
5、重復第三、第四,直到不滿足phone = 條件為止
6、在sort_buffer中的數據按照字段phone做快排
7、按照快排的結果取出前10行返回改客戶端即可
問題:所有的排序都是在內存中進行的?
當然不是,任何內存都不是無限制的,是否在內存中排序取決於MySQL參數sort_buffer_sort。
在MySQL8.0.26版本中這個值大小默認為256kb。
當需要排序的數據量大於256kb的閥值時,則會利用臨時文件進行輔助排序,也就是常說的歸並排序算法實現。
sort_buffer_size跟需要臨時文件的個數成正比,如果sort_buffer_size越小則臨時文件的數量就越多。
如何查看一個排序是否使用了臨時文件,這個答案就交給大家來實現,版本不一致會導致很多結果都不同。
問題:你知道歸並排序是如何實現的嗎?
現在你知道了如果排序的數據大於sort_buffer_size會使用臨時文件排序,這種排序使用的就是歸並排序的思想,接下來讓我們看看具體的流程是怎么樣的。
1、把需要排序的數據分割,分割成每塊數據都可以存放到sort_buufer中
2、對每塊數據在sort_buufer中進行排序,排序好后,寫入某個臨時文件
3、當所有的數據都寫入臨時文件后,這時對於每個臨時文件內部來說是有序的,但對於所有臨時文件是無序的,所以還需要合並數據
4、假設現在存在tmp1和tmp2兩個臨時文件,這時分別從tmp1、tmp2讀入部分數據到內存
5、假設從tmp1和tmp2中分別讀入[0-5]的數據,然后分別使用tmp1[0]、tmp2[0] 進行對比,一直到tmp1[5]、tmp2[5],這樣兩兩比較就可以把tmp1、tmp2合並為一個文件。經過幾輪下來所有分割的數據都會合並為一個有序的大文件
三、文件排序很慢,還有其它辦法嗎
通過上面的案例,如果排序的數據量非常大則會超過sort_buffer_size的最大值,就只能使用文件排序,文件排序涉及了多次的文件合並是非常消耗性能的。
在上文你有沒有發現一個細節,SQL中只需要排序code字段,但把phone字段也加到了sort_buufer中了。
這樣單行的數據大小無形中就增大了,這樣內存中能夠存放的行數就減少了,需要分割成多個臨時文件,排序性能會很差,那么有沒有其它方案可以解決這種問題呢?
答案是肯定有的,就是接下來要聊的rowid排序。
先看一個參數max_length_for_sort_data
默認max_length_for_sort_data的大小為4096字節,假設現在要排序的數據非常多,我們可以修改這個參數讓其使用rowid的算法。
MySQL中專門控制用戶排序的行數據長度的參數,如果單行的數據長度超過了這個值,則MySQL會自動更換為rowid算法。
rowid排序的思想就是把不需要的數據不放到sort_buufer中,讓sort_buffer中只存放需要排序的字段。
問題:如果你是設計者,你會存放那些字段
假設現在存放只需要排序的字段,排序很快完成了,拿到排序后的數據結果你應該怎么辦呢?你已經無從下手了。
因此,你可以把主鍵ID的值也存放到sort_buufer中,當排序完成后通過ID回表即可得到排序后的數據。
執行流程
試想一下,這個執行流程其實跟文件排序的流程大差不差。
只是存放到sort_buufer中的字段變為需要排序的字段加上主鍵字段。
接着在sort_buufer中按照排序字段進行排序
最后再遍歷排序結果,取需要的行數,並使用id進行回表一次,查出你需要的列即可。
注意點
這不是說使用了rowid的排序算法后就不使用臨時文件排序了,不是這樣的。
使用rowid只是存放到sort_buffer中的數據多個,若需要排序的數據很多還是需要使用臨時文件的。
四、優化文件排序
如果MySQL發現sort_buufer內存太小,會影響排序效率,才會采用rowid排序算法,使用rowid算法的好處就是sort_buffer中可以一次排序更多的行,缺點就是需要回表。
在MySQL中如果內存夠用,就多利用內存,盡量減少磁盤訪問。所有rowid的算法不會被優先選擇,因為回表會造成過的磁盤讀。
不是所有的order by語句,都需要排序操作的,上面分析的兩種排序算法的由來都是因為原來的數據都是無序的。
問題:什么是有序的?
看過了索引那一期文章后,你現在應該知道以下兩點。
索引本身具有順序性,在進行范圍查詢時,獲取的數據已經排好了序,從而避免服務器再次排序和建立臨時表的問題。
索引的底層實現本身具有順序性,通過磁盤預讀使得在磁盤上對數據的訪問大致呈順序的尋址,也就是將隨機的I/O變為順序I/O。
問題:如何防止進行排序
現在你應該知道答案了,就是給需要排序的列創建聯合索引。
現在給phone、code建立一個聯合索引,對應的SQL語句如下
alter table evt_sms add index idx_phone_code (phone,code);
那么執行同樣的語句就不會使用排序操作了,接下來看一下執行流程
執行流程
1、從索引(phone,code)找到滿足phone='123456'的記錄,取出phone、code的值,作為結果集的一部分直接返回
3、從索引(phone、code)取下一個記錄,同樣取出phone、code的值,作為結果集的一部分直接返回
4、重復步驟2直到查出1000行數據,或者不滿足查詢條件為止
五、總結
order by沒有用到索引時,執行計划中會出現using filesort
using filesort根據參數sort_buffer_size的值來決定使用需要使用臨時文件
max_length_for_sort_data參數決定是否使用rowid算法,若放入sort_buffer的每行數據大於設置的值就會使用rowid算法
現在你應該知道了rowid排序只是把需要排序的字段和主鍵ID放入sort_buffer中,而文件排序則是把查詢的所有字段全部放入sort_buffer中。
還有rowid會多造成一次回表操作,這個你也要知道。
最后提到了優化order by語句,這里提到了建立覆蓋索引,利用索引的有序性直接返回結果不用進行排序。
這里並不是提倡大家在實際生產環境中盲目建立,而是根據具體業務情況,如果數據非常的小在內存排序是非常快的。並且覆蓋索引會占用更多的存儲空間和維護開銷。
“堅持學習、堅持寫作、堅持分享是咔咔從業以來所秉持的信念。願文章在偌大的互聯網上能給你帶來一點幫助,我是咔咔,下期見。
”