16 - MySQL "order by" 工作原理

在開發(fā)應(yīng)用的時(shí)候拘哨,一定會(huì)經(jīng)常碰到需要根據(jù)指定的字段排序來顯示結(jié)果的需求。以市民表為例信峻,假設(shè)你要查詢城市是“杭州”的所有人名字倦青,并且按照姓名排序返回前 1000 個(gè)人的姓名、年齡盹舞。

假設(shè)這個(gè)表的部分定義是這樣的:

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  ;

這個(gè)語句看上去邏輯很清晰产镐,但是你了解它的執(zhí)行流程嗎?本文踢步,我們一起來看看這個(gè)語句是怎么執(zhí)行的癣亚,以及有什么參數(shù)會(huì)影響執(zhí)行的行為。

全字段排序

  • 為避免全表掃描获印,我們需要在 city 字段加上索引述雾。在 city 字段上創(chuàng)建索引之后,我們用 explain 命令來看看這個(gè)語句的執(zhí)行情況。
explain結(jié)果圖
  • Extra 這個(gè)字段中的“Using filesort”表示的就是需要排序玻孟,MySQL 會(huì)給每個(gè)線程分配一塊內(nèi)存用于排序唆缴,稱為 sort_buffer。為了說明這個(gè) SQL 查詢語句的執(zhí)行過程取募,我們先來看一下 city 這個(gè)索引的示意圖琐谤。
city索引字段示意圖
  • 從圖中可以看到蚪腐,滿足 city='杭州’條件的行惰蜜,是從 ID_X 到 ID_(X+N) 的這些記錄桂肌。通常情況下袍患,這個(gè)語句執(zhí)行流程如下所示 :
    1. 初始化 sort_buffer沈善,確定放入 name殖卑、city杠人、age 這三個(gè)字段填帽;
    2. 從索引 city 找到第一個(gè)滿足 city='杭州’條件的主鍵 id砰粹,也就是圖中的 ID_X唧躲;
      到主鍵 id 索引取出整行,取 name碱璃、city弄痹、age 三個(gè)字段的值,存入 sort_buffer 中嵌器;
    3. 從索引 city 取下一個(gè)記錄的主鍵 id肛真;
    4. 重復(fù)步驟 3、4 直到 city 的值不滿足查詢條件為止爽航,對(duì)應(yīng)的主鍵 id 也就是圖中的 ID_Y蚓让;
    5. 對(duì) sort_buffer 中的數(shù)據(jù)按照字段 name 做快速排序;
    6. 按照排序結(jié)果取前 1000 行返回給客戶端讥珍。
  • 我們暫且把這個(gè)排序過程历极,稱為全字段排序,執(zhí)行流程的示意圖如下所示:
全字段排序
  • 圖中“按 name 排序”這個(gè)動(dòng)作衷佃,可能在內(nèi)存中完成趟卸,也可能需要使用外部排序,這取決于排序所需的內(nèi)存和參數(shù) sort_buffer_size氏义。
  • sort_buffer_size衰腌,就是 MySQL 為排序開辟的內(nèi)存(sort_buffer)的大小。如果要排序的數(shù)據(jù)量小于 sort_buffer_size觅赊,排序就在內(nèi)存中完成。但如果排序數(shù)據(jù)量太大琼稻,內(nèi)存放不下吮螺,則不得不利用磁盤臨時(shí)文件輔助排序。你可以用下面介紹的方法,來確定一個(gè)排序語句是否使用了臨時(shí)文件鸠补。
/* 打開optimizer_trace萝风,只對(duì)本線程有效 */
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';
/* 執(zhí)行語句 */
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的當(dāng)前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 計(jì)算Innodb_rows_read差值 */
select @b-@a;
  • 這個(gè)方法是通過查看 OPTIMIZER_TRACE 的結(jié)果來確認(rèn)的,你可以從 number_of_tmp_files 中看到是否使用了臨時(shí)文件紫岩。
