我如何优化MySQL的ORDER BY RAND()函数?


90

我想优化查询,以便进行调查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

Answers:


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

这在 MyISAM(由于COUNT(*)是即时的),,但即使这样,InnoDB10效率也比更高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的或多或少均匀分布。


你好,夸斯诺伊!首先,感谢您的快速回复!也许这是我的错,但仍不清楚您的解决方案。我将用一个具体的示例更新我的原始帖子,如果您在此示例中解释您的解决方案,我将很高兴。
fabrik

在“ JOINaccomodation aco ON aco.id =“上有一个错字,其中aco.id实际上是aco.ac_id。另一方面,更正后的查询对我不起作用,因为它引发了错误#1241-操作数在第五个SELECT(第四个子选择)处应包含1列。我试图用括号找到问题(如果我没记错的话),但是我还没有找到问题。
fabrik

@fabrik: 现在试试。如果您发布了表脚本,以便在发布之前可以对其进行检查,那将非常有帮助。
Quassnoi

谢谢,它有效!:)您可以将JOIN ... ON aco.id部分编辑为JOIN ... ON aco.ac_id,以便我接受您的解决方案。再次感谢!一个问题:我想知道是否可能是像ORDER BY RAND()这样的更差的随机数?只是因为此查询多次重复某些结果。
fabrik

1
@Adam:不,这是故意的,以便您可以重现结果。
Quassnoi

12

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

但是,您应该可以使用以下命令在一个查询中执行此操作(用于选择单个值):

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

其他解决方案:

  • 添加一个名为 random在表格中,并在其中填充随机数。然后,您可以在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,但是这似乎并没有正常工作,因为它永远不会返回的最后一个记录
马克

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1似乎在为我做花招
Mark

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
FABRIK

3
上面的技术不依赖于id值是连续的。像其他解决方案一样,它选择1到COUNT(*)之间的随机数,而不是1到MAX(id)。
比尔·卡文

1
使用OFFSET(这是@r为了实现)不能避免进行扫描-直到进行全表扫描。
里克·詹姆斯

@RickJames,是的。如果今天要回答这个问题,我将按主键进行查询。与LIMIT一起使用偏移量会扫描很多行。通过主键进行查询虽然速度更快,但并不能保证选择每行的机会均一,它偏向于跟随空格的行。
比尔·卡文

1

(是的,我会因为这里没有足够的肉而感到沮丧,但是你不能有一天做素食主义者吗?)

案例:连续无间隙的AUTO_INCREMENT,返回1行
案例:连续无间隙的AUTO_INCREMENT,10行
案例:有间隙,连续AUTO_INCREMENT ,返回1行
案例:用于随机化的Extra 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,所以我只是在做一个简单的count + select。在您的情况下,它看起来像这样:

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的解决方案慢,因为我们需要重新计算表的大小。但是,我发现此编码更易于管理。这是精度+性能编码复杂度之间的权衡。话虽如此,在大型表上,这仍然比Rand()的Order快得多。

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


-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; 
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.