MySQL正確顯示隨機消息(排序)

 

 

一、概念

 

MySQL的另一種排序邏輯;mysql

 

在英語APP首頁有一個隨機顯示單詞的功能,也就是根據每一個用戶的級別有一個單詞表,而後這個用戶每次訪問首頁的時候,都會隨機滾動顯示三個單詞。他們發現隨着單詞表變大,選單詞這個邏輯變得愈來愈慢,甚至影響到了首頁的打開速度。算法

 

設計SQL語句應該如何進行設計呢?sql

 

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()

 

在這個表中插入1w行數據,接下來隨機選取三個,該如何設計呢?

 

二、內存臨時表

首先會想到使用orderby rand()實現這個邏輯

 

mysql> select word from words order by rand() limit 3;

 

這個語句邏輯寫法很簡單,可是語句執行狀況倒是很複雜的。

 

Extra字段顯示Using temporary,表示的是須要使用臨時表;Using filesort,表示的是須要執行排序操做。

 

所以這個Extra的意思就是,須要臨時表,而且須要在臨時表上排序。

 

 

對於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的表是用什麼方法來定位一行數據的。

 

 

order by rand()使用了內存臨時表,內存臨時表排序的時候使用了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

由於將max_length_for_sort_data設置成16,小於word字段的長度定義,因此咱們看到sort_mode裏面顯示的是rowid排序,這個是符合預期的,參與排序的是隨機值R字段和rowid字段組成的行。

 

數據總行數是10000,這樣算出來就有140000字節,超過了sort_buffer_size 定義的 32768字節了。可是,number_of_tmp_files的值竟然是0,難道不須要用臨時文件嗎?

 

這個SQL語句的排序確實沒有用到臨時文件,採用是MySQL 5.6版本引入的一個新的排序算法,即:優先隊列排序算法。接下來,咱們就看看爲何沒有使用臨時文件的算法,也就是歸併排序算法,而是採用了優先隊列排序算法。

 

其實,咱們如今的SQL語句,只須要取R值最小的3個rowid。可是,若是使用歸併排序算法的話,雖然最終也能獲得前3個值,可是這個算法結束後,已經將10000行數據都排好序了。

 

也就是說,後面的9997行也是有序的了。但,咱們的查詢並不須要這些數據是有序的。因此,想一下就明白了,這浪費了很是多的計算量。

 

而優先隊列算法,就能夠精確地只獲得三個最小值,執行流程以下:

 

1)對於這10000個準備排序的(R,rowid),先取前三行,構形成一個堆;

(對數據結構印象模糊的同窗,能夠先設想成這是一個由三個元素組成的數組)

 

2)取下一個行(R’,rowid’),跟當前堆裏面最大的R比較,若是R’小於R,把這個(R,rowid)從堆中去掉,換成(R’,rowid’);

 

3)重複第2步,直到第10000個(R’,rowid’)完成比較。

 

 

 

圖6是模擬6個(R,rowid)行,經過優先隊列排序找到最小的三個R值的行的過程。整個排序過程當中,爲了最快地拿到當前堆的最大值,老是保持最大值在堆頂,所以這是一個最大堆。

 

圖5的OPTIMIZER_TRACE結果中,filesort_priority_queue_optimization這個部分的chosen=true,就表示使用了優先隊列排序算法,這個過程不須要臨時文件,所以對應的number_of_tmp_files是0。

 

這個流程結束後,咱們構造的堆裏面,就是這個10000行裏面R值最小的三行。而後,依次把它們的rowid取出來,去臨時表裏面拿到word字段,這個過程就跟上一篇文章的rowid排序的過程同樣了。

 

四、隨機排序方法

 

若是隻隨機選擇1個word值,能夠怎麼作呢?思路上是這樣的:

 

取得這個表的主鍵id的最大值M和最小值N;

 

用隨機函數生成一個最大值到最小值之間的數 X = (M-N)*rand() + N;

 

取不小於X的第一個ID的行。

 

咱們把這個算法,暫時稱做隨機算法1。這裏,我直接給你貼一下執行語句的序列:

 

這個方法效率很高,由於取max(id)和min(id)都是不須要掃描索引的,而第三步的select也能夠用索引快速定位,能夠認爲就只掃描了3行。但實際上,這個算法自己並不嚴格知足題目的隨機要求,由於ID中間可能有空洞,所以選擇不一樣行的機率不同,不是真正的隨機。

 

好比你有4個id,分別是一、二、四、5,若是按照上面的方法,那麼取到 id=4的這一行的機率是取得其餘行機率的兩倍。

 

若是這四行的id分別是一、二、40000、40001呢?這個算法基本就能當bug來看待了。

 

因此,爲了獲得嚴格隨機的結果,你能夠用下面這個流程:

 

取得整個表的行數,並記爲C。

 

取得 Y = floor(C * rand())。 floor函數在這裏的做用,就是取整數部分。

 

再用limit Y,1 取得一行。

 

咱們把這個算法,稱爲隨機算法2。下面這段代碼,就是上面流程的執行語句的序列。

 

mysql> select count(*) into @C from t;

set @Y = floor(@C * rand());

set @sql = concat("select * from t limit ", @Y, ",1");

prepare stmt from @sql;

execute stmt;

DEALLOCATE prepare stmt;

因爲limit 後面的參數不能直接跟變量,因此我在上面的代碼中使用了prepare+execute的方法。你也能夠把拼接SQL語句的方法寫在應用程序中,會更簡單些。

 

這個隨機算法2,解決了算法1裏面明顯的機率不均勻問題。

MySQL處理limit Y,1 的作法就是按順序一個一個地讀出來,丟掉前Y個,而後把下一個記錄做爲返回結果,所以這一步須要掃描Y+1行。再加上,第一步掃描的C行,總共須要掃描C+Y+1行,執行代價比隨機算法1的代價要高。

固然,隨機算法2跟直接order by rand()比起來,執行代價仍是小不少的。

你可能問了,若是按照這個表有10000行來計算的話,C=10000,要是隨機到比較大的Y值,那掃描行數也跟20000差很少了,接近order by rand()的掃描行數,爲何說隨機算法2的代價要小不少呢?我就把這個問題留給你去課後思考吧。

如今,咱們再看看,若是咱們按照隨機算法2的思路,要隨機取3個word值呢?你能夠這麼作:

  1. 取得整個表的行數,記爲C;
  2. 根據相同的隨機方法獲得Y一、Y二、Y3;
  3. 再執行三個limit Y, 1語句獲得三行數據。

 

咱們把這個算法,稱做隨機算法3。下面這段代碼,就是上面流程的執行語句的序列。

mysql> select count(*) into @C from t;

set @Y1 = floor(@C * rand());

set @Y2 = floor(@C * rand());

set @Y3 = floor(@C * rand());

select * from t limit @Y1,1; //在應用代碼裏面取Y一、Y二、Y3值,拼出SQL後執行

select * from t limit @Y2,1;

select * from t limit @Y3,1;

五、小結

介紹了MySQL對臨時表排序的執行過程。

若是你直接使用order by rand(),這個語句須要Using temporary 和 Using filesort,查詢的執行代價每每是比較大的。因此,在設計的時候你要量避開這種寫法。

今天的例子裏面,咱們不是僅僅在數據庫內部解決問題,還會讓應用代碼配合拼接SQL語句。在實際應用的過程當中,比較規範的用法就是:儘可能將業務邏輯寫在業務代碼中,讓數據庫只作「讀寫數據」的事情。所以,這類方法的應用仍是比較普遍的。

相關文章
相關標籤/搜索