2009-08-07 51 views
86

我想優化我的查詢,以便查看mysql-slow.log如何優化MySQL的ORDER BY RAND()函數?

我的大部分緩慢查詢包含ORDER BY RAND()。我無法找到解決此問題的真正解決方案。 Theres在MySQLPerformanceBlog可能的解決方案,但我不認爲這是足夠的。在優化不充分(或頻繁更新,用戶管理)的表上,它不起作用,或者我需要運行兩個或多個查詢,然後才能選擇我生成的PHP生成的隨機行。

有沒有解決這個問題的方法?

一個虛擬的例子:

SELECT accomodation.ac_id, 
     accomodation.ac_status, 
     accomodation.ac_name, 
     accomodation.ac_status, 
     accomodation.ac_images 
FROM accomodation, accomodation_category 
WHERE accomodation.ac_status != 'draft' 
     AND accomodation.ac_category = accomodation_category.acat_id 
     AND accomodation_category.acat_slug != 'vendeglatohely' 
     AND ac_images != 'b:0;' 
ORDER BY 
     RAND() 
LIMIT 1 
+0

可能重複[MySQL從600K行中快速選擇10個隨機行](http://stackoverflow.com/questions/4329396/mysql-select-10-random-rows-from-600k-rows-fast ) – 2015-12-06 10:55:14

回答

62

試試這個:

SELECT * 
FROM (
     SELECT @cnt := COUNT(*) + 1, 
       @lim := 10 
     FROM t_random 
     ) vars 
STRAIGHT_JOIN 
     (
     SELECT r.*, 
       @lim := @lim - 1 
     FROM t_random r 
     WHERE (@cnt := @cnt - 1) 
       AND RAND(20090301) < @lim/@cnt 
     ) i 

這是MyISAM特別有效(因爲COUNT(*)是即時的),但即使在InnoDB10ORDER BY RAND()更有效。

這裏的主要思想是我們不排序,而是保留兩個變量並計算在當前步驟中要選擇的行的running probability

請參閱本文中我的博客更多的細節:

更新:

如果您需要選擇,但一個隨機記錄,試試這個:

SELECT aco.* 
FROM (
     SELECT minid + FLOOR((maxid - minid) * RAND()) AS randid 
     FROM (
       SELECT MAX(ac_id) AS maxid, MIN(ac_id) AS minid 
       FROM accomodation 
       ) q 
     ) q2 
JOIN accomodation aco 
ON  aco.ac_id = 
     COALESCE 
     (
     (
     SELECT accomodation.ac_id 
     FROM accomodation 
     WHERE ac_id > randid 
       AND ac_status != 'draft' 
       AND ac_images != 'b:0;' 
       AND NOT EXISTS 
       (
       SELECT NULL 
       FROM accomodation_category 
       WHERE acat_id = ac_category 
         AND acat_slug = 'vendeglatohely' 
       ) 
     ORDER BY 
       ac_id 
     LIMIT 1 
     ), 
     (
     SELECT accomodation.ac_id 
     FROM accomodation 
     WHERE ac_status != 'draft' 
       AND ac_images != 'b:0;' 
       AND NOT EXISTS 
       (
       SELECT NULL 
       FROM accomodation_category 
       WHERE acat_id = ac_category 
         AND acat_slug = 'vendeglatohely' 
       ) 
     ORDER BY 
       ac_id 
     LIMIT 1 
     ) 
     ) 

這假定您的ac_id的分佈差不多均勻。

+0

你好,Quassnoi!首先,感謝您的快速響應!也許這是我的錯,但目前還不清楚你的解決方案。我將用一個具體的例子更新我原來的帖子,如果你在這個例子中解釋你的解決方案,我會很高興。 – fabrik 2009-08-07 13:16:36

+0

有一個錯字 「JOIN accomodation aco ON aco.id =」 其中aco.id確實是aco.ac_id。 另一方面,更正後的查詢不適用於我,因爲它引發錯誤#1241 - 操作數應在第五個SELECT(第四個子選擇)中包含1列。我試圖用圓括號找到問題(如果我沒有錯),但我還找不到問題。 – fabrik 2009-08-10 12:11:31

+0

'@ fabrik':現在試試。如果您發佈表格腳本,以便在發佈前檢查它們,這將非常有幫助。 – Quassnoi 2009-08-10 12:14:46

1

以下是我會做:

SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*) 
    FROM accomodation a 
    JOIN accomodation_category c 
    ON (a.ac_category = c.acat_id) 
    WHERE a.ac_status != 'draft' 
     AND c.acat_slug != 'vendeglatohely' 
     AND a.ac_images != 'b:0;'; 

