web-dev-qa-db-ja.com

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

クエリを最適化したいので、_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
_
88
fabrik

これを試して:

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(*)がインスタントであるため)が、InnoDBでもORDER BY Rand()10倍の効率です。

ここでの主なアイデアは、ソートせず、代わりに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がほぼ均等に配布されることを前提としています。

67
Quassnoi

それは、あなたがどれほどランダムである必要があるかに依存します。リンクしたソリューションは、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としてキャッシュし、数時間保持します。
13
DisgruntledGoat

これが私がそれをする方法です:

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;
1
Bill Karwin

これにより、インデックスを使用してランダム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
Karl Mikko

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

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

これらの5つのケースは、大きなテーブルに対して非常に効率的にできます。詳細については my blog を参照してください。

0
Rick James

ダミーの例の解決策は次のとおりです。

_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
tereško

私は自分のプロジェクトで多くの既存のクエリを最適化しています。 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を使用しているため、単純なcount + selectionを実行しています。あなたの場合、次のようになります。

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()よりもはるかに高速です。

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

0
lawrenceshen