總結寫在前面:
1. 不建議直接使用order by rand(),原因是執行代價比較大
2. 介紹了內存臨時表,對於內存臨時表,由於回表不需要訪問磁盤,所以往往是用rowid排序,可以減少參與排序字段
3. 介紹了磁盤臨時表,當臨時表大小超過了 tmp_table_size的時候,就會使用磁盤存儲。
4. 介紹了優先隊列排序算法,該算法內使用了最大堆的思想,當排序時需要維護的堆的大小比sort_buffer_size小的時候(維護的堆大小往往跟需要取出的行數和排序字段相關),會使用該算法,否則會使用歸並排序算法,借助臨時文件排序。
現有一個需求:從一個單詞表中隨機選出三個單詞。
先創建一張單詞表,並且插入10000條數據
mysql> CREATE TABLE `words` ( `id` int(11) NOT NULL AUTO_INCREMENT, `word` varchar(64) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; delimiter ;; create procedure idata() begin declare i int; set i=0; while i<10000 do insert into words(word) values(concat(char(97+(i div 1000)), char(97+(i % 1000 div 100)), char(97+(i % 100 div 10)), char(97+(i % 10)))); set i=i+1; end while; end;; delimiter ; call idata();
接下來,我們一起探討下要隨機選擇 3 個單詞,有什么方法實現,存在什么問題以及如何改進。
內存臨時表
首先,我們用 order by rand() 來實現這個邏輯。
mysql> select word from words order by rand() limit 3;
意思很直白,隨機排序取前 3 個。這個 SQL 語句寫法很簡單,但執行流程卻有點復雜的。
用 explain 命令來看看這個語句的執行情況。
Extra 字段顯示 Using temporary,表示的是需要使用臨時表;Using filesort,表示的是需要執行排序操作。
意思就是,需要臨時表,並且需要在臨時表上排序。
那么對於臨時內存表的排序來說,它會選擇哪一種算法呢?
在 orderby工作機制 里說過對於 InnoDB 表來說,執行全字段排序會減少磁盤訪問,因此會被優先選擇。
這里強調“InnoDB 表”,但對於內存表,回表過程只是簡單地根據數據行的位置,直接訪問內存得到數據,根本不會導致多訪問磁盤。
優化器沒有了這一層顧慮,那么它會優先考慮的,就是用於排序的行越小越好了,所以,MySQL 這時就會選擇 rowid 排序。
接着看看語句的執行流程,嘗試分析一下語句的掃描行數:
1. 創建一個臨時表。這個臨時表使用的是 memory 引擎,表里有兩個字段,第一個字段是 double 類型,為了后面描述方便,記為字段 R,第二個字段是 varchar(64) 類型,記為字段 W。並且,這個表沒有建索引。
2. 從 words 表中,按主鍵順序取出所有的 word 值。對於每一個 word 值,調用 rand() 函數生成一個大於 0 小於 1 的隨機小數,並把這個隨機小數和 word 分別存入臨時表的 R 和 W 字段中,到此,掃描行數是 10000。
3. 現在臨時表有 10000 行數據了,接下來要在這個沒有索引的內存臨時表上,按照字段 R 排序。
4. 初始化 sort_buffer。sort_buffer 中有兩個字段,一個是 double 類型,另一個是整型。
5. 從內存臨時表中一行一行地取出 R 值和位置信息(后面會解釋這里為什么是“位置信息”),分別存入 sort_buffer 中的兩個字段里。這個過程要對內存臨時表做全表掃描,此時掃描行數增加 10000,變成了 20000。
6. 在 sort_buffer 中根據 R 的值進行排序。注意,這個過程沒有涉及到表操作,所以不會增加掃描行數。
7. 排序完成后,取出前三個結果的位置信息,依次到內存臨時表中取出 word 值,返回給客戶端。這個過程中,訪問了表的三行數據,總掃描行數變成了 20003。
通過慢查詢日志(slow log)可以驗證我們分析得到的掃描行數是否正確。
# Query_time: 0.900376 Lock_time: 0.000347 Rows_sent: 3 Rows_examined: 20003 SET timestamp=1541402277; select word from words order by rand() limit 3;
其中,Rows_examined:20003 就表示這個語句執行過程中掃描了 20003 行,也就驗證了我們分析得出的結論。
下面解釋下“位置信息”。
先提個問題 : MySQL 的表是用什么方法來定位“一行數據”的? 主鍵?如果把主鍵刪了呢?
簡單解答如下:
如果創建的表沒有主鍵,或者把一個表的主鍵刪掉了,那么 InnoDB 會自己生成一個長度為 6 字節的 rowid 來作為主鍵。
這也是排序模式里面,rowid 名字的來歷。實際上它表示的是:每個引擎用來唯一標識數據行的信息。
- 對於有主鍵的 InnoDB 表來說,這個 rowid 就是主鍵 ID;
- 對於沒有主鍵的 InnoDB 表來說,這個 rowid 就是由系統生成的;
- MEMORY 引擎不是索引組織表。在這個例子里面,你可以認為它就是一個數組。因此,這個 rowid 其實就是數組的下標。
即有主鍵,位置信息就是主鍵;沒主鍵,位置信息就是rowid。
磁盤臨時表
不是所有的臨時表都是內存表。tmp_table_size 這個配置限制了內存臨時表的大小,默認值是 16M。
如果臨時表大小超過了 tmp_table_size,那么內存臨時表就會轉成磁盤臨時表。
磁盤臨時表使用的引擎默認是 InnoDB,是由參數 internal_tmp_disk_storage_engine 控制的。
當使用磁盤臨時表的時候,對應的就是一個沒有顯式索引的 InnoDB 表的排序過程。
為了復現這個過程,把 tmp_table_size 設置成 1024,把 sort_buffer_size 設置成 32768, 把 max_length_for_sort_data 設置成 16。
set tmp_table_size=1024; set sort_buffer_size=32768; set max_length_for_sort_data=16; /* 打開 optimizer_trace,只對本線程有效 */ SET optimizer_trace='enabled=on'; /* 執行語句 */ select word from words order by rand() limit 3; /* 查看 OPTIMIZER_TRACE 輸出 */ SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
然后,我們來看一下這次 OPTIMIZER_TRACE 的結果。
sort_mode 里面顯示的是 rowid 排序,這個是符合預期的,因為將 max_length_for_sort_data 設置成 16,小於 word 字段的長度定義,參與排序的是隨機值 R 字段和 rowid 字段組成的行。
但是number_of_tmp_files 的值居然是 0,難道不需要用臨時文件嗎?R 字段存放的隨機值就 8 個字節,rowid 是 6 個字節(至於為什么是 6 字節,就留給你課后思考吧),數據總行數是 10000,這樣算出來就有 140000 字節,超過了 sort_buffer_size 定義的 32768 字節了。感覺不太對。
但實際上這個 SQL 語句的排序確實沒有用到臨時文件,采用是 MySQL 5.6 版本引入的一個新的排序算法,即:優先隊列排序算法。接下來,我們就看看為什么沒有使用臨時文件的算法,也就是歸並排序算法,而是采用了優先隊列排序算法。
我們現在的 SQL 語句,其實只需要取 R 值最小的 3 個 rowid。使用歸並排序算法的話,雖然最終也能得到前 3 個值,但其實已經將 10000 行數據都排好序了。浪費了非常多的計算量。
而優先隊列算法,就可以精確地只得到三個最小值,執行流程如下:
1. 對於這 10000 個准備排序的 (R,rowid),先取前三行,構造成一個堆;(對數據結構印象模糊的同學,可以先設想成這是一個由三個元素組成的數組)
2. 取下一個行 (R’,rowid’),跟當前堆里面最大的 R 比較,如果 R’小於 R,把這個 (R,rowid) 從堆中去掉,換成 (R’,rowid’);
3. 重復第 2 步,直到第 10000 個 (R’,rowid’) 完成比較。
整個排序過程中,為了最快地拿到當前堆的最大值,總是保持最大值在堆頂,因此這是一個最大堆【這也保證了新加入的(R’,rowid’)會跟堆里另外兩個排序,最終選出最小的三個】。
OPTIMIZER_TRACE 結果中,filesort_priority_queue_optimization 這個部分的 chosen=true,就表示使用了優先隊列排序算法,這個過程不需要臨時文件,因此對應的 number_of_tmp_files 是 0。
這個流程結束后,我們構造的堆里面,就是這個 10000 行里面 R 值最小的三行。然后,依次把它們的 rowid 取出來,去臨時表里面拿到 word 字段。
我們再看一下 orderby工作機制 文章的 SQL 查詢語句:
select city,name,age from t where city='杭州' order by name limit 1000 ;
這里也用到了 limit,為什么沒用優先隊列排序算法呢?
原因是,這條 SQL 語句是 limit 1000,如果使用優先隊列算法的話,需要維護的堆的大小就是 1000 行的 (name,rowid),超過了設置的 sort_buffer_size 大小,所以只能使用歸並排序算法。
可以看到,不論是使用哪種類型的臨時表,order by rand() 這種寫法都會讓計算過程非常復雜,需要大量的掃描行數,因此排序過程的資源消耗也會很大。