如何从 30k MySQL 表中快速选择 3 条随机记录,其中按单个查询进行过滤


How to quickly SELECT 3 random records from a 30k MySQL table with a where filter by a single query?

嗯,这是一个非常古老的问题,从来没有得到真正的解决方案。我们希望从包含大约 30k 条记录的表中随机 3 行。从MySQL的角度来看,该表并不大,但如果它代表商店的产品,那么它具有代表性。例如,当一个人在网页中展示 3 个随机产品时,随机选择很有用。我们需要一个满足以下条件的 SQL 字符串解决方案:

  1. 在 PHP 中,PDO或MySQLi的记录集必须正好有3行。
  2. 它们必须由不使用存储过程的单个 MySQL 查询获取。
  3. 解决方案必须快速,例如繁忙的apache2服务器,MySQL查询在许多情况下是瓶颈。因此,它必须避免临时创建表等。
  4. 这 3 个记录必须不连续,即它们不得彼此相邻。

该表包含以下字段:

CREATE TABLE Products (
  ID INT(8) NOT NULL AUTO_INCREMENT,
  Name VARCHAR(255) default NULL,
  HasImages INT default 0,
  ...
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

WHERE 约束是 Products.HasImages=1,允许仅获取具有可在网页上显示的图像的记录。大约三分之一的记录满足 HasImages=1 的条件。

为了寻求完美,我们首先抛开现有的有缺点的解决方案:

<小时 />

我。这个基本解决方案使用 ORDER BY RAND(),

太慢了,但保证每个查询有 3 条真正随机的记录:

SELECT ID, Name FROM Products WHERE HasImages=1 ORDER BY RAND() LIMIT 3;

*CPU约0.10s,扫描9690行因为WHERE子句,使用where;使用临时的;使用文件排序,在Debian Squeeze双核Linux盒子上,还不错,但是

不像使用临时表和文件排序那样可扩展到更大的表,并且在测试 Windows7::MySQL 系统上的第一个查询需要我 8.52 秒。在如此糟糕的性能下,避免网页不是吗?

<小时 />

二、使用JOIN的里德西奥的明亮解决方案...兰德(),

从 MySQL 中快速从 600K 行中选择 10 个随机行,此处改编仅对单个随机记录有效,因为以下查询几乎总是连续的记录。实际上,它只在 ID 中随机获取一组 3 条连续记录:

SELECT Products.ID, Products.Name
FROM Products
INNER JOIN (SELECT (RAND() * (SELECT MAX(ID) FROM Products)) AS ID)
  AS t ON Products.ID >= t.ID
WHERE (Products.HasImages=1)
ORDER BY Products.ID ASC
LIMIT 3;

*CPU大约0.01 - 0.19s,随机扫描3200,9690,12000行左右,但大多是9690条记录,使用在哪里。

<小时 />

三、最佳解决方案如下,其中...兰德(),

在MySQL上看到,从Bernardo-siu提出的600K行中随机选择10行:

SELECT Products.ID, Products.Name FROM Products
WHERE ((Products.Hasimages=1) AND RAND() < 16 * 3/30000) LIMIT 3;

*CPU 约 0.01 - 0.03 秒,扫描 9690 行,使用位置。

这里 3 是希望行数,30000 是表 Product 的 RecordCount,16 是放大选择以保证 3 条记录选择的实验系数。我不知道因子 16 在什么基础上是可接受的近似值。

在大多数情况下,我们会得到 3 条随机记录,它非常快,但这是不保证的:有时查询只返回 2 行,有时甚至根本不返回记录。

以上三种方法扫描符合 WHERE 子句的表的所有记录,此处为 9690 行。

更好的 SQL 字符串?

丑陋,但快速和随机。可能会很快变得非常丑陋,尤其是在下面描述的调整中,所以请确保您真的想要这种方式。

(SELECT Products.ID, Products.Name
FROM Products
    INNER JOIN (SELECT RAND()*(SELECT MAX(ID) FROM Products) AS ID) AS t ON Products.ID >= t.ID
WHERE Products.HasImages=1
ORDER BY Products.ID
LIMIT 1)
UNION ALL
(SELECT Products.ID, Products.Name
FROM Products
    INNER JOIN (SELECT RAND()*(SELECT MAX(ID) FROM Products) AS ID) AS t ON Products.ID >= t.ID
WHERE Products.HasImages=1
ORDER BY Products.ID
LIMIT 1)
UNION ALL
(SELECT Products.ID, Products.Name
FROM Products
    INNER JOIN (SELECT RAND()*(SELECT MAX(ID) FROM Products) AS ID) AS t ON Products.ID >= t.ID
WHERE Products.HasImages=1
ORDER BY Products.ID
LIMIT 1)

第一行出现的频率超过应有的频率

如果表中的 ID 之间存在较大间隙,则紧跟在此类间隙之后的行将有更大的机会被此查询获取。在某些情况下,它们会出现的频率会比应有的高得多。这通常无法解决,但有一个常见特定情况的修复:当 0 和表中的第一个现有 ID 之间存在间隙时。

不要使用子查询(SELECT RAND()*<max_id> AS ID)使用类似 (SELECT <min_id> + RAND()*(<max_id> - <min_id>) AS ID)

删除重复项

如果按原样使用查询,则可能会返回重复的行。可以通过使用 UNION 而不是 UNION ALL 来避免这种情况。这样,重复项将被合并,但查询不再保证只返回 3 行。您也可以通过获取比您需要的更多的行并限制外部结果来解决此问题,如下所示:

(SELECT ... LIMIT 1)
UNION (SELECT ... LIMIT 1)
UNION (SELECT ... LIMIT 1)
...
UNION (SELECT ... LIMIT 1)
LIMIT 3

但是,仍然不能保证会获取 3 行。它只是使它更有可能。

SELECT Products.ID, Products.Name
FROM Products
INNER JOIN (SELECT (RAND() * (SELECT MAX(ID) FROM Products)) AS ID) AS t ON Products.ID     >= t.ID
WHERE (Products.HasImages=1)
ORDER BY Products.ID ASC
LIMIT 3;

当然,上面是给出"近"连续记录的,你每次都给它提供相同的ID,而没有太多考虑rand函数的seed

这应该提供更多的"随机性"

SELECT Products.ID, Products.Name
FROM Products
INNER JOIN (SELECT (ROUND((RAND() * (max-min))+min)) AS ID) AS t ON Products.ID     >= t.ID
WHERE (Products.HasImages=1)
ORDER BY Products.ID ASC
LIMIT 3;

其中maxmin是您选择的两个值,例如:

max = select max(id)
min = 225

此语句执行速度非常快(在 30k 记录表上为 19 毫秒):

$db = new PDO('mysql:host=localhost;dbname=database;charset=utf8', 'username', 'password');
$stmt = $db->query("SELECT p.ID, p.Name, p.HasImages
                    FROM (SELECT @count := COUNT(*) + 1, @limit := 3 FROM Products WHERE HasImages = 1) vars
                    STRAIGHT_JOIN (SELECT t.*, @limit := @limit - 1 FROM Products t WHERE t.HasImages = 1 AND (@count := @count -1) AND RAND() < @limit / @count) p");
$products = $stmt->fetchAll(PDO::FETCH_ASSOC);

这个想法是用随机值"注入"一个新列,然后按此列排序。这个注入的列的生成和排序比"ORDER BY RAND()"命令快得多。

"可能"有一个警告:您必须包含两次 WHERE 查询。

创建另一个仅包含带有图像的项目的表怎么样?此表将轻得多,因为它仅包含原始表的三分之一的项目!

------------------------------------------
|ID     | Item ID (on the original table)|
------------------------------------------
|0      | 0                              |
------------------------------------------
|1      | 123                            |
------------------------------------------
            .
            .
            .
------------------------------------------
|10 000 | 30 000                         |
------------------------------------------

然后,您可以在代码的PHP部分生成三个随机ID,然后从数据库中获取它们。

我一直在一个 10M 记录、设计不佳的数据库上测试以下一堆 SQL。

SELECT COUNT(ID)
INTO @count
FROM Products
WHERE HasImages = 1;
PREPARE random_records FROM
'(
    SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1
) UNION (
    SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1
) UNION (
    SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1
)';
SET @l1 = ROUND(RAND() * @count);
SET @l2 = ROUND(RAND() * @count);
SET @l3 = ROUND(RAND() * @count);
EXECUTE random_records USING @l1
    , @l2
    , @l3;
DEALLOCATE PREPARE random_records;

花了将近 7 分钟才得到三个结果。但我相信它的性能会在你的情况下好得多。然而,如果您正在寻找更好的性能,我建议您使用以下性能,因为它们花了不到 30 秒的时间来完成工作(在同一个数据库上)。

SELECT COUNT(ID)
INTO @count
FROM Products
WHERE HasImages = 1;
PREPARE random_records FROM
'SELECT * FROM Products WHERE HasImages = 1 LIMIT ?, 1';
SET @l1 = ROUND(RAND() * @count);
SET @l2 = ROUND(RAND() * @count);
SET @l3 = ROUND(RAND() * @count);
EXECUTE random_records USING @l1;
EXECUTE random_records USING @l2;
EXECUTE random_records USING @l3;
DEALLOCATE PREPARE random_records;
请记住,如果你想

一次性执行它们,这两个命令都需要 PHP 中的 MySQLi 驱动程序。它们唯一的区别是后一个需要调用MySQLi的next_result方法来检索所有三个结果。

我个人认为这是最快的方法。

如果你愿意接受"开箱即用"类型的答案,我将重复我在一些评论中所说的。

解决问题的最佳方法是提前缓存数据(无论是在外部 JSON 或 XML 文件中,还是在单独的数据库表中,甚至可能是内存表中)。

通过这种方式,您可以将产品表上的性能命中安排到您知道服务器安静的时间,并减少您在访问者到达您的网站时"随机"时间创建性能命中的担忧。

我不会建议一个明确的解决方案,因为关于如何构建解决方案的可能性太多了。 然而,@ahmed提出的答案并不愚蠢。 如果不想在查询中创建联接,则只需将所需的更多数据加载到新表中即可。