使用带有INNER JOIN的RAND()的MySQL子查询不返回任何行

时间:2016-08-16 15:59:28

标签: mysql subquery inner-join

我有一个对象列表和一个地方列表,我想随机将一个对象放在一个地方。

CREATE TABLE so_object (
    `id` INT,
    `name` TINYTEXT,
    PRIMARY KEY (`id`)
);
CREATE TABLE so_place (
    `id` INT,
    `name` TINYTEXT,
    PRIMARY KEY (`id`)
);

TRUNCATE TABLE `so_object`;
TRUNCATE TABLE `so_place`;

INSERT INTO `so_object` VALUES (1, 'banana'), (2, 'apple'), (3, 'chocolate'), (4, 'milk'), (5, 'phone');
INSERT INTO `so_place` VALUES (1, 'room'), (4, 'kitchen'), (7, 'living'), (8, 'cave');

然后,我选择对象表的最大id,并为对象分配一个随机整数。

SET @idMax := (SELECT MAX(id) FROM so_place);
SELECT @idMax;

SELECT
    FLOOR(RAND()*@idMax+1) AS id_place, id, name
FROM
    so_object
;

然后,我检查分配的整数是否存在于

SELECT
    *
FROM (
    SELECT
        FLOOR(RAND()*@idMax+1) AS id_place, id, name
    FROM
        so_object
) AS t
INNER JOIN so_place AS p
    ON p.id = t.id_place
;

so_place表中有漏洞,因此我执行INNER JOIN以确保该位置存在。我希望一个对象不会放在任何地方(即:如果它挑选的随机数在so_place洞)。我希望一个地方可以是空的,或者可以包含两个对象。

当你尝试像这两个小表时,它看起来工作正常。但是我添加的地方越多,返回的行数就越少:

INSERT INTO so_place VALUES
    (9, 'room 9'),
    (10, 'room 10'),
    (11, 'room 11'),
    (12, 'room 12'),
    (13, 'room 13'),
    (14, 'room 14'),
    (15, 'room 15'),
    (16, 'room 16'),
    (17, 'room 17'),
    (18, 'room 18'),
    (19, 'room 19'),
    (20, 'room 20'),
    (21, 'room 21'),
    (22, 'room 22'),
    (23, 'room 23'),
    (24, 'room 24'),
    (25, 'room 25'),
    (26, 'room 26'),
    (27, 'room 27'),
    (28, 'room 28'),
    (29, 'room 29'),
    (30, 'room 30'),
    (31, 'room 31'),
    (32, 'room 32'),
    (33, 'room 33'),
    (34, 'room 34'),
    (35, 'room 35'),
    (36, 'room 36'),
    (37, 'room 37'),
    (38, 'room 38'),
    (39, 'room 39'),
    (40, 'room 40'),
    (41, 'room 41'),
    (42, 'room 42'),
    (43, 'room 43'),
    (44, 'room 44'),
    (45, 'room 45'),
    (46, 'room 46'),
    (47, 'room 47'),
    (48, 'room 48'),
    (49, 'room 49'),
    (50, 'room 50'),
    (51, 'room 51'),
    (52, 'room 52'),
    (53, 'room 53'),
    (54, 'room 54'),
    (55, 'room 55'),
    (56, 'room 56'),
    (57, 'room 57'),
    (58, 'room 58'),
    (59, 'room 59');

这没有任何意义,因为so_place没有更多漏洞。事实上,我怀疑MySQL引擎首先解析位置表,然后选择随机整数,并且只有随机整数匹配才能保留行。 s id(随着更多地方的添加,它的可能性更小)。

此查询"工作正常"在MySQL 5.6.25和MySQL 5.5.24中(即:MySQL首先解析嵌套表,然后进行内部连接,只保留内部表中的行,如果它们匹配一个地方)但在MySQL 5.7.10中,它不再起作用

我不知道它是否是一个" MySQL 5.7.10错误",或者它是否是预期的SQL结果(然后,之前的版本被窃听更新的是"固定")。我不知道如何取回MySQL 5.5 / 5.6的行为,因此欢迎任何查询修复或其他含义相同的查询。

在那晚睡眠之后,EXPLAIN显示MySQL进行了中间简化:*

id  select_type table   type    rows    filtered    Extra
1   SIMPLE  so_object   ALL     5       100.00      \N
1   SIMPLE  p           ALL     55      10.00       Using where; Using join buffer (Block Nested Loop)

t表格未显示。那么如何强制MySQL来做中间表,因为这里的查询优化器过多地优化并破坏了查询结果?

更新: 根据{{​​3}},查询优化器现在不再实现子查询(生成我需要的临时表)。所以我可以通过使用SET optimizer_switch = 'derived_merge=off';停用此行为来解决此问题,但我不喜欢这样做,因为我需要在执行查询后重新激活此选项。

2 个答案:

答案 0 :(得分:1)

我会回答自己,即使这个解决方案看起来更像是黑客。至少,解释非常明确:

  

从MySQL 5.7.6开始,优化器以相同的方式处理派生表和视图引用:尽可能避免不必要的实现。 [...]在MySQL 5.7.6之前,派生表总是具体化

https://dev.mysql.com/doc/refman/5.7/en/subquery-optimization.html#derived-table-optimization

因此,查询被内部翻译为“选择所有地点,为每个对象选择随机数,并匹配地方的ID,然后为此对象保留此位置”。地点越多,匹配的机会就越少,因此“没有排,有时甚至没有”。 EXPLAIN显示非常明确:

id  select_type table   type    rows    filtered    Extra
1   SIMPLE  so_object   ALL     5       100.00      \N
1   SIMPLE  p           ALL     55      10.00       Using where; Using join buffer (Block Nested Loop)

子查询不生成临时表(未实现),而以前的版本则生成。

强制子查询实现的唯一方法(因此,只评估RAND()一次)是DISTINCT

  

阻止合并的构造与阻止视图合并的构造相同。示例是子查询中的SELECT DISTINCT或LIMIT。

所以查询现在是

SET @idMax := (SELECT MAX(id) FROM so_place);
SELECT
    *
FROM (
    SELECT DISTINCT
        FLOOR(RAND()*@idMax+1) AS id_place, id, name
    FROM
        so_object
) AS t
INNER JOIN so_place AS p
    ON p.id = t.id_place
;

它返回对象列表,其中“几乎每个”与一个地方匹配,并且地点可以匹配0,1个或更多对象。

id_place    id  name        id  name
16          1   banana      16  room 16
25          3   chocolate   25  room 25
16          4   milk        31  room 16
22          5   phone       22  room 22

答案 1 :(得分:0)

尝试使用cast as integer

时,

可能是投射问题

SELECT t.*
FROM (
    SELECT
        cast(FLOOR(RAND()*@idMax+1) as UNSIGNED)  AS rnum, u.id
    FROM underground AS u
)  t
INNER JOIN integers AS i
ON i.n = t.rnum