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

jac*_*ouh 19 php mysql random performance

嗯,这是一个非常古老的问题,从未得到真正的解决方案.我们想要一个表中有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;
Run Code Online (Sandbox Code Playgroud)

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

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


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

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

SELECT ID, Name FROM Products WHERE HasImages=1 ORDER BY RAND() LIMIT 3;
Run Code Online (Sandbox Code Playgroud)

*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;
Run Code Online (Sandbox Code Playgroud)

*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;
Run Code Online (Sandbox Code Playgroud)

*CPU约0.01 - 0.03s,扫描9690行,使用位置.

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

我们在大多数情况下得到3个随机记录并且它非常快,但是没有保证:有时查询只返回2行,有时甚至根本没有记录.

上述三种方法扫描满足WHERE子句的表的所有记录,这里是9690行.

一个更好的SQL字符串?

Leo*_*Nyx 5

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

(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)
Run Code Online (Sandbox Code Playgroud)

第一行出现的频率比它应该的要高

如果您的表中的 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
Run Code Online (Sandbox Code Playgroud)

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

  • 该解决方案确保了一个随机行的 3 次。缺点是三个 ID 在数学上可以相同,例如 10088、10088、10088。但这在现实世界中可能永远不会发生。它占用 CPU 0.01 到 0.20 秒。 (2认同)