日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

mysql实战17 | 如何正确地显示随机消息?

發布時間:2024/4/17 数据库 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 mysql实战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() begindeclare i int;set i=0;while i<10000 doinsert 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(); 復制代碼


為了便于量化說明,我在這個表里面插入了 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 排序。

理解了這個算法選擇的邏輯,我們再來看看語句的執行流程。同時,通過今天的這個例子,我們來嘗試分析一下語句的掃描行數。

這條語句的執行流程是這樣的:

  • 創建一個臨時表。這個臨時表使用的是 memory 引擎,表里有兩個字段,第一個字段是 double 類型,為了后面描述方便,記為字段 R,第二個字段是 varchar(64) 類型,記為字段 W。并且,這個表沒有建索引。
  • 從 words 表中,按主鍵順序取出所有的 word 值。對于每一個 word 值,調用 rand() 函數生成一個大于 0 小于 1 的隨機小數,并把這個隨機小數和 word 分別存入臨時表的 R 和 W 字段中,到此,掃描行數是 10000。
  • 現在臨時表有 10000 行數據了,接下來你要在這個沒有索引的內存臨時表上,按照字段 R 排序。
  • 初始化 sort_buffer。sort_buffer 中有兩個字段,一個是 double 類型,另一個是整型。
  • 從內存臨時表中一行一行地取出 R 值和位置信息(我后面會和你解釋這里為什么是“位置信息”),分別存入 sort_buffer 中的兩個字段里。這個過程要對內存臨時表做全表掃描,此時掃描行數增加 10000,變成了 20000。
  • 在 sort_buffer 中根據 R 的值進行排序。注意,這個過程沒有涉及到表操作,所以不會增加掃描行數。
  • 排序完成后,取出前三個結果的位置信息,依次到內存臨時表中取出 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 行也是有序的了。但,我們的查詢并不需要這些數據是有序的。所以,想一下就明白了,這浪費了非常多的計算量。

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

  • 對于這 10000 個準備排序的 (R,rowid),先取前三行,構造成一個堆;(對數據結構印象模糊的同學,可以先設想成這是一個由三個元素組成的數組)
  • 取下一個行 (R’,rowid’),跟當前堆里面最大的 R 比較,如果 R’小于 R,把這個 (R,rowid) 從堆中去掉,換成 (R’,rowid’);
  • 重復第 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 值,可以怎么做呢?思路上是這樣的:

  • 取得這個表的主鍵 id 的最大值 M 和最小值 N;
  • 用隨機函數生成一個最大值到最小值之間的數 X = (M-N)*rand() + N;
  • 取不小于 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 來看待了。

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

    • 取得整個表的行數,并記為 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 值呢?你可以這么做:

  • 取得整個表的行數,記為 C;
  • 根據相同的隨機方法得到 Y1、Y2、Y3;
  • 再執行三個 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) 聯合索引的特性,把這一條語句拆成兩條語句,執行流程如下:

  • 執行 select * from t where city=“杭州” order by name limit 100; 這個語句是不需要排序的,客戶端用一個長度為 100 的內存數組 A 保存結果。
  • 執行 select * from t where city=“蘇州” order by name limit 100; 用相同的方法,假設結果被存進了內存數組 B。
  • 現在 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://juejin.im/post/5d035ea8f265da1b94214631

    總結

    以上是生活随笔為你收集整理的mysql实战17 | 如何正确地显示随机消息?的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。