MySQL Order BY 排序過程


MySQL 在進行 Order By 操作排序時,通常有兩種排序方式:

  • 全字段排序
  • Row_id 排序

MySQL 中每個線程在執行排序時,都會被分配一塊區域 - sort buffer,它的大小通過 sort_buffer_size 控制。

全字段排序指的是,將要查詢的字段,全都存入 sort buffer 中,然后對 sort buffer 進行排序,然后將結果返回給客戶端。

Row_id 排序:將被排序的字段和對應主鍵索引的 ID 放入,sort buffer 中,然后對 sort buffer 進行排序,最后額外進行一次回表操作查找額外的信息,然后將結果返回給客戶端。

全字段排序和 Row_id 排序的主要區別在:

  1. sort buffer 存入的內容不同
  2. 回表查詢的次數不一致。

對於 InnoDB 表來說,在內存足夠的情況下,會優先選擇全字段排序的方式。在內存不足的情況下,可能會借用外部文件進行排序。

但如果單行內容較大時,會導致拆分的外部文件過多,進行歸並排序時,效率變低。此時會采用 Row_id 的排序方式。

對於 Memory 表來說,會優先選擇 Row_id 的排序方式。

接下來會對全字段排序和 Row_id 排序進行驗證,最后並給出一些調優的技巧。

環境准備

假設存在如下表結構,表里有 5萬的數據行, 其中 type 為二級索引。

# MySQL5.7.28, RR
CREATE TABLE `test_table` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(16) NOT NULL,
  `type` varchar(16) NOT NULL,
  `phone` int(11) NOT NULL,
  `addr` varchar(128) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `type` (`type`)
) ENGINE=InnoDB;

向表中插入數據:

import random
import MySQLdb

def prepare_data():
    result = []
    type = ["a", "b", "c", "d", "e"]

    for i in range(50000):
        index = random.randint(0, 4)
        result.append((str(i), str(type[index]), str(i + 10), str(i)))
    return result

def insert_data():
    db = MySQLdb.connect(host='10.124.207.xxx',
                         user='xxxx',
                         passwd='xxxxx',
                         db='usecase',
                         charset='utf8')
    sql = 'INSERT INTO test_table ( name, type, phone, addr) VALUES ( %s, %s, %s, %s);'
    cur = db.cursor()
    cur.executemany(sql, prepare_data())
    db.commit()
    db.close()

if __name__ == '__main__':
    try:
        insert_data()
    except Exception as e:

下面會進行查詢操作:

