【问题标题】:SQL with multiple joins causing duplicates具有多个连接导致重复的 SQL
【发布时间】:2020-09-12 05:25:53
【问题描述】:

我正在尝试使用多个左连接进行此查询,但为与项目 ID 关联的每个费用返回重复的更新和科学家(例如,如果有 5 次费用,则每次更新和科学家返回 5 次)。我试图避免使用多个 select 语句,但遇到了问题。

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

【问题讨论】:

  • 以表格格式提供样本数据和您的预期输出
  • 更新了!感谢@Fahmi 的建议
  • “如果有 5 次收费,则每位科学家返回 5 次” - 因此仅限于您想要的 1 次收费。这称为笛卡尔积。数据库正在给你你所要求的;您必须更准确地要求您要求的内容。您没有告诉我们您想要哪一项费用(最新的、最高的,将所有费用分组到一个 csv 中),所以我们不知道该做什么,就像数据库知道该做什么一样
  • @CaiusJard 我不认为我理解正确。我希望为指定的项目 ID 返回所有科学家和更新,但在与该项目 ID 关联的数据库中只有一个不同的更新和科学家。当我添加带有费用的连接(与项目 ID 相关联的 X 项不同费用)时,科学家和更新会重复 X 次。
  • 当然是。您希望矩形结果网格看起来如何?您有 1 个项目、1 个项目科学家、1 个项目更新和 5 个项目费用。如果您加入项目、科学家和更新,您将获得 1 行。当您在数据库中添加费用时,必须重复项目/科学家/更新数据 5 次,每 5 次费用重复一次。如果只有最新的费用是相关的,那么您就进行查询的那一部分,因为只有五个费用行中的一个适用,您将再次获得一行

标签: sql json postgresql count left-join


【解决方案1】:

如果你有这个:

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]

并分别对[c1, c2, c3, c4, c5]桶拉c1c5应用MIN和MAX函数

在您看来,习惯于将分组操作视为准备 group by 中列的唯一组合,加上将所有这些其他数据项放在一个大的无序存储桶中,并且 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,那么数据库会将 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

数据库中没有任何人工智能会看起来并说“我会在加入之前从 s 和 u 存储桶中删除重复项”,也不应该有。正如我之前提到的,当数据进入存储桶进行聚合时,所有行和排序的概念都会丢失。如果您的数据是:

p1, x1, y1
p1, x2, y2

你分组/加入你最终可能会得到

p1, x1|x2, y2|y1

查看 Y 字符串中元素的顺序与 x 相比是反转的 - 不要依赖“集合中元素的顺序”来推断任何关于例如他们最初来自的那一行

那么,您的查询是怎么回事?好吧,您只按一列分组并聚合其他列,就像上面一样,所以您可以看到如何获得非分组列的重复。

如果您继续按所有列进行分组,那么您将拥有自己的科学家和更新。如果您迫切希望它们作为 JSON,那么(假设这确实是 postgres)您将拥有 to_json 和 row_to_json,它们将提供单个 json 值,但它并没有真正增加单个列还没有给您的东西。 Postgres(如果这是 postgres)将允许您 GROUP BY * 让 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

【讨论】:

    【解决方案2】:

    您的联接将多行,因为在多个表中有多个匹配项,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}'
    

    【讨论】:

    • 谢谢你!我想我混淆了人们,说有一位独特的科学家并在不一定是这种情况时更新。使用 CROSS JOINS 为我工作!
    【解决方案3】:

    基本问题一模一样和这里一样:

    你后来评论说:

    在与该项目 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 之前以类似的方式聚合。

    【讨论】:

      猜你喜欢
      • 2018-12-12
      • 1970-01-01
      • 2020-10-30
      • 1970-01-01
      • 2014-04-05
      • 1970-01-01
      • 2017-06-02
      • 1970-01-01
      • 2013-04-01
      相关资源
      最近更新 更多