1. 程式人生 > 其它 >MySQL45講之order工作原理

MySQL45講之order工作原理

本文介紹 order 的三種排序方式,全欄位排序、rowid 排序和索引樹排序,以及每種排序方式具體是如何工作的。

前言

本文介紹 order 的三種排序方式,全欄位排序、rowid 排序和索引樹排序,以及每種排序方式具體是如何工作的。

當使用 explain 檢視執行計劃時,如果 extra 中有 Using filesort,表示經過了排序。

MySQL 會在記憶體中分配一塊記憶體專門用來排序,可以通過 sort_buffer_size 設定大小。如果需要排序的資料量小於 sort_buffer_size,排序在記憶體中進行,否則,需要採用 外部排序方法,即藉助磁碟排序。

可以通過 OPTIMIZER_TRACE 的結果來檢視是否使用了臨時檔案,

/* 開啟optimizer_trace,只對本執行緒有效 */
SET optimizer_trace='enabled=on'; 

/* 檢視 OPTIMIZER_TRACE 輸出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`;

1. 全欄位排序

在 city 列已建立普通索引情況下,對於語句 select city,name,age from t where city='杭州' order by name limit 1000; 進行全欄位排序流程是:

  1. 初始化 sort_buffer,確定放入 city,name,age 欄位
  2. 遍歷 city 索引樹,找到第一個 city = '杭州' 的節點,拿到 id
  3. 根據 id 從主鍵索引樹中拿到需要返回的欄位值,放入 sort_buffer
  4. 迴圈執行 2,3 往後遍歷,直到 city != '杭州',然後再執行後面流程
  5. 對 sort_buffer 中的資料按 name 排序
  6. 取出前 1000 行返回客戶端

將要返回的欄位全部放到 sort_buffer 進行排序,所以叫全欄位排序。

這個演算法有個缺點,如果要返回的欄位很多,則一行資料的體積很大,這樣很可能要用到外部排序,並且一個檔案存下的行數有限,需要比較多的臨時檔案,臨時檔案一多,排序效能將十分低,所以這時 MySQL 會採用 rowid 排序演算法。

2. rowid排序

當返回的欄位很多時,MySQL 將採用 rowid 排序演算法。那欄位很多的標準是如何界定的呢?MySQL 有一個引數 max_length_for_sort_data,當欄位型別的總位元組數大於 max_length_for_sort_data 時將採用 rowid 演算法。比如,select city,name,age from t where city='杭州' order by name limit 1000;

中 city 和 name 字串長度都是 16,age 佔 4 位元組,即總共 36 位元組。

在 city 列已建立普通索引情況下,對於語句 select city,name,age from t where city='杭州' order by name limit 1000; 進行 rowid 排序流程是:

  1. 初始化 sort_buffer,確定放入 name, id 欄位
  2. 遍歷 city 索引樹,找到第一個 city = '杭州' 的節點,拿到 id
  3. 根據 id 從主鍵索引樹中拿到 name,id 欄位值,放入 sort_buffer
  4. 迴圈執行 2,3 往後遍歷,直到 city != '杭州',然後再執行後面流程
  5. 對 sort_buffer 中的資料按 name 排序
  6. 取出前 1000 行,按照 id 回到原表中取到 city,name,age 值再返回客戶端

從上面流程可見,rowid 排序演算法在 sort_buffer 中只放入了排序欄位和 id,儘可能避免了外部排序低效的問題,但排序之後,還需要回表重新取一遍返回值的資料。

3. 索引樹排序

你或許會問,那有沒有可以不排序的演算法?

有的,就是索引樹排序,因為欄位值在索引樹上已經有序,所以可以直接遍歷索引樹取到 id,然後到主鍵索引樹拿返回值返回,不需要再排序。

那能不能直接從索引樹中就拿到返回的資料,不要再回表呢?

當然也是可以的,這就是索引覆蓋的思想,比如 select city,name,age from t where city='杭州' order by name limit 1000; 語句,只要建立聯合索引 (city, name, age),就可以避免回表操作。

提問

假設你的表裡面已經有了 city_name(city, name) 這個聯合索引,然後你要查杭州和蘇州兩個城市中所有的市民的姓名,並且按名字排序,顯示前 100 條記錄。如果 SQL 查詢語句是這麼寫的 :

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;

select * from t where city in ('杭州',"蘇州") order by name limit 100;

1、那麼,這個語句執行的時候會有排序過程嗎,為什麼?

回答:會,因為 city_name 索引只能保證 city 相同的情況下,name 有序。而此時查詢兩個城市,那麼顯然不能保證按 name 有序。

2、如果業務端程式碼由你來開發,需要實現一個在資料庫端不需要排序的方案,你會怎麼實現呢?

回答:可以建立聯合索引 (name, city) 來避免排序。

3、進一步地,如果有分頁需求,要顯示第101頁,也就是說語句最後要改成 “limit 10000,100”, 你的實現方法又會是什麼呢?

回答:

沒有比較好的優化方法。首先看業務是否可以砍掉這個排序的需求,讓使用者只能一頁一頁翻,這樣使用者基本也就只會看前幾頁,就不需要考慮這個大分頁情況了。為了意義不大的功能優化,可能會得不償失。

如果實在需要,就可以先建立聯合索引 (name, city),再通過下面的 SQL 查詢。

SELECT * FROM t WHERE id IN ( SELECT id FROM t WHERE city IN ('杭州',"蘇州") ORDER BY name LIMIT 10000,100 ) AS tmp;

內查詢直接索引覆蓋,遍歷 10100 個節點,拿到末尾的 100 個 id,不需要回表。再在外查詢中,根據 id 從表中拿到資料返回客戶端。這樣,可以避免回表取 10100 次資料,如果符合的資料夠 10100 條的話。

參考