具有多个联接的SQL导致重复

时间:2020-09-12 05:25:53

标签: sql json postgresql count left-join

我正在尝试使用多个左联接进行此查询,但是针对与项目ID相关的每个费用返回重复的更新和科学家(例如,如果有5个费用,则每次更新和科学家将返回5次)。我正在尝试避免使用多个选择语句,但是对此一直遇到麻烦。

SELECT
  projects.*,
  coalesce(json_agg(updates ORDER BY update_date DESC) FILTER (WHERE updates.id IS NOT NULL), '[]') AS updates,
  coalesce(json_agg(scientists) FILTER (WHERE scientists.user_id IS NOT NULL), '[]') AS scientists,
  coalesce(SUM(charges.amount), 0) AS donated,
  coalesce(COUNT(charges), 0) AS num_donations
FROM projects
LEFT JOIN updates
ON updates.project_id = projects.id
LEFT JOIN scientists
ON scientists.project_id = projects.id
LEFT JOIN charges
ON charges.project_id = projects.id
WHERE projects.id = '${id}'
GROUP BY projects.id;

预期结果(更改为仅返回ID):

                  id                  |                   updates                |             scientists             | donated | num_donations 
--------------------------------------+------------------------------------------+------------------------------------+---------+---------------
 17191850-9a03-482f-9afe-7dc6b69974ea | ["0c29417f-0afb-44df-a8cf-24dc5cc7962c"] | ["auth0|5efcfb5f652e5a0019ce2193"] |     155 |             5

实际结果:

                  id                  |                                                                                                 updates                                                                                                  |                                                                                 scientists                                                                                 | donated | num_donations 
--------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------+---------------
 17191850-9a03-482f-9afe-7dc6b69974ea | ["0c29417f-0afb-44df-a8cf-24dc5cc7962c", "0c29417f-0afb-44df-a8cf-24dc5cc7962c", "0c29417f-0afb-44df-a8cf-24dc5cc7962c", "0c29417f-0afb-44df-a8cf-24dc5cc7962c", "0c29417f-0afb-44df-a8cf-24dc5cc7962c"] | ["auth0|5efcfb5f652e5a0019ce2193", "auth0|5efcfb5f652e5a0019ce2193", "auth0|5efcfb5f652e5a0019ce2193", "auth0|5efcfb5f652e5a0019ce2193", "auth0|5efcfb5f652e5a0019ce2193"] |     155 |             5

3 个答案:

答案 0 :(得分:2)

如果您有这样的话:

SELECT p.column, s.column, u.column
FROM 
  p 
  JOIN s ON ...
  JOIN u ON ...

它会产生一行

p1, s1, u1

然后您在以下位置联接另一个表:

SELECT p.column, s.column, u.column, c.column
FROM 
  p 
  JOIN s ON ...
  JOIN u ON ...
  JOIN c ON ...

它突然产生5行。

p1, s1, u1, c1
p1, s1, u1, c2
p1, s1, u1, c3
p1, s1, u1, c4
p1, s1, u1, c5   

您希望它再次产生一行,但又产生另一列,其计数为5:

p1, s1, u1, 5

然后,您需要将重复数据分组并添加计数:

SELECT p.column, s.column, u.column, count(*)
FROM 
  p 
  JOIN s ON ...
  JOIN u ON ...
  JOIN c ON ...
GROUP BY p.column, s.column, u.column

您会注意到GROUP BY部分只是SELECT部分​​的精确重复,减去了计数(一个聚合列)

