innodb 排序原理?十分鐘讓你秒懂

小小的排序,到底隱藏着什麼奧祕,讓我們通過生活中的例子去快速瞭解吧!

一、排序怎麼用

  我們都知道,排序就是使用 order by 這個關鍵字 ,可以是升序(asc),也可以是降序 (desc),在我們日常的使用中經常會有各種排序的需求,例如隔壁老王想給他班裏的學生按成績降序排下序,來獲得班級的排名, 就是 order by grade desc ,使用是十分簡單的,但是,有沒有發覺有時候加上了排序會讓你的SQL突然執行的特慢,體驗感從開飛機變成了開摩托。此時,你該思考,這該死的 order by 到底做了什麼?

二、排序的原理

   我們先拋開 MySQL 不講,先來聊聊一個場景,假設有一個班的學生,你需要按照數學考試的成績給他們進行排序,最後讓他們拿着成績單並且揹着他們的書包按照成績從高到低進行排隊,你會怎麼做呢?
  首先我們假設成績單上都有學生的姓名並且學生都知道他們的書包是哪一個。而且你只有幾個小辦公室,可能不能同時把學生放到一個房間裏,但是走廊是可以用的。

那麼首先你要先考慮,辦公室夠不夠所有存下所有揹着書包並且拿着試卷的學生呢?

場景1: 假設是夠的,那麼你可能會直接讓學生背好書包拿着試卷,然後你就直接按照成績來讓學生站好隊,不管你用的是快速排序,還是插入排序,還是希爾排序,我們暫時不關心。就當做你知道怎麼排序就好了。

這種情況看似可取啊,因爲空間夠大啊,我一次性讓他們都帶好裝備直接排不是挺好嗎?還不麻煩

場景2: 辦公室空間不夠大啊,可能你就只有一張桌子,那咋辦啊??? 能不能不管學生和書包,直接先把試卷排好序呢?哦呦,可以啊,試卷排好序後,然後到走廊叫名字,讓對應的學生帶好書包過來排隊不就好了。喔,好聰明啊。

場景3: 哎呀我的辦公室又小,但是我又不想用先排序試卷的方式咋辦,對哦,我不是還有幾個小辦公室可以用嗎?那就先排前十名學生,就放到一個辦公室裏,然後再拍十一名到二十名放到第二個辦公室裏,最後按順序一個辦公室一個辦公室把學生叫出來走廊,拼接起來不也行。

好,我們現在分析了上面三種場景。可以知道,空間是很關鍵的因素啊,房子夠大,你在房子裏打高爾夫都行,我窮我能咋辦。

三、MySQL的排序機制

接下來我們進入正題 ,MySQL的排序機制是什麼呢?

現在我們把上面的場景實例化一下,假設成績單就是 主鍵id,分數、學生和書包分別是另外三個字段, MySQL主要有以下兩種類型的排序方式。

1.全字段排序

全字段排序,顧明思議,就是我們排序的時候已經有所有需要返回的字段了,可以直接排序後就返回,不需要再回表查詢,例如場景1,我們需要的是 學生、試卷、書包 三個字段,一開始,我們就讓學生們帶好試卷背好書包,這樣目的就是不需要再重新再喊一遍學生過來排隊了(回表),但是這得要求你有足夠的空間,一般來說,這個空間就是內存,因爲數據在內存中計算機纔可以處理,在數據庫領域就叫做 “sort buff”,那麼執行過程就像下面那樣:

(1)初始化 sort buff (收拾好辦公室) ,確定好需要的字段 (學生、試卷、分數、書包)、
(2)根據查詢條件找到第一個滿足條件的數據(在一堆試卷中找到第一個老王班的學生試卷)
(3)根據主鍵id(試卷),取出分數、學生和學生書包(根據試卷到班裏叫學生帶上書包和試卷到辦公室等)
(4)重複 (2)(3)步驟,直到叫完了所有學生
(5)對數據進行排序(根據成績分數讓學生排好隊)

辦公室的大小我們怎麼確定呢,在 mysql 中有這麼一個參數 sort_buff_size ,你可以在執行下列 SQL獲取到它的值:

root@localhost : (none) 12:19:05> SELECT @@sort_buffer_size;
+--------------------+
| @@sort_buffer_size |
+--------------------+
|            8388608 |
+--------------------+
1 row in set (0.00 sec)

這個值如果沒有特殊要求,一般都是採用默認配置,因爲每一個線程都是會開闢這麼一個內存空間,因此,如果這個值過大,很可能會造成數據庫內存佔用過高,引起系統 oom ,除了這個排序內存空間,mysql 還有一種通過外置文件輔助來排序的方式。我們可以通過以下 SQL 來判斷 SQL是否用到了文件排序,並且用到了幾個文件:
以下的 SQL 只能在 mysql 5.6 及以上版本能夠使用


/* 打開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';

/* 執行語句 */
/* 你的SQL語句 */