OPTIMIZER_TRACE部分結(jié)果圖
  • number_of_tmp_files 表示的是规惰,排序過程中使用的臨時(shí)文件數(shù)。你一定奇怪泉蝌,為什么需要 12 個(gè)文件歇万??jī)?nèi)存放不下時(shí),就需要使用外部排序勋陪,外部排序一般使用歸并排序算法贪磺。可以這么簡(jiǎn)單理解诅愚,MySQL 將需要排序的數(shù)據(jù)分成 12 份寒锚,每一份單獨(dú)排序后存在這些臨時(shí)文件中。然后把這 12 個(gè)有序文件再合并成一個(gè)有序的大文件违孝。
  • 如果 sort_buffer_size 超過了需要排序的數(shù)據(jù)量的大小刹前,number_of_tmp_files 就是 0,表示排序可以直接在內(nèi)存中完成雌桑。
  • 否則就需要放在臨時(shí)文件中排序喇喉。sort_buffer_size 越小筹燕,需要分成的份數(shù)越多,number_of_tmp_files 的值就越大撒踪。
  • 我們的示例表中有 4000 條滿足 city='杭州’的記錄,所以你可以看到 examined_rows=4000制妄,表示參與排序的行數(shù)是 4000 行。
  • sort_mode 里面的 packed_additional_fields 的意思是耕捞,排序過程對(duì)字符串做了“緊湊”處理。即使 name 字段的定義是 varchar(16)俺抽,在排序過程中還是要按照實(shí)際長(zhǎng)度來分配空間的敞映。
  • 同時(shí)振愿,最后一個(gè)查詢語句 select @b-@a 的返回結(jié)果是 4000捷犹,表示整個(gè)執(zhí)行過程只掃描了 4000 行。
  • 這里需要注意的是冕末,為了避免對(duì)結(jié)論造成干擾萍歉,我把 internal_tmp_disk_storage_engine 設(shè)置成 MyISAM。否則档桃,select @b-@a 的結(jié)果會(huì)顯示為 4001枪孩。
  • 這是因?yàn)椴樵?OPTIMIZER_TRACE 這個(gè)表時(shí),需要用到臨時(shí)表藻肄,而 internal_tmp_disk_storage_engine 的默認(rèn)值是 InnoDB蔑舞。如果使用的是 InnoDB 引擎的話,把數(shù)據(jù)從臨時(shí)表取出來的時(shí)候仅炊,會(huì)讓 Innodb_rows_read 的值加 1斗幼。

rowid 排序

  • 在上面這個(gè)算法過程里面,只對(duì)原表的數(shù)據(jù)讀了一遍抚垄,剩下的操作都是在 sort_buffer 和臨時(shí)文件中執(zhí)行的蜕窿。但這個(gè)算法有一個(gè)問題,就是如果查詢要返回的字段很多的話呆馁,那么 sort_buffer 里面要放的字段數(shù)太多桐经,這樣內(nèi)存里能夠同時(shí)放下的行數(shù)很少,要分成很多個(gè)臨時(shí)文件浙滤,排序的性能會(huì)很差阴挣。
  • 所以如果單行很大,這個(gè)方法效率不夠好纺腊。那么畔咧,如果 MySQL 認(rèn)為排序的單行長(zhǎng)度太大會(huì)怎么做呢?接下來揖膜,我來修改一個(gè)參數(shù)誓沸,讓 MySQL 采用另外一種算法。