``select name,type,phone from test_table where type='a' order by name limit 1000 ;`

並對排序的過程進行分析。

全字段排序

這里對該查詢語句進行了 EXPLAIN 操作,可以看到在 Extra 列 :

  • 用到了索引
  • 進行了排序操作 - filesort (無法利用索引默認有序的情況)

針對本次 SELECT 來說,經歷如下的過程:

  1. 使用 type = a 的二級索引,找到滿足的第一個值。
  2. 根據該值,找到主鍵 ID。回表去找 name 和 phone 的值。
  3. 然后將 type,name, phone 存入 sort_buffer。
  4. 然后重復 1 - 4 過程,查詢所有 type =a 的內容,然后將 name, phone, type 存入 sort_buffer。
  5. 然后對分配內存里的信息進行排序。
  6. 然后選擇前 1000 條,返回給客戶端。

打開 optimizer_trace ,查看執行的流程:

可以看到,對應 "sort_mode": 中的內容為 sort_key 和其他字段,這就表示用的是全字段排序。

chosen: true 表示使用的是優先隊列的排序算法。

但在實際情況下,往往會出現待排序的內容大於分配用於排序的空間,此時就會用到外部的文件排序,而這種外部排序一般都使用歸並排序。

將 sort_buffer 調小,重新執行:

將默認大小臨時改小:

# 將 size 調小, 並重新登錄
set sort_buffer_size= 24 * 1024;

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

/* 執行語句 */
select type,name,phone from test_table where type='a' order by name limit 1000 ;

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

在 OPTIMIZER_TRACE 中查看:

number_of_tmp_files 表示使用的臨時文件數為 20,上面的 chosen 表示由於空間不足,無法使用堆排序。

可以發現:

  • sort_buffer_size 大於要待排序的內容,則使用內存排序。

  • 如果小於,則使用外部臨時文件輔助排序。

但還有一種情況,就是待排序的內容數據量太大或者單行查詢的字段太多(如 SELECT *)這種情況,會導致生成的臨時文件數量太多,效率不高。所以就出現了另一種 Row_id 的排序方式。

Row_id 排序

在單行查詢字段很多時,在 sort_buffer 中僅僅保存必要的字段,最后額外再進行一次統一回表的操作,查詢必要的信息。

這里可以將 SET max_length_for_sort_data = 16; 改小,模擬這種情況。

這里 sort_mode: sort_key, rowid 指的就是用的 Row_id 這種處理方式。

對應經歷的過程就會變成:

  1. 使用 type = a 的二級索引,找到滿足的第一個值。
  2. 根據該值,找到主鍵 ID。回表去找 row_id 的值。(發生變化)
  3. 然后將 type, row_id 存入 sort_buffer。(發生變化)
  4. 然后重復 1 - 4 過程,查詢所有 type = a 的內容,然后將 type, row_id 存入 sort_buffer。(發生變化)
  5. 然后對分配內存里的信息進行排序。
  6. 然后選擇前 1000 條,並取到需要的 row_id 集合。
  7. 根據 row_id,回表查詢所需要的信息,然后返回給客戶端。(發生變化)

這里 2, 3, 4, 7 和之前全字段排序相比,發生了變化。

但需要注意的是,在內存足夠的情況下,InooDB 會優先選擇全排序的方式。但對於 Memory 方式的表結構,則會有不同的選擇。

內存臨時表的排序選擇

在如使用 Union 或者 Group By 等查詢的情況下,會創建臨時表,采用 Memory 作為存儲的引擎。

而對於內存臨時表,會優先采用 row_id 排序。

因為內存臨時表,本身會在原表基礎上,新建一張臨時表保存需要的信息。因為臨時表本身就在內存中,所以最后一次回表的操作,不會進行額外的磁盤 IO。所以 MySQL 會優先選擇 row_id 的排序方式。

優化方法

場景1:利用索引有序,讓 Order by 不排序

之前僅僅有 type 類型的索引,可以將其改成 type, name 的聯合索引:

alter table test_table add index type_name(type, name);

在創建聯合索引時,因為本身有序。在查詢 typem,name,phone 時,會將 sort buffer 中的排序過程省略,也就是全排序過程中的第五步。

可以看到,在 Extra 中 filesort 的過程,已經被省略了。

場景2:利用覆蓋索引,簡化回表流程

alter table test_table add index type_name(type, name, phone);

這里由於想要查詢的字段,都已經在二級索引上了。所以不需要進行回表,而且本身也是有序的。

並且可以發現,Extra 變成了 Using index. 表示直接使用了索引。

場景3:IN 導致 Order By 需要排序

假設存在 type_name(type, name); 聯合索引。

將之前的 type = 換成了 IN,由於是對 ab 兩個類型同時排序,所以就需要 filesort 操作。

如果不想讓 MySQL 進行排序的操作,可以將 IN 拆分成多個 = 執行,然后在調用端,自己進行合並。

總結

在 MySQL 中,使用 Order BY 時,通常有全字段排序和 Row_id 兩種排序方式。

對於 InooDB 來說,在內存足夠的情況下,會優先選擇全字段排序。在內存不足,並所需排序內容不多時,會采用外部歸並排序的方式。

但如果所在單行內容太大,導致拆分文件過多的情況下,會選擇 Row_id 的排序方式。

對於 Memory 表來說,由於本身就在內存中,所以會優先選擇 Row_id 的排序方式。

使用 Order By 操作時,不一定真的意味着真的去做排序,可以利用索引本身有序,或者覆蓋索引,拆分 SQL 的方式,減少 MySQL 的排序過程。

參考

題外話:最近在系統的學習 MySQL,推薦一個比較好的學習材料就是<<丁奇老師的 MySQL 45 講>>,鏈接已經附在文章末尾。

文章中很多知識點就是從中學來,加入自己的理解並整理的。

大家在購買后,強烈推薦讀一讀評論區的內容,價值非常高,不少同學問出了自己在思考時的一些困惑。

丁奇 MySQL 45 講鏈接

ordery by 優化

taobao-內部臨時表


免責聲明!

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



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