/* 查看 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;

如果你的數據庫是 MariaDB 或者 mysql 5.6以下,在打開optimizer_trace 時會報 Unknown system variable ‘optimizer_trace’,這是因爲版本不支持,你可以使用一下語句檢查你的數據庫版本:

SELECT @@VERSION, @@VERSION_COMMENT;
+---------------------+-------------------+
| @@VERSION           | @@VERSION_COMMENT |
+---------------------+-------------------+
| 10.0.13-MariaDB-log | MariaDB Server    |
+---------------------+-------------------+
1 row in set (0.00 sec)

在這裏插入圖片描述
我們可以看到, 在此我們解釋一下這些字段的意思:

rows : 掃描的函數

examined_rows: 參與排序的行數

number_of_tmp_files: 參數排序的文件數,最後會將這些文件合併到一起,其實類似場景3

sort_buffer_size :排序內存大小

sort_mode :排序模式,packed_additional_fields
的意思是,排序過程對字符串做了“緊湊”處理,即使字段是vachar類型,排序時也是按照實際長度來分配空間

注:這裏需要注意的是,爲了避免對結論造成干擾,需要把 internal_tmp_disk_storage_engine 設置成 MyISAM。否則,select @b-@a 的結果會顯示爲 4001。這是因爲查詢 OPTIMIZER_TRACE 這個表時,需要用到臨時表,而 internal_tmp_disk_storage_engine 的默認值是 InnoDB。如果使用的是 InnoDB 引擎的話,把數據從臨時表取出來的時候,會讓 Innodb_rows_read 的值加 1

MySQL的宗旨是儘可能的使用內存,如果內存夠放,就不要多事去用文件了,因爲文件有很多的性能問題,要考慮 io 等方面的磁盤訪問,一般來說,內存如果放不下,就需要用到外部排序,而且外部排序一般會使用歸併排序算法。

那麼我們如何去判斷有沒有用到外部文件呢?
根據 number_of_tmp_files 的值就可以了,如果值爲 0 ,說這個排序可以直接在內存中實現,不需要使用到文件,否則,說明還需要外部文件進行輔助排序。這就好比我辦公室夠大,就不用別人的辦公室來輔助呢

2.Row id 排序

考慮到一種情況,如果我們所需要的列太多了,很可能 sort_buff 能存的行數就會減少,那麼排序就需要用到更多了輔助文件了,這就好比說每個學生的書包都賊大,搞得辦公室能存下的學生就變少了,這種情況下 我們就需要換種策略來實現了。也就是我們常說的 row_id 排序,MySQL 有個字段是用來指定一行的最大長度的。

max_length_for_sort_data:
這是MySQL中專門控制用於排序的行數據長度的一個參數,如果一行的數據長度超過這個值,那麼,就需要換一個算法。上面的執行過程就會變成了下面這樣:
(1)初始化 sort buff (收拾好辦公室) ,確定好需要的字段 (試卷和分數)
(2)根據查詢條件找到第一個滿足條件的數據(在一堆試卷中找到第一個老王班的學生試卷)
(3)根據主鍵id(試卷),取出整行數據,把主鍵id(試卷)和分數 放進 sort_buff
(4)重複 (2)(3)步驟,直到找到了所有試卷
(5)對數據進行排序(根據成績讓學生排好隊)
(6)根據主鍵 id的值到原表中取出對應所需的其他字段(根據排好的試卷,到班裏叫對應的學生背好書包過來認領)

我們可以對比兩種算法,會發現這一種多了一次訪問表的操作,就是最後一個步驟
但是這裏需要強調的一點是,MySQL服務端從 sort_buff 依次取出 id 然後查表拿其他字段的結果,不再需要在服務端暫存結果,而是直接返回給客戶端,因爲沒有必要去把結果存起來,讓客戶端存儲即可

3.其他優化方式

我們對比了上面的兩種排序方式,認真的想一想,爲什麼需要排序,歸根結底就是原來的數據是無序的,那什麼情況下拿到的數據本身就是有序的呢?索引樹不是本身就有序嗎?
如果我們建立對應的覆蓋索引不是就可以免去排序的痛苦了。
也就是說,在上面的例子中,如果我們給班級和分數做了一個聯合索引,那麼取出來的數據本身不就是按照分數排好序的。

然後我們再考慮一種索引,覆蓋索引,覆蓋索引是指索引上的信息足夠滿足查詢的請求,不需要再回到主鍵索引中取數據。
因此,如果我們建立一個 班級、分數、學生 的索引,不就更好了,這樣就不需要回表(因爲普通索引存儲的是主鍵id的值,其他字段都是通過這個主鍵id去主鍵索引數取行數據)

到了這裏,排序的大致原理也就說完了,
例子還是有些小殘缺,例如試卷作爲主鍵id,本身來說不是特別準確,但是不影響使用哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章