SET max_length_for_sort_data = 16;
  • max_length_for_sort_data壹粟,是 MySQL 中專門控制用于排序的行數(shù)據(jù)的長(zhǎng)度的一個(gè)參數(shù)拜隧。它的意思是,如果單行的長(zhǎng)度超過這個(gè)值趁仙,MySQL 就認(rèn)為單行太大洪添,要換一個(gè)算法。
  • city雀费、name、age 這三個(gè)字段的定義總長(zhǎng)度是 36盏袄,我把 max_length_for_sort_data 設(shè)置為 16,我們?cè)賮砜纯从?jì)算過程有什么改變炭菌。
    新的算法放入 sort_buffer 的字段黑低,只有要排序的列(即 name 字段)和主鍵 id酌毡。
    但這時(shí),排序的結(jié)果就因?yàn)樯倭?city 和 age 字段的值菩暗,不能直接返回了停团,整個(gè)執(zhí)行流程就變成如下所示的樣子:
    1. 初始化 sort_buffer掏熬,確定放入兩個(gè)字段,即 name 和 id舌胶;
    2. 從索引 city 找到第一個(gè)滿足 city='杭州’條件的主鍵 id疮丛,也就是圖中的 ID_X;
    3. 到主鍵 id 索引取出整行履恩,取 name似袁、id 這兩個(gè)字段咐刨,存入 sort_buffer 中;
      從索引 city 取下一個(gè)記錄的主鍵 id而涉;
    4. 重復(fù)步驟 3联予、4 直到不滿足 city='杭州’條件為止,也就是圖中的 ID_Y季眷;
      對(duì) sort_buffer 中的數(shù)據(jù)按照字段 name 進(jìn)行排序;
    5. 遍歷排序結(jié)果威酒,取前 1000 行挺峡,并按照 id 的值回到原表中取出 city、name 和 age 三個(gè)字段返回給客戶端尤仍。
rowid排序示意圖
  • 對(duì)比前面圖的全字段排序流程圖你會(huì)發(fā)現(xiàn)宰啦,rowid 排序多訪問了一次表 t 的主鍵索引绑莺,就是步驟 7惕耕。
  • 需要說明的是,最后的“結(jié)果集”是一個(gè)邏輯概念欺缘,實(shí)際上 MySQL 服務(wù)端從排序后的 sort_buffer 中依次取出 id挤安,然后到原表查到 city、name 和 age 這三個(gè)字段的結(jié)果嫩絮,不需要在服務(wù)端再耗費(fèi)內(nèi)存存儲(chǔ)結(jié)果剿干,是直接返回給客戶端的穆刻。
    根據(jù)這個(gè)說明過程和圖示氢伟,你可以想一下幽歼,這個(gè)時(shí)候執(zhí)行 select @b-@a甸私,結(jié)果會(huì)是多少呢设褐?
  • 現(xiàn)在,我們就來看看結(jié)果有什么不同。
    • 首先外冀,圖中的 examined_rows 的值還是 4000掀泳,表示用于排序的數(shù)據(jù)是 4000 行。但是 select @b-@a 這個(gè)語句的值變成 5000 了脑沿。
    • 因?yàn)檫@時(shí)候除了排序過程外马僻,在排序完成后韭邓,還要根據(jù) id 去原表取值。由于語句是 limit 1000瞭郑,因此會(huì)多讀 1000 行鸭你。
OPTIMIZER_TRACE結(jié)果輸出圖
  • 從 OPTIMIZER_TRACE 的結(jié)果中袱巨,你還能看到另外兩個(gè)信息也變了。
    • sort_mode 變成了 <sort_key, rowid>笛厦,表示參與排序的只有 name 和 id 這兩個(gè)字段俺夕。
    • number_of_tmp_files 變成 10 了贱鄙,是因?yàn)檫@時(shí)候參與排序的行數(shù)雖然仍然是 4000 行逗宁,但是每一行都變小了瞎颗,因此需要排序的總數(shù)據(jù)量就變小了捌议,需要的臨時(shí)文件也相應(yīng)地變少了。

全字段排序 VS rowid 排序

  • 如果 MySQL 實(shí)在是擔(dān)心排序內(nèi)存太小倦逐,會(huì)影響排序效率宫补,才會(huì)采用 rowid 排序算法,這樣排序過程中一次可以排序更多行健民,但是需要再回到原表去取數(shù)據(jù)贫贝。
  • 如果 MySQL 認(rèn)為內(nèi)存足夠大,會(huì)優(yōu)先選擇全字段排序凤优,把需要的字段都放到 sort_buffer 中蜈彼,這樣排序后就會(huì)直接從內(nèi)存里面返回查詢結(jié)果了,不用再回到原表去取數(shù)據(jù)棍辕。
  • 這也就體現(xiàn)了 MySQL 的一個(gè)設(shè)計(jì)思想:如果內(nèi)存夠楚昭,就要多利用內(nèi)存拍顷,盡量減少磁盤訪問。
  • 對(duì)于 InnoDB 表來說尿贫,rowid 排序會(huì)要求回表多造成磁盤讀,因此不會(huì)被優(yōu)先選擇匾乓。
  • 看到這里拼缝,你就了解了彰亥,MySQL 做排序是一個(gè)成本比較高的操作。那么你會(huì)問猪叙,是不是所有的 order by 都需要排序操作呢仁卷?如果不排序就能得到正確的結(jié)果锦积,那對(duì)系統(tǒng)的消耗會(huì)小很多歉嗓,語句的執(zhí)行時(shí)間也會(huì)變得更短。
  • 其實(shí)哮幢,并不是所有的 order by 語句志珍,都需要排序操作的伦糯。從上面分析的執(zhí)行過程,我們可以看到喂击,MySQL 之所以需要生成臨時(shí)表淤翔,并且在臨時(shí)表上做排序操作,其原因是原來的數(shù)據(jù)都是無序的监嗜。
  • 你可以設(shè)想下,如果能夠保證從 city 這個(gè)索引上取出來的行稚补,天然就是按照 name 遞增排序的話课幕,是不是就可以不用再排序了呢五垮?
  • 確實(shí)是這樣的。所以润绎,我們可以在這個(gè)市民表上創(chuàng)建一個(gè) city 和 name 的聯(lián)合索引诞挨,對(duì)應(yīng)的 SQL 語句是:
alter table t add index city_user(city, name);
city和name聯(lián)合索引示意圖
  • 在這個(gè)索引里面惶傻,我們依然可以用樹搜索的方式定位到第一個(gè)滿足 city='杭州’的記錄银室,并且額外確保了,接下來按順序取“下一條記錄”的遍歷過程中辜荠,只要 city 的值是杭州抓狭,name 的值就一定是有序的。
  • 這樣整個(gè)查詢過程的流程就變成了:
    1. 從索引 (city,name) 找到第一個(gè)滿足 city='杭州’條件的主鍵 id狱从;
    2. 到主鍵 id 索引取出整行叠纹,取 name、city与涡、age 三個(gè)字段的值,作為結(jié)果集的一部分直接返回氨肌;
    3. 從索引 (city,name) 取下一個(gè)記錄主鍵 id酌畜;
    4. 重復(fù)步驟 2、3恳守,直到查到第 1000 條記錄催烘,或者是不滿足 city='杭州’條件時(shí)循環(huán)結(jié)束缎罢。
引入聯(lián)合索引排序示意圖

explain結(jié)果圖
  • 從圖中可以看到策精,Extra 字段中沒有 Using filesort 了咽袜,也就是不需要排序了。而且由于 (city,name) 這個(gè)聯(lián)合索引本身有序,所以這個(gè)查詢也不用把 4000 行全都讀一遍抽莱,只要找到滿足條件的前 1000 條記錄就可以退出了。也就是說匕垫,在我們這個(gè)例子里虐呻,只需要掃描 1000 次斟叼。
  • 覆蓋索引是指,索引上的信息足夠滿足查詢請(qǐng)求忽孽,不需要再回到主鍵索引上去取數(shù)據(jù)。按照覆蓋索引的概念兄一,我們可以再優(yōu)化一下這個(gè)查詢語句的執(zhí)行流程出革。
    針對(duì)這個(gè)查詢,我們可以創(chuàng)建一個(gè) city耳璧、name 和 age 的聯(lián)合索引栖雾,對(duì)應(yīng)的 SQL 語句就是:
