19

好吧,这是一个非常古老的问题,从未得到真正的解决方案。我们想要一个包含大约 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 的条件。

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


I.这个使用 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 机器上使用 filesort,还不错,但是

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


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

MySQL select 10 random rows from 600K rows fast,此处改编仅对单个随机记录有效,因为以下查询导致几乎总是连续的记录。实际上,它仅在 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 条记录,使用 where。


三、最好的解决方案似乎是 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 行,使用 where。

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

在大多数情况下,我们得到了 3 条随机记录,而且速度非常快,但没有保证:有时查询只返回 2 行,有时甚至根本没有记录。

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

更好的 SQL 字符串?

4

6 回答 6

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)

第一行出现的频率高于应有的频率

如果表中的 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 行。它只会让它更有可能。

于 2013-09-22T15:52:32.750 回答
2
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每次都喂它一样,而不考虑函数 seed的。rand

这应该给更多的“随机性”

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
于 2013-09-22T13:23:07.907 回答
1

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

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

于 2013-09-22T14:47:48.543 回答
1

如何创建另一个只包含带有图像的项目的表?这张桌子会更轻,因为它只包含原始桌子的三分之一!

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

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

于 2013-09-22T20:14:58.190 回答
1

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

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

这样,您可以将您对产品表的性能影响安排在您知道服务器将处于安静状态的时间,并减少您对在访问者到达您的站点时“随机”时间造成性能影响的担忧。

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

于 2013-09-22T22:42:00.007 回答
1

该语句执行得非常快(在 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 查询。

于 2013-09-22T12:40:25.050 回答