繁体   English   中英

如何通过单个查询快速从一个30k的MySQL表中选择3个随机记录?

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

嗯,这是一个非常古老的问题,从未得到真正的解决方案。 我们想要一个表中有3个随机行,大约有30k记录。 从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的条件。

寻求完美,我们首先抛开存在缺陷的现有解决方案:


I.使用ORDER BY RAND()的这个基本解决方案

太慢但在每个查询中保证3个真正随机的记录:

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

* CPU约0.10s,因WHERE子句扫描9690行,使用where; 使用临时; 在Debian Squeeze双核Linux机器上使用filesort ,并不是那么糟糕

因为使用临时表和filesort而不能扩展到更大的表,并且在测试Windows7 :: MySQL系统上的第一个查询需要8.52秒。 如此糟糕的表现,避免网页不是吗?


II。 使用JOIN ... RAND()的riedsio的明亮解决方案,

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条记录,使用位置。


III。 最好的解决方案似乎如下WHERE ... RAND(),

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.03s,扫描9690行,使用位置。

这里3是所希望的行数,30000是表Products的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 ms):

$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查询两次。

我一直在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方法来检索所有三个结果。

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

如何创建另一个只包含带有图像的项目的表? 这个表格要轻得多,因为它只包含原始表格中三分之一的项目!

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

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

如果您愿意接受“开箱即用”类型的答案,那么我将重复我在一些评论中所说的内容。

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM