17 | 如何正確地顯示隨機消息?

我在上一篇文章,為你講解完order by語句的幾種執行模式后,就想到了之前一個做英語學習App的朋友碰到過的一個性能問題。今天這篇文章,我就從這個性能問題說起,和你說說MySQL中的另外一種排序需求,希望能夠加深你對MySQL排序邏輯的理解。

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

現在,如果讓你來設計這個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行記錄。接下來,我們就一起看看要隨機選擇3個單詞,有什么方法實現,存在什么問題以及如何改進。

內存臨時表

首先,你會想到用order by rand()來實現這個邏輯。

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

這個語句的意思很直白,隨機排序取前3個。雖然這個SQL語句寫法很簡單,但執行流程卻有點復雜的。

我們先用explain命令來看看這個語句的執行情況。

圖1 使用explain命令查看語句的執行情況

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

因此這個Extra的意思就是,需要臨時表,并且需要在臨時表上排序。

這里,你可以先回顧一下上一篇文章中全字段排序和rowid排序的內容。我把上一篇文章的兩個流程圖貼過來,方便你復習。

圖2 全字段排序

圖3 rowid排序

然后,我再問你一個問題,你覺得對于臨時內存表的排序來說,它會選擇哪一種算法呢?回顧一下上一篇文章的一個結論:對于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行,也就驗證了我們分析得出的結論。

這里插一句題外話,在平時學習概念的過程中,你可以經常這樣做,先通過原理分析算出掃描行數,然后再通過查看慢查詢日志,來驗證自己的結論。我自己就是經常這么做,這個過程很有趣,分析對了開心,分析錯了但是弄清楚了也很開心。

現在,我來把完整的排序執行流程圖畫出來。

圖4 隨機排序完整流程圖1

圖中的pos就是位置信息,你可能會覺得奇怪,這里的“位置信息”是個什么概念?在上一篇文章中,我們對InnoDB表排序的時候,明明用的還是ID字段。

這時候,我們就要回到一個基本概念:MySQL的表是用什么方法來定位“一行數據”的。

在前面第4和第5篇介紹索引的文章中,有幾位同學問到,如果把一個InnoDB表的主鍵刪掉,是不是就沒有主鍵,就沒辦法回表了?

其實不是的。如果你創建的表沒有主鍵,或者把一個表的主鍵刪掉了,那么InnoDB會自己生成一個長度為6字節的rowid來作為主鍵。

這也就是排序模式里面,rowid名字的來歷。實際上它表示的是:每個引擎用來唯一標識數據行的信息。

  • 對于有主鍵的InnoDB表來說,這個rowid就是主鍵ID;
  • 對于沒有主鍵的InnoDB表來說,這個rowid就是由系統生成的;
  • MEMORY引擎不是索引組織表。在這個例子里面,你可以認為它就是一個數組。因此,這個rowid其實就是數組的下標。

到這里,我來稍微小結一下: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 

圖5 OPTIMIZER_TRACE部分結果

然后,我們來看一下這次OPTIMIZER_TRACE的結果。

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

這時候你可能心算了一下,發現不對。R字段存放的隨機值就8個字節,rowid是6個字節(至于為什么是6字節,就留給你課后思考吧),數據總行數是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),先取前三行,構造成一個堆;

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

  1. 取下一個行(R’,rowid’),跟當前堆里面最大的R比較,如果R’小于R,把這個(R,rowid)從堆中去掉,換成(R’,rowid’);

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

這里我簡單畫了一個優先隊列排序過程的示意圖。

圖6 優先隊列排序算法示例

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

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

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

我們再看一下上面一篇文章的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()這種寫法都會讓計算過程非常復雜,需要大量的掃描行數,因此排序過程的資源消耗也會很大。

再回到我們文章開頭的問題,怎么正確地隨機排序呢?

隨機排序方法

我們先把問題簡化一下,如果只隨機選擇1個word值,可以怎么做呢?思路上是這樣的:

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

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

  3. 取不小于X的第一個ID的行。

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

