【问题标题】:如何优化 MySQL 的 ORDER BY RAND() 函数?
【发布时间】:2022-01-23 06:31:35
【问题描述】:

我想优化我的查询,所以我研究了mysql-slow.log

我的大部分慢查询都包含ORDER BY RAND()。我找不到真正的解决方案来解决这个问题。 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

【问题讨论】:

标签: mysql random performance


【解决方案1】:

我的建议是添加具有 UUID(版本 4)或其他随机值、唯一索引(或仅主键)的列。

然后您可以在查询时简单地生成一个随机值,并选择大于生成值的行,按随机列排序。

确保如果收到的行数少于预期,则重复查询而不使用大于子句(在结果集的“开始”处选择行)。

uuid = generateUUIDV4()

select * from foo
where uuid > :uuid
order by uuid
limit 42

if count(results) < 42 {
  select * from foo
  order by uuid
  limit :remainingResultsRequired
}

【讨论】:

    【解决方案2】:
    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; 
    }
    

    【讨论】:

      【解决方案3】:

      (是的,我会因为这里没有足够的肉而被骂,但你不能成为一天的素食主义者吗?)

      案例:连续 AUTO_INCREMENT 无间隙,返回 1 行
      案例:连续 AUTO_INCREMENT 无间隙,10 行
      案例:AUTO_INCREMENT 有间隙,返回 1 行
      案例:用于随机化的额外 FLOAT 列
      案例:UUID 或 MD5 列

      这 5 种情况对于大型表来说非常有效。详情请见my blog

      【讨论】:

        【解决方案4】:

        我正在优化我的项目中的许多现有查询。 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() 快得多。

        注意:如果查询逻辑允许,请在任何连接操作之前尽早执行随机选择。

        【讨论】:

          【解决方案5】:

          您的虚拟示例的解决方案是:

          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

          【讨论】:

            【解决方案6】:

            这将为您提供单个子查询,该查询将使用索引获取随机 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
            )
            

            【讨论】:

              【解决方案7】:

              试试这个:

              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 上,它的效率也比10 高出ORDER 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 分布或多或少是均匀的。

              【讨论】:

              • 你好,夸斯诺伊!首先,感谢您的快速回复!也许这是我的错,但仍然不清楚你的解决方案。我会用一个具体的例子更新我原来的帖子,如果你在这个例子中解释你的解决方案,我会很高兴。
              • 在“JOIN accomodation aco ON aco.id =”处有错字,其中 aco.id 确实是 aco.ac_id。另一方面,更正后的查询对我不起作用,因为它会引发错误 #1241 - 操作数应在第五个 SELECT(第四个子选择)处包含 1 列。我试图用括号找到问题(如果我没记错的话),但我还找不到问题。
              • @fabrik :现在试试。如果您发布表格脚本以便我可以在发布之前检查它们,那将非常有帮助。
              • 谢谢,它有效! :) 您能否将 JOIN ... ON aco.id 部分编辑为 JOIN ... ON aco.ac_id 以便我可以接受您的解决方案。再次感谢!一个问题:我想知道这是否可能是像 ORDER BY RAND() 这样更糟糕的随机?只是因为这个查询重复了一些结果很多次。
              • @Adam:不,这是故意的,以便您可以重现结果。
              【解决方案8】:

              这取决于您需要的随机性。您链接的解决方案非常适用于 IMO。除非您在 ID 字段中有很大的空白,否则它仍然是非常随机的。

              但是,您应该能够在一个查询中使用它(用于选择单个值):

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

              其他解决方案:

              • 将名为random 的永久浮点字段添加到表中并用随机数填充它。然后您可以在 PHP 中生成一个随机数并执行"SELECT ... WHERE rnd &gt; $random"
              • 获取整个 ID 列表并将它们缓存在文本文件中。读取文件并从中选择一个随机 ID。
              • 将查询结果缓存为 HTML 并保留几个小时。

              【讨论】:

              • 只有我还是这个查询不起作用?我尝试了几种变体,它们都抛出“无效使用组函数”..
              • 您可以使用子查询 SELECT [fields] FROM [table] WHERE id &gt;= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1 来实现,但这似乎无法正常工作,因为它从不返回最后一条记录
              • SELECT [fields] FROM [table] WHERE id &gt;= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1 似乎对我有用
              【解决方案9】:

              我会这样做:

              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。
              • 上述技术不依赖于连续的 id 值。它在 1 和 COUNT(*) 之间选择一个随机数,而不是像其他一些解决方案那样选择 1 和 MAX(id)。
              • 使用OFFSET(这是@r 的用途)并不能避免扫描——最多是全表扫描。
              • @RickJames,没错。如果我今天要回答这个问题,我会按主键进行查询。使用带有 LIMIT 的偏移量确实会扫描很多行。按主键查询虽然快得多,但并不能保证选择每一行的机会均等——它有利于跟随间隙的行。
              猜你喜欢
              • 1970-01-01
              • 2012-06-12
              • 2016-07-22
              • 1970-01-01
              • 1970-01-01
              • 2012-03-14
              • 2016-12-27
              • 1970-01-01
              相关资源
              最近更新 更多