mysql 記憶體藝術 MySQL 排序的藝術

2021-10-18 03:06:18 字數 4905 閱讀 4330

前言

業務中的各種查詢通常對應了使用者所看到的各項列表,列表一般是根據某個維度進行排序。

換句話說,業務中使用 select 語句的時候除了不可避免的搭配 where 以外,還會配合 order by 進行使用。

今天來好好聊聊 mysql 的 order by 排序。

排序演算法

說到排序演算法,有插入排序、選擇排序、歸併排序、堆排序、快速排序、計數排序、桶排序、基數排序、氣泡排序、希爾排序、梳排序 ...

關於各種排序演算法的排序流程和具體實現,不是本篇部落格的重點,不作詳細說明。

這裡直接貼各類排序演算法的時空複雜度:

通常我們實現的這些排序演算法,都是在」純記憶體「環境中進行。

mysql 作為資料庫難道是在先將所有要排序的資料載入到記憶體,再應用排序演算法嗎?

mysql 的排序方案

在分析 mysql 的不同的排序方案之前,先來了解 sort buffer 概念。

mysql 會為每個執行緒分配固定大小的 sort buffer 用作排序。

sort buffer 是具有邏輯概念的記憶體區域,大小由 sort_buffer_size 引數控制,預設為 256 kb。

由於 sort buffer 大小固定,而 data(待排序的資料量)並不固定,所以根據 sort buffer 與 data(待排序資料量)的大小差值,可分為內部排序和外部排序:

data <= sort buffer:即 sort buffer 夠用,這時候 mysql 只需要在記憶體中進行排序即可。內部排序使用的是快速排序

data > sort buffer:這時候 sort buffer 不夠用,mysql 需要借助外部「容器」(通常是檔案)進行排序。通常會將待排序資料分成多個「小檔案」,對各個「小檔案」進行排序,再彙總成乙個有序的「大檔案」。外部排序使用的是歸併排序

如何驗證當前執行的排序語句使用的是內部排序還是外部排序?

可以通過 explain 命令來檢視,如果在分析結果中的 extra 欄位裡包含 using filesort 字眼,說明執行了外部排序操作。

全欄位排序

全欄位排序是指,只要與最終結果集有關的字段都會被放進 sort buffer,而不管該欄位本身是否參與排序。

以下面的 sql 為例子:

select nick_name, age, phone

from t_user

where city = "深圳"

order by nick_name;

假設 city 欄位上有索引,全欄位排序的過程:

從 city 索引樹上找到第一條值為深圳的資料,取得 id 之後回表(回到主鍵索引)取得 nick_name、age、phone 三個字段放入 sort buffer

從 city 索引樹取下一條值為深圳的資料,重複 1 過程,直到下一條資料不滿足值為深圳條件

到這一步,所有 city = 深圳 的資料都在 sort buffer 了。對 nick_name 執行快速排序

將排序結果返回

可以看到當查詢條件本身有索引可用的話,全欄位排序的排序過程都在 sort buffer(記憶體)進行,回表次數為符合條件的資料個數。

當然,如果我們建立的是 city、nick_name、age、phone 的聯合索引,還可以實現「索引覆蓋」,即在一棵索引樹上取得全部所需資料,減少回表(隨機讀)次數。

不過針對每個查詢或排序語句建立聯合索引,會導致索引過多,大大降低寫入更新資料的速度,以及大大提公升資料所需要的儲存空間。

生產上對索引的建立修改需要格外謹慎。

rowid 排序

rowid 就是 mysql 對每行資料的唯一識別符號。

當資料表有主鍵時,rowid 就是表主鍵;當資料表沒有主鍵或者主鍵被刪除時,mysql 會自動生成乙個長度為 6 位元組的 rowid 為作為 rowid。

rowid 排序是指只將與排序相關的字段和 rowid 放入 sort buffer,其餘結果集需要用到的資料在排序完成後,通過 rowid 回表取得。

全欄位排序的流程看著已經十分合理,為什麼還需要有個 rowid 排序?

這是我們只需要輸出三個欄位的情況,假如我們有上百個字段需要返回呢?sort buffer 預設只有 256 kb。能夠裝下多少行的原始資料行?

所以當待排序的資料行很大的時候,使用全欄位排序必然會導致「外部排序」。而且是使用很多臨時檔案的「外部排序」,效率很低下。

相比全欄位排序,rowid 排序的好處是在 sort buffer 大小固定的情況下,sort buffer 能夠容納更多的資料行,能夠避免使用或者少使用「外部排序檔案」。

缺點是最終返回結果集的時候,需要再次進行回表。

還是之前那個例子:

select nick_name, age, phone

from t_user

where city = "深圳"

order by nick_name;

rowid 排序全過程:

從 city 索引樹上找到第一條值為深圳的資料,取得 id 之後回表(回到主鍵索引)取得 nick_name 這個與排序相關的字段和主鍵 id 一起放入 sort buffer

從 city 索引樹取下一條值為深圳的資料,重複 1 過程,直到下一條資料不滿足值為深圳條件

這時候,所有 city = 深圳 的資料都在 sort buffer 了(sort buffer 裡面的資料報含兩個字段:id 和 nick_name)。對 nick_name 執行快速排序

利用排序好的資料,使用主鍵 id 再次回表取其他字段,將結果返回