SET @sql := CONCAT(' 
    SELECT a.ac_id, 
     a.ac_status, 
     a.ac_name, 
     a.ac_status, 
     a.ac_images 
    FROM accomodation a 
    JOIN accomodation_category c 
    ON (a.ac_category = c.acat_id) 
    WHERE a.ac_status != ''draft'' 
     AND c.acat_slug != ''vendeglatohely'' 
     AND a.ac_images != ''b:0;'' 
    LIMIT ', @r, ', 1'); 

PREPARE stmt1 FROM @sql; 

EXECUTE stmt1; 
+0

另請參閱http://stackoverflow.com/questions/211329/quick-selection-of-a-random-row-from-a-large-table-in-mysql/213242#213242 – 2009-08-07 21:01:15

+0

我的表不是連續的,因爲它經常被編輯。例如當前第一個ID是121. – fabrik 2009-08-10 12:27:29

+3

上面的技術不依賴於連續的id值。它選擇1和COUNT(*)之間的一個隨機數,而不是像其他一些解決方案那樣使用1和MAX(id)。 – 2009-08-10 17:23:13

13

這取決於你需要多少隨意定。您鏈接的解決方案非常適合IMO。除非你在ID字段中有很大的差距,否則它仍然是非常隨機的。

但是,你應該能夠使用此做一個查詢(選擇單個值):

SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1 

其他的解決方案:

  • 添加名爲random到一個永久漂浮場表格並用隨機數填充。然後你可以在PHP中生成一個隨機數,然後執行"SELECT ... WHERE rnd > $random"
  • 獲取整個ID列表並將它們緩存在文本文件中。閱讀文件並從中選擇一個隨機ID。
  • 將查詢結果緩存爲HTML並保留幾小時。
+5

它只是我或這個查詢不起作用?我嘗試了幾個變化,他們都拋出「無效的使用組功能」.. – Sophivorus 2012-03-15 03:48:20

+0

你可以做一個子查詢'SELECT [fields] FROM [table] WHERE id> = FLOOR(RAND()*(SELECT MAX (FROM)[table]))LIMIT 1'但是這似乎不能正常工作,因爲它永遠不會返回最後一條記錄 – Mark 2013-02-23 22:56:43

+7

SELECT [fields] FROM [table] WHERE id> = FLOOR(1 + RAND()*( SELECT MAX(id)FROM [table]))LIMIT 1'似乎爲我做了詭計 – Mark 2013-02-23 23:07:28

0

這會給你一個單獨的子查詢,它將使用索引來獲得一個隨機ID,然後另一個查詢將觸發獲取你的連接表。

SELECT accomodation.ac_id, 
     accomodation.ac_status, 
     accomodation.ac_name, 
     accomodation.ac_status, 
     accomodation.ac_images 
FROM accomodation, accomodation_category 
WHERE accomodation.ac_status != 'draft' 
     AND accomodation.ac_category = accomodation_category.acat_id 
     AND accomodation_category.acat_slug != 'vendeglatohely' 
     AND ac_images != 'b:0;' 
AND accomodation.ac_id IS IN (
     SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1 
) 
0

您的虛擬 - 例如解決辦法是:

SELECT accomodation.ac_id, 
     accomodation.ac_status, 
     accomodation.ac_name, 
     accomodation.ac_status, 
     accomodation.ac_images 
FROM accomodation, 
     JOIN 
      accomodation_category 
      ON accomodation.ac_category = accomodation_category.acat_id 
     JOIN 
      ( 
       SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id 
      ) AS Choices 
      USING (ac_id) 
WHERE accomodation.ac_id >= Choices.ac_id 
     AND accomodation.ac_status != 'draft' 
     AND accomodation_category.acat_slug != 'vendeglatohely' 
     AND ac_images != 'b:0;' 
LIMIT 1 

想了解更多關於替代ORDER BY RAND(),你應該閱讀this article

0

我正在優化我的項目中的很多現有查詢。 Quassnoi的解決方案幫助我加快了查詢速度!但是,我發現很難將所述解決方案合併到所有查詢中,尤其是對於涉及多個大型表上的許多子查詢的複雜查詢。

所以我使用的是一個不太優化的解決方案。基本上它與Quassnoi的解決方案一樣。

SELECT accomodation.ac_id, 
     accomodation.ac_status, 
     accomodation.ac_name, 
     accomodation.ac_status, 
     accomodation.ac_images 
FROM accomodation, accomodation_category 
WHERE accomodation.ac_status != 'draft' 
     AND accomodation.ac_category = accomodation_category.acat_id 
     AND accomodation_category.acat_slug != 'vendeglatohely' 
     AND ac_images != 'b:0;' 
     AND rand() <= $size * $factor/[accomodation_table_row_count] 
LIMIT $size 

$size * $factor/[accomodation_table_row_count]作品挑選出來排隨機的概率。 rand()將生成一個隨機數。如果rand()小於或等於概率,則該行將被選中。這有效地執行隨機選擇來限制表格大小。由於有可能返回小於定義的限制數量,我們需要增加概率以確保選擇足夠的行。因此,我們將$ size乘以一個$因子(我通常設置$ factor = 2,在大多數情況下都適用)。最後我們做limit $size

現在的問題是制定出accomodation_table_row_count。 如果我們知道表的大小,我們可以硬編碼表的大小。這會跑得最快,但顯然這並不理想。如果你正在使用Myisam,獲得餐桌計數是非常有效的。自從我使用innodb以來,我只是做一個簡單的計數+選擇。在你的情況下,它看起來像這樣:

SELECT accomodation.ac_id, 
     accomodation.ac_status, 
     accomodation.ac_name, 
     accomodation.ac_status, 
     accomodation.ac_images 
FROM accomodation, accomodation_category 
WHERE accomodation.ac_status != 'draft' 
     AND accomodation.ac_category = accomodation_category.acat_id 
     AND accomodation_category.acat_slug != 'vendeglatohely' 
     AND ac_images != 'b:0;' 
     AND rand() <= $size * $factor/(select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`)) 
LIMIT $size 

棘手的部分是找出正確的概率。正如你所看到的,下面的代碼實際上只計算粗略的臨時表大小(實際上太粗糙了!):(select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))但是你可以改進這個邏輯來給出更接近的表大小近似值。 請注意,OVER-select比選擇下面的行更好。即如果概率設置得太低,則可能無法選擇足夠的行。

由於我們需要重新計算表格大小,所以此解決方案運行速度比Quassnoi的解決方案要慢。但是,我發現這種編碼更易於管理。這是在精度+性能編碼複雜度之間的折衷。儘管如此,在大型表格上,這仍然比Rand()的Order快得多。

注意:如果查詢邏輯允許,請在任何聯接操作之前儘早執行隨機選擇。

0

(是的,我會因爲沒有足夠的肉在這裏受到叮咬,但是你不能成爲一天中的素食主義者嗎?)

案例:無間隙連續AUTO_INCREMENT,1行返回
案例:無間隙連續AUTO_INCREMENT,10行
案例:AUTO_INCREMENT具有間隙,1行返回
案例:用於隨機
案例額外FLOAT柱: UUID或MD5列

這5種情況對於大型表格可以非常有效。有關詳細信息,請參閱my blog

-1
function getRandomRow(){ 
    $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT); 
    $res = getRowById($id); 
    if(!empty($res)) 
    return $res; 
    return getRandomRow(); 
} 

//rowid is a key on table 
function getRowById($rowid=false){ 

    return db select from table where rowid = $rowid; 
}