【问题标题】:Left outer join two levels deep in Postgres results in cartesian product在 Postgres 中左外连接两层导致笛卡尔积
【发布时间】:2013-01-20 01:34:59
【问题描述】:

给定以下 4 个表格:

CREATE TABLE events ( id, name )
CREATE TABLE profiles ( id, event_id )
CREATE TABLE donations ( amount, profile_id )
CREATE TABLE event_members( id, event_id, user_id )

我正在尝试获取所有活动的列表,以及任何成员的数量以及任何捐赠的总和。问题是捐赠的总和返回错误(似乎是捐赠的笛卡尔结果 * # of event_members)。

这是 SQL 查询 (Postgres)

SELECT events.name, COUNT(DISTINCT event_members.id), SUM(donations.amount)
FROM            events
LEFT OUTER JOIN profiles      ON events.id = profiles.event_id
LEFT OUTER JOIN donations     ON donations.profile_id = profiles.id
LEFT OUTER JOIN event_members ON event_members.event_id = events.id
GROUP BY events.name

总和(donations.amount)返回 = 实际捐赠总和 * event_members 中的行数。如果我注释掉 count(distinct event_members.id) 和 event_members left outer join,总和是正确的。

编辑:Erwin 为我指出了正确的方向。查询重写为:

选择 events.name, COUNT(DISTINCT event_members.id), 从捐赠中选择(SUM(donations.amount),其中donations.profile_id = profiles.id 和profiles.event_id = events.id 的个人资料)作为total_donations 从事件 左外连接 event_members ON event_members.event_id = events.id GROUP BY events.name

【问题讨论】:

  • 顺便说一下,将 sum(donations.amount) 更改为 count(distinct Donations.id) 确实会产生正确的捐款数量
  • 嗨,刚刚重新排序了您问题中的create table 语句以反映join 的顺序。
  • 嗨,您可以将自己的解决方案变成一个新的答案(甚至接受它,如果您最喜欢它:)
  • 使用EXPLAIN ANALYZE 测试性能。相关的子查询(就像您在解决方案中使用的那样)通常要慢得多。如果在基表中的许多行中结果中只有几行,它仍然可能获胜。

标签: sql postgresql left-join aggregate-functions


【解决方案1】:

正如我详细解释的under the referenced question,您需要先聚合,然后加入表以避免代理CROSS JOIN。像这样:

SELECT e.name, e.sum_donations, m.ct_members
FROM (
    SELECT e.id, e.name, SUM(d.amount) AS sum_donations
    FROM   events             e
    LEFT   JOIN profiles      p ON p.event_id = e.id
    LEFT   JOIN donations     d ON d.profile_id = p.id
    GROUP  BY 1, 2
    ) e
LEFT   JOIN (
    SELECT event_id, COUNT(DISTINCT id) AS ct_members
    FROM   event_members
    GROUP  BY 1
    ) m ON m.event_id = e.id

如果 event_members.id 是主键(可能有人假设),您可以简化为

COUNT(*) AS ct_members

因为id 保证为UNIQUE NOT NULL。这有点快。

【讨论】:

  • 你为什么在第一个子选择中 GROUP BY event.idevent.nameevent.name 可能取决于 event.id
  • @biziclop:因为我可能不得不这样做。每个SELECT 项目必须要么在GROUP BY 列表中,要么在聚合函数中使用。从 PostgreSQL 9.1 开始,表的主键覆盖了该表的所有列,但从问题中不清楚它主键并且我们正在运行 Postgres 9.1 +.
  • 对于我的示例结果集(2 个事件,其他表中的每个事件 200 个),所有解决方案都彼此占用 +-3 毫秒。我确实喜欢这个解决方案的结构方式
【解决方案2】:

您似乎有这两个独立的结构(-[ 表示1-N 关联):

events -[ profiles -[ donations
events -[ event members

我将第二个包裹到一个子查询中:

SELECT events.name,
  member_count.the_member_count
  COUNT(DISTINCT event_members.id),
  SUM(donations.amount)

FROM            events
LEFT OUTER JOIN profiles      ON events.id = profiles.event_id
LEFT OUTER JOIN donations     ON donations.profile_id = profiles.id

LEFT OUTER JOIN (
  SELECT
    event_id,
    COUNT(*) AS the_member_count
  FROM event_members
  GROUP BY event_id
) AS member_count
  ON member_count.event_id = events.id

GROUP BY events.name

【讨论】:

  • 警告:我的查询可能不是语法正确的 PostgreSQL 查询。
  • 这对将 select 语句放入连接中的新颖方法进行了投票。性能几乎与我的 解决方案完全相同。为了后代,需要将 member_count.the_member_count 添加到 group by
  • 你确定我的答案更好,而不是欧文的答案吗?随意不接受我的回答:)
  • @JohnP:不知道你为什么接受这个。基本思路是对的,但是查询无效。
【解决方案3】:

当您进行查询时,您要求所有事件 - 假设有两个,事件 Alpha 和事件 Beta - 然后加入成员。假设有一个成员 Alice 参加了这两个活动。

SELECT events.name, COUNT(DISTINCT event_members.id), SUM(donations.amount)
FROM            events
LEFT OUTER JOIN profiles      ON events.id = profiles.event_id
LEFT OUTER JOIN donations     ON donations.profile_id = profiles.id
LEFT OUTER JOIN event_members ON event_members.event_id = events.id
GROUP BY events.name

在每一行中,您都询问了爱丽丝的捐款总额。如果 Alice 捐赠了 100 美元,那么您要求:

Alpha  Alice  100USD
Beta   Alice  100USD

因此,当询问 总金额时,Alice 显示捐赠了 200 美元也就不足为奇了。

如果您想要所有捐款的总和,您最好使用两个不同的查询。尝试使用单个查询完成所有操作,虽然可能,但将是经典的SQL Antipattern(实际上是第 18 章“意大利面条查询”中的那个):

非预期产品

产生你所有的一个常见后果 导致一个查询是笛卡尔积。当两个 查询中的表没有限制它们的条件 关系。没有这样的限制,两个表对的连接 第一个表中的每一行到另一个表中的每一行。每一个这样的 配对成为结果集的一行,你最终得到更多 行数超出您的预期。

【讨论】:

  • 不幸的是,我正在使用的报告系统必须从单个查询中获取所有结果。在列列表中嵌入子选择就可以了
【解决方案4】:

当然,您会在每个事件的捐赠和事件之间获得笛卡尔积,因为两者都只绑定到事件,除了事件 ID 之外,捐赠和 event_members 之间没有连接关系,这当然意味着每个成员都匹配每个捐赠。

【讨论】:

    猜你喜欢
    • 2019-11-23
    • 1970-01-01
    • 1970-01-01
    • 2015-10-07
    • 1970-01-01
    • 1970-01-01
    • 2020-04-29
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多