注意:在步驟 4 中不會等所有排序好的 id 回表完再返回,而是每個 id 回表一次,取得該行資料之後立即返回,所以不會消耗額外的記憶體。

優先佇列排序

無論是使用全欄位排序還是 rowid 排序,都不可避免了對所有符合 whrer 條件的資料進行了排序。

有讀者可能會認為,那不是應該的嗎?

設想一下,如果我們還搭配著 limit 使用呢?

例如我們在排序語句後新增 limit 3 ,哪怕查出來的資料有 10w 行,我們也只需要前 3 行有序。

為了得到前 3 行資料,而不得不將 10w 行資料載入記憶體,大大降低了 sort buffer 的利用率。

這時候你可能想到利用「最小堆」、「最大堆」來進行排序。

沒錯,這正是 mysql 針對帶有 limit 的 order by 語句的優化:使用優先佇列進行排序。

以下面的 sql 為例子:

select nick_name, age, phone

from t_user

where city = "深圳"

order by nick_name limit 3;

優先佇列進行排序的流程:

在所有待排序的資料,取數量為

limit (本例中為 3)的資料,構建乙個堆

不斷的取下一行資料,更新堆節點

當所有行的掃瞄完,得到最終的排序結果

如何選擇?

現在我們知道有全欄位排序和 rowid 排序,那麼 mysql 是如何在這兩種排序方案中做選擇呢?

由於 rowid 排序相對於全欄位排序,不可避免的多了一次回表操作,回表操作意味著隨機讀,而隨機 io 是資料庫中最昂貴的操作。

所以 mysql 會在盡可能的情況下選擇全欄位排序。

那麼什麼情況下 mysql 會選擇 rowid 排序呢,是否有具體的值可以量度?

答案是有的,通過引數 max_length_for_sort_data 可以控制用於排序的行資料最大長度,預設值為 1024 位元組。

當單行資料長度超過該值,mysql 就會覺得如果還用全欄位排序,會導致 sort buffer 容納下的行數太少,從而轉為使用 rowid 排序。

臨時表排序

通常對於乙個執行較慢的排序語句,在使用 explain 進行執行過程分析的時候除了能看到 using filesort 以外,還能看到 using temporary,代表在排序過程中使用到了臨時表。

記憶體臨時表排序

mysql 優先使用記憶體臨時表。當 mysql 使用記憶體臨時表時,臨時表儲存引擎為 memory 。

如果當前 mysql 使用的是記憶體臨時表的話,將會直接使用 rowid 排序,因為這時候所謂的「回表」只是在記憶體表中讀資料,操作不涉及硬碟的隨機 io 讀。

使用 rowid 可以在 sort buffer 容納更多的行,避免或減少外部排序檔案的使用。

磁碟臨時表排序

如果系統中很多需要使用臨時表的排序語句執行,而又不加以限制,全都使用臨時表的話,記憶體很快就會被打滿。

所以 mysql 提供了 tmp_table_size 引數限制了記憶體臨時表的大小,預設值是 16m。

如果臨時表大小超過了tmp_table_size,那麼記憶體臨時表就會轉成磁碟臨時表。

當使用磁碟臨時表的時候,表儲存引擎將不再是 memory,而是由 internal_tmp_disk_storage_engine 引數控制,預設為 innodb 。

這時候 mysql 會根據單行大小是否超過 max_length_for_sort_data 決定採用全欄位排序還是 rowid 排序。

總結總結一下,mysql 總是使用 「最快」 的排序方案:

當排序資料量不超過 sort buffer 容量時,mysql 將會在記憶體使用快速排序演算法進行排序(內部排序);當排序資料量超過 sort buffer 容量時,mysql 將會借助臨時磁碟檔案使用歸併排序演算法進行排序(外部排序)

在進行真正排序時,mysql 又會根據資料單行長度是否超過

max_length_for_sort_data而決定使用 rowid 排序還是全欄位排序,優先選擇全欄位排序,以減少回表次數

當需要借助臨時表的時候,mysql 會優先使用記憶體臨時表(此時表引擎為 memory 引擎),回記憶體臨時表取資料並不涉及隨機讀,也不涉及掃瞄行,效率較高。所以在配合記憶體臨時表的時候,會使用 rowid 排序方式;當記憶體臨時表大小超過

tmp_table_size 限制時,則需要將記憶體臨時表轉換為磁碟臨時表,這時候由於回表意味著隨機讀,所以會搭配全欄位排序方式

mysql記憶體結構 MySQL記憶體結構

實際上mysql記憶體的組成和oracle類似,也可以分為sga 系統全域性區 和pga 程式快取區 mysql show variables like buffer 一 sga 1.innodb buffer bool 用來快取innodb表的資料 索引 插入緩衝 資料字典等資訊。2.innodb...

mysql記憶體釋放 MySQL記憶體不釋放

歡迎進入linux社群論壇,與200萬技術人員互動交流 進入 連線了一下,並進行了一次查詢操作 root nj 245 thu apr 24 16 38 38 2014 information schema select count from client statistics count 5 1 ...

mysql 記憶體使用 mysql記憶體使用分析 一

author skate time 2012 02 16 mysql記憶體使用分析 從記憶體的使用方式來說,mysql的記憶體使用主要分為以下兩類 1.執行緒獨享記憶體 2.全域性共享記憶體 1.執行緒獨享記憶體 在mysql 中,執行緒獨享記憶體主要用於各客戶端連線線程儲存各種操作的獨享資料,如執...