数据库将根据GROUP BY中指定的键将数据分组。 p1, s1, u1是唯一的组合,并且与5个不同的c1 .. c5值相关联。这种情况下的汇总不适用于cX数据(因为它是count(*),但是可以-如果我们要说的话:

SELECT p.column, s.column, u.column, min(c.column), max(c.column)

然后,数据库与包含所有c值的存储桶一起创建此数据集:

p1, s1, u1, [c1, c2, c3, c4, c5]

并将MIN和MAX函数应用于[c1, c2, c3, c4, c5]存储桶,分别拉动c1c5

在您的脑海中,习惯于将分组操作看作是准备分组中各列的唯一组合,并且将所有其他这些数据项放在一个大的无序存储桶中,并且对MAX / MIN / AVG等函数进行操作存储桶中的内容并提取相关数据(该数据可以来自任何行,自然而然MIN和MAX可能来自不同的行)。分组丢失了“此输入行”的概念,因为它准备了一组新的行


在各种数据库中最典型的分组情况下,如果要分组,则不能使用SELECT *-列出SELECT中的每一列,然后再次列出GROUP BY。这似乎是多余的(确实有些数据库允许您跳过提供分组依据),但是在高级方案中可以按与您选择的内容不同的方式进行分组,因此仅在简单情况下是多余的


现在,希望您能接受以上所有内容。一些数据库不仅具有MIN / MAX等功能,而且还会将存储桶中的所有结果连接起来。像这样的伪SQL:

SELECT p.column, s.column, u.column, STRING_JOIN(c.column, '|')

可能产生:

p1, s1, u1, c1|c2|c3|c4|c5

string_join函数旨在使用指定为定界符的管道字符将存储桶中的所有内容连接在一起。

但是请记住,我们的原始数据是:

p1, s1, u1, c1
p1, s1, u1, c2
p1, s1, u1, c3
p1, s1, u1, c4
p1, s1, u1, c5  

如果仅将p.column作为GROUP BY,则DB将把p1作为键和更多存储桶:

p1, [s1,s1,s1,s1,s1], [u1,u1,u1,u1,u1], [c1,c2,c3,c4,c5]

如果您要STRING_JOIN每一个,您最终都会得到您想要的:

SELECT p.column, STRING_JOIN(s.column, '|'), STRING_JOIN(u.column, '|'), STRING_JOIN(c.column, '|'), 

p1, s1|s1|s1|s1|s1, u1|u1|u1|u1|u1, c1|c2|c3|c4|c5

数据库中没有任何AI会说“在加入之前,我将从s和u桶中删除重复项”。正如我之前提到的,当数据进入存储桶进行聚合时,所有行和排序的概念都会丢失。如果您的数据是:

p1, x1, y1
p1, x2, y2

您分组/加入后可能会得到

p1, x1|x2, y2|y1

请参阅与x相比,将Y字符串中的元素顺序颠倒了-不要依赖“集合中的元素顺序”来推断有关例如他们最初来自的行

那么,查询发生了什么?好吧,就像上面一样,您仅按一个列进行分组,然后将其他列进行汇总,因此您可以看到如何获得未分组列的重复。

如果继续按所有列分组,那么您将只有一个科学家和更新。如果您非常希望将它们作为JSON,那么(假设这确实是postgres),您将具有to_json和row_to_json,它们将给出一个json值,但实际上并没有增加太多,因为各个列尚未提供给您。 Postgres(如果是postgres)将允许您GROUP * *来使json工作:

SELECT p.column, row_to_json(s), row_to_json(u), count(*)
...
GROUP BY p.column, s.*, u.*

存在s。*和u。*将允许row_to_json调用生成描述S和U的json单行,并且计数将计算Cs

答案 1 :(得分:1)

由于在多个表中存在多个匹配项,因此您将多个行连接起来,正如Caius Jard对此进行了充分解释。

典型的解决方案是在子查询中进行预聚合。对于您的用例,您仅在项目上进行过滤,横向联接应该是最有效的选择:

SELECT p.*, u.*, s.*, c.*
FROM projects
CROSS JOIN LATERAL (
    SELECT coalesce(json_agg(updates ORDER BY update_date DESC) FILTER (WHERE u.id IS NOT NULL), '[]') AS updates
    FROM updates u
    WHERE u.project_id = p.id
) u
CROSS JOIN LATERAL (
    SELECT coalesce(json_agg(scientists) FILTER (WHERE s.user_id IS NOT NULL), '[]') AS scientists
    FROM scientists s
    WHERE s.project_id = p.id
) s
CROSS JOIN LATERAL (
    SELECT coalesce(SUM(c.amount), 0) AS donated, coalesce(COUNT(charges), 0) AS num_donations
    FROM charges c
    WHERE c.project_id = p.id
) c ON TRUE
WHERE p.id = '${id}'

答案 2 :(得分:1)

基本问题是 完全 与此处相同:

您后来评论:

与该项目ID相关联的数据库中只有一个不同的更新和科学家

如果可以肯定,那么您所需要做的就是聚集表charges中的行在加入之前

SELECT p.*
     , COALESCE(to_json(u), '[]') AS updates
     , COALESCE(to_json(s), '[]') AS scientists
     , c.donated
     , c.num_donations
FROM   projects        p
LEFT   JOIN updates    u ON u.project_id = p.id
LEFT   JOIN scientists s ON s.project_id = p.id
CROSS  JOIN (
   SELECT COALESCE(SUM(amount), 0) AS donated
        , COUNT(*)    AS num_donations
   FROM   charges
   WHERE  project_id = '${id}'
   ) c
WHERE  p.id = '${id}'

charges上的子查询可以这么简单,因为唯一的过滤器与外部查询中使用的ID相同。我们也不需要COALESCE()来计数,因为...

  1. ... count()永远不会返回NULL。看到:
  2. ...保证子查询(具有聚合函数且没有GROUP BY)仅返回一行,汇总所有符合条件的行-即使0行符合条件。

如果表updatesscientists中毕竟可以有多个相关行,请在CROSS JOIN之前以类似的方式进行汇总。