【问题标题】:Left-joining the same table multiple times多次左连接同一个表
【发布时间】:2011-12-19 16:41:18
【问题描述】:

假设我有一个可以由 2、3 或 4 名玩家玩的游戏。我在我的数据库(MySQL 5.1)中的三个表中跟踪这样的游戏,如下所示。我希望这些字段是不言自明的:

create table users (id int, login char(8));
create table games (id int, stime datetime, etime datetime);
create table users_games (uid int, gid int, score int);

[比赛表中跟踪的两个时间是开始和结束时间]

这里有一些用于填充表格的虚拟数据:

insert into games values
(1, '2011-12-01 10:00:00', '2011-12-01 13:00:00'),
(2, '2011-12-02 11:00:00', '2011-12-01 14:00:00'),
(3, '2011-12-03 12:00:00', '2011-12-01 15:00:00'),
(4, '2011-12-04 13:00:00', '2011-12-01 16:00:00');

insert into users_games values
(101, 1, 10),
(102, 1, 11),
(101, 2, 12),
(103, 2, 13),
(104, 2, 14),
(102, 3, 15),
(103, 3, 16),
(104, 3, 17),
(105, 3, 18),
(102, 4, 19),
(104, 4, 20),
(105, 4, 21);

现在,我需要生成以下格式的报告:

gid     p1    p2    p3    p4  started ended
1      101   102               [g1]    [g1]
2      101   103   104         [g2]    [g2]
3      102   103   104   105   [g3]    [g3]
4      102   104   105         [g4]    [g4]

也就是说,一份报告显示了在同一行中玩过游戏的所有玩家。我还需要他们的分数和用户表中的一些其他信息,但那是第 2 阶段。:-)

我是从这个开始的:

select g.id, g.stime, g.etime, ug1.uid, ug2.uid, ug3.uid, ug4.uid
from games g, users_games ug1, users_games ug2, users_games ug3, users_games ug4
where
g.id = ug1.gid and
ug1.gid = ug2.gid and
ug1.uid < ug2.uid and
ug2.gid = ug3.gid and
ug2.uid < ug3.uid and
ug3.gid = ug4.gid and
ug3.uid < ug4.uid

这给了我所有四个座位都被占用的游戏(即上述虚拟数据中只有游戏 ID 3)。但这只是我需要的数据的一个子集。

这是我的第二次尝试:

select g.id, g.stime, g.etime, ug1.uid, ug2.uid,
    ifnull(ug3.uid, ''), ifnull(ug4.uid, '')
from ( games g, users_games ug1, users_games ug2 )
left join users_games ug3 on ug2.gid = ug3.gid and ug2.uid < ug3.uid
left join users_games ug4 on ug3.gid = ug4.gid and ug3.uid < ug4.uid
where
g.id = ug1.gid and
ug1.gid = ug2.gid and
ug1.uid < ug2.uid

这给了我 14 行上面的虚拟数据。我试图通过将 ug1 锚定到最低 UID 播放器的条目来消除一个错误来源:

select g.id, g.stime, g.etime, ug1.uid, ug2.uid,
    ifnull(ug3.uid, ''), ifnull(ug4.uid, '')
from
( games g, users_games ug1, users_games ug2,
    (select gid as g, min(uid) as u from users_games group by g) as xx
)
left join users_games ug3 on ug2.gid = ug3.gid and ug2.uid < ug3.uid
left join users_games ug4 on ug3.gid = ug4.gid and ug3.uid < ug4.uid
where
g.id = xx.g and
ug1.uid = xx.u and
g.id = ug1.gid and
ug1.gid = ug2.gid and
ug1.uid < ug2.uid

现在我减少到 9 行,但我仍然有很多虚假数据。我可以看到问题 - 例如在游戏 3 中,ug1 锚定到用户 102,仍然有 ug2 可以锚定到的三个玩家。等等。但是我想不出解决这个难题的方法——我如何最终实现一个查询,以正确的顺序和数量输出 4 行的玩家?

在我看来,这在其他情况下应该是一个已解决的问题。在这里感谢所有帮助。

【问题讨论】:

  • 我强烈建议您不要混合使用,JOIN 语法。就用JOIN吧,没过20年...

标签: mysql sql


【解决方案1】:

您遇到的一个问题是您没有将用户描述为玩家 1、2、3 或 4 的字段。但是,您需要确保每次 LEFT JOIN 只加入一个玩家。

如果你向 users_games 添加一个“player_id”字段,它就变得微不足道了......

SELECT
  *
FROM
  games
LEFT JOIN
  users_games      AS p1
    ON  p1.gid = games.id
    AND p1.player_id = 1
LEFT JOIN
  users_games      AS p2
    ON  p2.gid = games.id
    AND p2.player_id = 2
LEFT JOIN
  users_games      AS p3
    ON  p3.gid = games.id
    AND p3.player_id = 3
LEFT JOIN
  users_games      AS p4
    ON  p4.gid = games.id
    AND p4.player_id = 4

个替代方法可以避免所有的 LEFT JOIN,但是这个例子很好用,因为它是下一步的基础......)


