Postgres深入左外连接两个级别导致笛卡尔积

时间:2013-02-05 07:28:00

标签: sql postgresql left-join aggregate-functions

鉴于以下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 )

我正在尝试获取所有活动的清单,以及任何成员的数量,以及任何捐赠的总和。问题是捐款的总和出现了错误(似乎是捐赠*#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,则总和是正确的。

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

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

4 个答案:

答案 0 :(得分:4)

正如我详细解释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。那要快一点。

答案 1 :(得分: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

答案 2 :(得分:1)

当然,每次活动都会在捐款和活动之间获得笛卡尔产品,因为两者都只与事件有关,除了活动ID之外,捐赠和event_members之间没有联系关系,这当然意味着每个成员都匹配捐赠。

答案 3 :(得分:0)

当您进行查询时,您要求所有事件 - 假设有两个事件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

因此,当要求总和时,爱丽丝出来捐赠200美元就不足为奇了。

如果您想要所有捐款的总和,您最好使用两个不同的查询。尝试使用单个查询尽可能地执行所有操作都是经典SQL Antipattern(实际上是第18章中的“Spaghetti Query”):

  

非预期产品

     

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