mysql> select max(id),min(id) into @M,@N from t ;
set @X= floor((@M-@N+1)*rand() + @N);
select * from t where id >= @X limit 1; 

這個方法效率很高,因為取max(id)和min(id)都是不需要掃描索引的,而第三步的select也可以用索引快速定位,可以認為就只掃描了3行。但實際上,這個算法本身并不嚴格滿足題目的隨機要求,因為ID中間可能有空洞,因此選擇不同行的概率不一樣,不是真正的隨機。

比如你有4個id,分別是1、2、4、5,如果按照上面的方法,那么取到 id=4的這一行的概率是取得其他行概率的兩倍。

如果這四行的id分別是1、2、40000、40001呢?這個算法基本就能當bug來看待了。

所以,為了得到嚴格隨機的結果,你可以用下面這個流程:

  1. 取得整個表的行數,并記為C。

  2. 取得 Y = floor(C * rand())。 floor函數在這里的作用,就是取整數部分。

  3. 再用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. 根據相同的隨機方法得到Y1、Y2、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; //在應用代碼里面取Y1、Y2、Y3值,拼出SQL后執行 select * from t limit @Y2,1; select * from t limit @Y3,1; 

小結

今天這篇文章,我是借著隨機排序的需求,跟你介紹了MySQL對臨時表排序的執行過程。

如果你直接使用order by rand(),這個語句需要Using temporary 和 Using filesort,查詢的執行代價往往是比較大的。所以,在設計的時候你要量避開這種寫法。

今天的例子里面,我們不是僅僅在數據庫內部解決問題,還會讓應用代碼配合拼接SQL語句。在實際應用的過程中,比較規范的用法就是:盡量將業務邏輯寫在業務代碼中,讓數據庫只做“讀寫數據”的事情。因此,這類方法的應用還是比較廣泛的。

最后,我給你留下一個思考題吧。

上面的隨機算法3的總掃描行數是 C+(Y1+1)+(Y2+1)+(Y3+1),實際上它還是可以繼續優化,來進一步減少掃描行數的。

我的問題是,如果你是這個需求的開發人員,你會怎么做,來減少掃描行數呢?說說你的方案,并說明你的方案需要的掃描行數。

你可以把你的設計和結論寫在留言區里,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一起閱讀。

上期問題時間

我在上一篇文章最后留給你的問題是,select * from t where city in (“杭州”," 蘇州 ") order by name limit 100;這個SQL語句是否需要排序?有什么方案可以避免排序?

雖然有(city,name)聯合索引,對于單個city內部,name是遞增的。但是由于這條SQL語句不是要單獨地查一個city的值,而是同時查了"杭州"和" 蘇州 "兩個城市,因此所有滿足條件的name就不是遞增的了。也就是說,這條SQL語句需要排序。

那怎么避免排序呢?

這里,我們要用到(city,name)聯合索引的特性,把這一條語句拆成兩條語句,執行流程如下:

  1. 執行select * from t where city=“杭州” order by name limit 100; 這個語句是不需要排序的,客戶端用一個長度為100的內存數組A保存結果。

  2. 執行select * from t where city=“蘇州” order by name limit 100; 用相同的方法,假設結果被存進了內存數組B。

  3. 現在A和B是兩個有序數組,然后你可以用歸并排序的思想,得到name最小的前100值,就是我們需要的結果了。

如果把這條SQL語句里“limit 100”改成“limit 10000,100”的話,處理方式其實也差不多,即:要把上面的兩條語句改成寫:

select * from t where city="杭州" order by name limit 10100; 

 select * from t where city="蘇州" order by name limit 10100。 

這時候數據量較大,可以同時起兩個連接一行行讀結果,用歸并排序算法拿到這兩個結果集里,按順序取第10001~10100的name值,就是需要的結果了。

當然這個方案有一個明顯的損失,就是從數據庫返回給客戶端的數據量變大了。