如果你不能添加这个字段,它会变得更加复杂。 (SQL Server、Oracle 等可以使用 ROW_NUMBER() 代理这个 player_id 字段,MySQL 不行。)

相反,您需要相关的子查询来识别“下一个玩家”。

SELECT
  *
FROM
  games
LEFT JOIN
  users_games      AS p1
    ON  p1.gid = games.id
    AND p1.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id)
LEFT JOIN
  users_games      AS p2
    ON  p2.gid = games.id
    AND p2.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id AND uid > p1.uid)
LEFT JOIN
  users_games      AS p3
    ON  p3.gid = games.id
    AND p3.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id AND uid > p2.uid)
LEFT JOIN
  users_games      AS p4
    ON  p4.gid = games.id
    AND p4.uid = (SELECT MIN(uid) FROM users_games WHERE gid = games.id AND uid > p3.uid)


EDIT 加入免费版本,假设存在 player_id 字段...

SELECT
  games.id,
  MAX(CASE WHEN users_games.player_id = 1 THEN users_games.uid END)   AS p1_id,
  MAX(CASE WHEN users_games.player_id = 2 THEN users_games.uid END)   AS p2_id,
  MAX(CASE WHEN users_games.player_id = 3 THEN users_games.uid END)   AS p3_id,
  MAX(CASE WHEN users_games.player_id = 4 THEN users_games.uid END)   AS p4_id
FROM
  games
LEFT JOIN
  users_games
    ON users_games.gid = games.id
GROUP BY
  games.id

【讨论】:

  • 哇,太棒了。这当然解决了我的问题 :-) 如果你能给出避免所有左连接的方法,我今天的教育就完成了。
  • @ObiObi - 也测试 EugenRieck 的答案。它可能比相关的子查询版本更快。
【解决方案2】:
SELECT games.*,
IF(min(ifnull(ug1.uid,9999999))=9999999,null,ug1.uid) AS user1,
IF(min(ifnull(ug2.uid,9999999))=9999999,null,ug2.uid) AS user2,
IF(min(ifnull(ug3.uid,9999999))=9999999,null,ug3.uid) AS user3,
IF(min(ifnull(ug4.uid,9999999))=9999999,null,ug4.uid) AS user4
FROM games
LEFT JOIN users_games AS ug1 ON ug1.gid=games.id
LEFT JOIN users_games AS ug2 ON ug2.gid=games.id AND ug2.uid>ug1.uid
LEFT JOIN users_games AS ug3 ON ug3.gid=games.id AND ug3.uid>ug2.uid
LEFT JOIN users_games AS ug4 ON ug4.gid=games.id AND ug4.uid>ug3.uid
GROUP BY games.id

当然 9999999 应该是最大可能的用户 ID -1。 这会将上一个答案的子查询与大型分组查询进行交换。

使用您的测试数据在 MySQL 5.1 Ubuntu Lucid 上进行测试。

【讨论】:

  • +1 :我想这确实有效,我个人回避它,因为你做的是半笛卡尔积。 (对于 4 名玩家,您将获得 4*3*2*1=24 条记录,然后您将其分组处理以获得一条记录。)然后您还需要重新加入 users_games 表 4 次以获得每个玩家的分数。但是,我的答案中的相关子查询也不太理想。您最好对这两种方法进行测试,看看在性能和优雅方面您更喜欢哪一种。
  • 你真的需要 IF() 吗?我不使用 MySQL,但我会认为它是相同的,因为 MIN 不会返回 NULL,除非所有值都是 NULL?这意味着由于LEFT JOINs 中的&gt; 谓词,MIN(ugX.uid) 本身就足够了?
  • 冒着投反对票的风险:如果我需要分数,我会使用类似 'concat(ugx.uid,'.',ugx.score') 之类的东西,将其转换为浮点数然后再次分解 - 在大多数数据库主机上,IO 比一些 CPU 周期要昂贵得多
  • 我没有完全开发这个 - 也许 IF 是不必要的,但这只是一个快速破解
  • 我不会对串联建议投反对票。我只会跑一英里然后哭。这是一种选择,但如果可能的话,我真的会尽量避免这种技术债务。
【解决方案3】:

这样岂不是更简单.....

SELECT g.id, GROUP_CONCAT(u.login ORDER BY u.login), g.stime, g.etime
FROM games g,
users u,
users_games ug
WHERE ug.gid=g.id
AND ug.uid=u.id
GROUP BY g.id, g.stime, g.etime

如果你想要分数,只需添加一个函数,然后......

SELECT g.id, GROUP_CONCAT(
     CONCAT(u.login, '=', get_score(u.login, g.id)) ORDER BY 1
     ), g.stime, g.etime
FROM games g,
users u,
users_games ug
WHERE ug.gid=g.id
AND ug.uid=u.id
GROUP BY g.id, g.stime, g.etime

【讨论】:

  • 然后如果您想加入其他Users 表以获取用户元数据等?除非有人可以证明替代方案不合适,否则我绝不会建议将多个值连接到单个字段中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-09-22
  • 1970-01-01
  • 1970-01-01
  • 2023-03-11
  • 2015-08-26
  • 2021-09-02
  • 1970-01-01
相关资源
最近更新 更多