MySQLのORDER BY RAND()関数を最適化するにはどうすればよいですか?


90

クエリを最適化して、を調べますmysql-slow.log

遅いクエリのほとんどにが含まれていますORDER BY RAND()。この問題を解決するための実際の解決策が見つかりません。MySQLPerformanceBlogで可能な解決策がありますが、これで十分だとは思いません。最適化が不十分な(または頻繁に更新されるユーザー管理の)テーブルでは機能しないか、-でPHP生成されたランダムな行を選択する前に2つ以上のクエリを実行する必要があります。

この問題の解決策はありますか?

ダミーの例:

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

回答:


67

これを試して:

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

これはMyISAMCOUNT(*)がインスタントであるため)特に効率的ですが、のInnoDB場合10よりも効率的ですORDER BY RAND()

ここでの主なアイデアは、並べ替えではなく、2つの変数を保持running probabilityして、現在のステップで選択される行のを計算することです。

詳細については、私のブログのこの記事を参照してください。

更新:

ランダムなレコードを1つだけ選択する必要がある場合は、次のことを試してください。

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がほぼ均等に分散されていることを前提としています。


こんにちは、Quassnoi!まず、迅速な対応に感謝します!多分それは私のせいですが、それでもあなたの解決策は不明です。元の投稿を具体的な例で更新します。この例の解決策を説明していただければ幸いです。
fabrik 2009

"JOIN accomodation aco ON aco.id ="に誤植がありました。aco.idは本当にaco.ac_idです。一方、エラー#1241-オペランドの5番目のSELECT(4番目のサブ選択)には1つの列が含まれているため、修正されたクエリは機能しませんでした。括弧で問題を見つけようとしましたが(間違っていない場合)、まだ問題を見つけることができません。
fabrik 2009

@fabrik: 今すぐやってみて下さい。テーブルスクリプトを投稿して、投稿する前に確認できるようにすると、非常に役立ちます。
Quassnoi、2009

ありがとう、うまくいきました!:) JOIN ... ON aco.idパーツをJOIN ... ON aco.ac_idに編集して、ソリューションを受け入れることができますか?再度、感謝します!質問:可能であれば、これはORDER BY RAND()のようなより悪いランダムでしょうか?このクエリがいくつかの結果を何度も繰り返すためです。
fabrik、2009

1
@アダム:いいえ、それは意図的なものであり、結果を再現できるようにします。
Quassnoi、2011

12

それはあなたがどれくらいランダムである必要があるかに依存します。リンクしたソリューションはIMOでかなりうまく機能します。IDフィールドに大きなギャップがない限り、それはかなりランダムです。

ただし、これを使用して1つのクエリでそれを行うことができるはずです(単一の値を選択するため):

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

その他の解決策:

  • randomテーブルに呼び出される永続的なfloatフィールドを追加し、乱数を入力します。次に、PHPで乱数を生成し、"SELECT ... WHERE rnd > $random"
  • IDのリスト全体を取得して、テキストファイルにキャッシュします。ファイルを読み取り、ランダムなIDを選択します。
  • クエリの結果をHTMLとしてキャッシュし、数時間保持します。

8
それは私だけですか、このクエリは機能しませんか?私はいくつかのバリエーションでそれを試してみましたが、それらはすべて投げ、「グループ機能の使用が無効です。」..
Sophivorus

サブクエリを使用してそれを行うことができSELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1ますが、最後のレコードを返さないため、これは適切に機能していないようです
Mark

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1私のためにトリックをしているようです
マーク

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;


テーブルは頻繁に編集されるため、連続的ではありません。例えば、現在、最初のIDが121である
ファブリク

3
上記の手法は、id値が連続的であることに依存していません。他のいくつかのソリューションのように1とMAX(id)ではなく、1とCOUNT(*)の間の乱数を選択します。
ビルカーウィン

1
使用してOFFSET(何である@rアップフルテーブルスキャンに-スキャンを回避していないためです)。
リックジェームズ

@RickJames、そうだね。今日この質問に答えるなら、主キーでクエリを実行します。LIMITでオフセットを使用すると、多くの行がスキャンされます。主キーによるクエリは、はるかに高速ですが、各行を選択する可能性を保証するものではありません。ギャップに続く行が優先されます。
ビルカーウィン2015

1

(ええ、私はここに肉が足りないのでうんざりしますが、あなたは一日ビーガンになれませんか?)

ケース:ギャップのない連続AUTO_INCREMENT、1行が返される
ケース:ギャップのない連続AUTO_INCREMENT、10行
ケース:ギャップのあるAUTO_INCREMENT、1行が返される
ケース:ランダム化のための追加のFLOAT列
ケース:UUIDまたはMD5列

これらの5つのケースは、大きなテーブルに対して非常に効率的にすることができます。詳細はブログをご覧ください。


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()この記事をお読みください


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を掛けます(通常、$ 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))ただし、このロジックを調整して、テーブルサイズの近似値を近似できます。行を選択するよりも、選択するのが良いことに注意してください。つまり、確率の設定が低すぎると、十分な行が選択されないおそれがあります。

テーブルサイズを再計算する必要があるため、このソリューションはQuassnoiのソリューションよりも実行速度が遅くなります。ただし、このコーディングの方がはるかに扱いやすいと思います。これは、精度+パフォーマンスコーディングの複雑さの間のトレードオフです。そうは言っても、大きなテーブルでは、これはOrder by Rand()よりもはるかに高速です。

注:クエリロジックが許可する場合は、結合操作の前にできるだけ早くランダム選択を実行してください。


-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; 
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.