所以,如果數據的單行比較大的話,可以考慮把這兩條SQL語句改成下面這種寫法:

select id,name from t where city="杭州" order by name limit 10100; 

select id,name from t where city="蘇州" order by name limit 10100。 

然后,再用歸并排序的方法取得按name順序第10001~10100的name、id的值,然后拿著這100個id到數據庫中去查出所有記錄。

上面這些方法,需要你根據性能需求和開發的復雜度做出權衡。

?

轉載于:https://www.cnblogs.com/gaosf/p/11142166.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/248578.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/248578.shtml
英文地址,請注明出處:http://en.pswp.cn/news/248578.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

看完這篇文章保你面試穩操勝券——React篇

? 進大廠收藏這一系列就夠了,全方位搜集總結,為大家歸納出這篇面試寶典,面試途中祝你一臂之力!,共分為四個系列 ? 本 篇 為 《 看 完 這 篇 文 章 保 你 面 試 穩 操 勝 券 》 第 五 篇 ( r

HTML的footer置于頁面最底部

vue項目中&#xff0c;使用element-ui的布局&#xff0c;仍然出現footer不固定頁面底部的情況&#xff0c;網上找到的一個管用的 方法是&#xff1a;footer高度固定絕對定位 <html><head></head><body><div class"header">header</…

logstash異常

logstash異常 123Unrecognized VM option UseParNewGCError: Could not create the Java Virtual Machine.Error: A fatal exception has occurred. Program will exit.logstash的版本6.4.1&#xff0c;修改config/jvm.options&#xff0c;注釋掉-XX:UseParNewGC這個配置即可。…

QT+VS中使用qDebbug()打印調試信息無法顯示

首先右鍵點擊項目名稱&#xff0c;找到最后一項屬性 然后依次設置為如圖所示即可 再次編譯后&#xff0c;會彈出CMD窗口&#xff0c;出現qDebug的調試信息。 轉載于:https://www.cnblogs.com/WindSun/p/10328404.html

WebAPIs移動端特效——不看你就虧大了

Web APIs 本篇學習目標: ?能夠寫出移動端觸屏事件 ?能夠寫出常見的移動端特效 ?能夠使用移動端開發插件開發移動端特效 ?能夠使用移動端開發框架開發移動端特效 ?能夠寫出 sessionStorage 數據的存儲以及獲取 ?能夠寫出 localStorage 數據的存儲以及獲取 ?能夠說出它們兩…

MVC是一種用于表示層設計的復合設計模式

它們之間的交互有以下幾種&#xff1a;1.當用戶在視圖上做任何需要調用模型的操作時&#xff0c;它的請求將被控制器截獲。2.控制器按照自身指定的策略&#xff0c;將用戶行為翻譯成模型操作&#xff0c;調用模型相應邏輯實現。3.控制器可能會在接到視圖操作時&#xff0c;指定…

Centos7.2源碼安裝redis

1、下載redis包&#xff08;此處可到官網查看&#xff0c;有相應的命令&#xff09; wget http://download.redis.io/releases/redis-5.0.3.tar.gz 2、解壓之后&#xff0c;并進行make編譯 tar xzf redis-5.0.3.tar.gz -C /usr/local/cd /usr/local/redis-5.0.3/make如果出現如…

手擼移動端輪播圖(內含源碼)

移動輪播圖 移動端輪播圖與PC段輪播圖&#xff0c;在技術選擇上是有區別的&#xff0c;因為移動端的瀏覽器版本非常好&#xff0c;對于H5和CSS3的支持非常完美&#xff0c;所以很多效果可以CSS3的方式實現&#xff0c;比如可以使用 Transorm 屬性替代原來的動畫函數 可以自動…

原創jquery插件treeTable(轉)

由于工作需要&#xff0c;要直觀的看到某個業務是由那些子業務引起的異常&#xff0c;所以我需要用樹表的方式來展現各個層次的數據。 需求&#xff1a; 1、數據層次分明&#xff1b; 2、數據讀取慢、需要動態加載孩子節點&#xff1b; 3、支持默認展開多少層。 在網上找到了很…

初探Vue3

&#x1f31c;本篇文章目錄\textcolor{green}{本篇文章目錄}本篇文章目錄 &#x1f31b; &#x1f435; 新構建工具Vite\textcolor{blue}{新構建工具Vite}新構建工具Vite &#x1f435; CompositionAPI火爆來襲\textcolor{blue}{Composition API火爆來襲}CompositionAPI火爆來…

linux執行python命令后permission denied

linux下執行python后顯示被拒絕問題定位&#xff1a; 1、檢查下要執行的文件的權限是否存在執行權限&#xff0c;否則執行chmod命令賦予權限&#xff1b; 2、若賦予權限后仍然顯示沒有權限&#xff0c;檢查下執行的python文件是否有權限&#xff0c;否則執行chmod賦予執行權限。…

mysql zip 安裝

第一步下載mysql.zip https://dev.mysql.com/downloads/mysql/5.7.html#downloads 第二步&#xff1a;解壓文件后在其目錄下&#xff0c; 新建 my.ini 注意編碼為ansi&#xff0c;新建 data 空文件夾 my.ini內容為&#xff1a; [mysql]# 設置mysql客戶端默認字符集default…

Vue3的響應式原理解析

Vue3的響應式原理解析 Vue2響應式原理回顧 // 1.對象響應化&#xff1a;遍歷每個key&#xff0c;定義getter、setter // 2.數組響應化&#xff1a;覆蓋數組原型方法&#xff0c;額外增加通知邏輯 const originalProto Array.prototype const arrayProto Object.create(orig…

react Native 環境安裝配置——圖解版一目了然

?原創不易&#xff0c;還希望各位大佬支持一下\textcolor{blue}{原創不易&#xff0c;還希望各位大佬支持一下}原創不易&#xff0c;還希望各位大佬支持一下 &#x1f525; Flutter和reactNative的區別\textcolor{green}{Flutter和react Native的區別}Flutter和reactNative的…

第七章 字典和集合[DDT書本學習 小甲魚]【2】

7.1.2 字典的各種內置方法在序列里為不存在位置賦值&#xff0c;會出現錯誤&#xff1b;而在字典不存在得位置賦值&#xff0c;會創建。工廠函數&#xff08;類型&#xff09;以前學過 str(),int(),list(),tuple()....... 1.fromkeys() 用于創建和返回一個新的字典 不是修改 2…

Installing Node.js and Express on Ubuntu

Installing Node.js and Express on Ubuntu 1. 在nodejs官網上下載Linux Binaries(已經包含了npm):2. 安裝Node.js下載后解壓&#xff0c;并在解壓的文件夾中啟動Terminal后&#xff0c;輸入命令&#xff1a; sudo cp * /usr/local/ -r再輸入命令&#xff1a; node -v …

Chrome插件我只服你——10w人都在使用的瀏覽器插件

?文章摘要導讀\textcolor{blue}{文章摘要導讀}文章摘要導讀 &#x1f525; 為什么選擇Chrome插件\textcolor{green}{為什么選擇Chrome插件}為什么選擇Chrome插件 &#x1f525; 插件具備的強大優勢\textcolor{green}{插件具備的強大優勢}插件具備的強大優勢 &#x1f525; …

H3C通過端口ID決定端口角色

轉載于:https://www.cnblogs.com/fanweisheng/p/11153312.html

特殊屬性

轉載于:https://www.cnblogs.com/mengbin0546/p/10338371.html

一款超強的手機屏幕投影工具

?文章摘要導讀\textcolor{blue}{文章摘要導讀}文章摘要導讀 &#x1f525; 前言\textcolor{green}{前言}前言 &#x1f525; 準備工作\textcolor{green}{準備工作}準備工作 &#x1f525; Scrcpy安裝\textcolor{green}{Scrcpy安裝}Scrcpy安裝 &#x1f525; 工具調試\text…