alter table t add index city_user_age(city, name, age);
  • 這時(shí),對(duì)于 city 字段的值相同的行來說召廷,還是按照 name 字段的值遞增排序的竞慢,此時(shí)的查詢語句也就不再需要排序了治泥。這樣整個(gè)查詢語句的執(zhí)行流程就變成了:
    1. 從索引 (city,name,age) 找到第一個(gè)滿足 city='杭州’條件的記錄,取出其中的 city败潦、name 和 age 這三個(gè)字段的值劫扒,作為結(jié)果集的一部分直接返回狸膏;
    2. 從索引 (city,name,age) 取下一個(gè)記錄,同樣取出這三個(gè)字段的值贤旷,作為結(jié)果集的一部分直接返回砾脑;
    3. 重復(fù)執(zhí)行步驟 2,直到查到第 1000 條記錄县遣,或者是不滿足 city='杭州’條件時(shí)循環(huán)結(jié)束。
聯(lián)合索引其兴,查詢語句示意圖

explain示意圖
  • 可以看到元旬,Extra 字段里面多了“Using index”守问,表示的就是使用了覆蓋索引耗帕,性能上會(huì)快很多。
  • 當(dāng)然体啰,這里并不是說要為了每個(gè)查詢能用上覆蓋索引,就要把語句中涉及的字段都建上聯(lián)合索引荒勇,畢竟索引還是有維護(hù)代價(jià)的沽翔。這是一個(gè)需要權(quán)衡的決定窿凤。

小結(jié)

  • 本文介紹了 MySQL 里面 order by 語句的幾種算法流程。
  • 在開發(fā)系統(tǒng)的時(shí)候哨颂,你總是不可避免地會(huì)使用到 order by 語句相种。你心里要清楚每個(gè)語句的排序邏輯是怎么實(shí)現(xiàn)的寝并,還要能夠分析出在最壞情況下衬潦,每個(gè)語句的執(zhí)行對(duì)系統(tǒng)資源的消耗植酥,這樣才能做到下筆如有神弦牡,不犯低級(jí)錯(cuò)誤驾锰。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末椭豫,一起剝皮案震驚了整個(gè)濱河市旨指,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裸扶,老刑警劉巖搬素,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔗蹋,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡餐塘,警方通過查閱死者的電腦和手機(jī)戒傻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蜂筹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人不翩,你說我怎么就攤上這事麻裳。” “怎么了妙蔗?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵眉反,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我梳凛,道長(zhǎng)播歼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任叭莫,我火速辦了婚禮雇初,結(jié)果婚禮上减响,老公的妹妹穿的比我還像新娘。我一直安慰自己刊橘,他們只是感情好颂鸿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布嘴纺。 她就那樣靜靜地躺著,像睡著了一般尖坤。 火紅的嫁衣襯著肌膚如雪闲擦。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天纯路,我揣著相機(jī)與錄音,去河邊找鬼装哆。 笑死定嗓,一個(gè)胖子當(dāng)著我的面吹牛宵溅,可吹牛的內(nèi)容都是我干的恃逻。 我是一名探鬼主播藕施,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼矛市!你這毒婦竟也來了浊吏?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤救氯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后墩衙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體享扔,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惧眠,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了暮顺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秀存。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖惫恼,靈堂內(nèi)的尸體忽然破棺而出澳盐,到底是詐尸還是另有隱情令宿,我是刑警寧澤粒没,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布癞松,位于F島的核電站入蛆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏厕妖。R本人自食惡果不足惜挑庶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望举畸。 院中可真熱鬧凳枝,春花似錦、人聲如沸叛买。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椒功。三九已至智什,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旱眯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國打工础爬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叫搁。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓渴逻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親惨奕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容