【问题标题】:Prevent duplicate rows when using LEFT JOIN in Postgres without DISTINCT在没有 DISTINCT 的 Postgres 中使用 LEFT JOIN 时防止重复行
【发布时间】:2021-11-08 21:12:03
【问题描述】:

我有 4 张桌子:

  • 项目
  • 购买
  • 购买物品
  • 购买折扣

在这些表中,Purchase Discount 有两个条目,所有其他的只有一个条目。但是当我查询它们时,由于LEFT JOIN,我得到了重复的条目。

此查询将在大型数据库中运行,我听说使用DISTINCT 会降低性能。有没有其他方法可以在不使用DISTINCT 的情况下删除重复项?

这里是SQL Fiddle

结果显示:

[{"item_id":1,"purchase_items_ids":[1234,1234],"total_sold":2}]

但结果应该是:

[{"item_id":1,"purchase_items_ids":[1234],"total_sold":1}]

【问题讨论】:

  • 从小提琴的外观来看,甚至没有理由在那里使用左连接,因为您甚至没有真正使用表。加入那里的目的是什么?
  • 是的,我们必须接受,因为我从 purchase_discounts 表中获取折扣金额。我会在小提琴中更新。谢谢@Padagomez
  • “所以我听说 DISTINCT 会降低性能” - 我会首先衡量 DISTINC 对性能的影响,然后寻找解决方案。也许你正在解决不存在的问题。 Tag [mysql] 这里需要吗?
  • 只是一个问题——你能改变数据库设计吗?我可以就我认为它如何更好地工作提出一些想法,但据我所知(这只是我的意见,我在这里的排名相当低,所以请把它当作意见)你不会能够规避这个问题,而无需让您的数据库设计有所不同。如果这是一个选项,请告诉我,我很想有机会写下来
  • 小提琴很棒。但也请在问题中添加表定义和查询字符串。这是推荐的方式。当外部链接失效时,更易于阅读且不易受到 bitrot 的影响。

标签: sql json postgresql many-to-many left-join


【解决方案1】:

核心问题是您的LEFT JOIN 增加了行数。见:

在加入之前将折扣汇总到单行。或者使用(不相关的)子查询表达式:

SELECT json_agg(items)
FROM  (
   SELECT pi.item_id
        , array_agg(pi.id) AS purchase_items_ids
        , sum(pi.sold) AS total_sold
        ,(SELECT COALESCE(sum(pd.discount_amount), 0)
          FROM   purchase_discounts pd
          WHERE  pd.purchase_id = 200) AS discount_amount
   FROM   purchase_items pi
   WHERE  pi.purchase_id = 200
   GROUP  BY 1
   ) AS items;

结果:

[{"item_id":1,"purchase_items_ids":[1234],"total_sold":1,"discount_amount":12}]

db小提琴here

我添加了一些额外的改进:

  • 假设 FK constraints 强制执行参照完整性,我们根本不需要涉及表 purchaseitems

  • 删除了一个不做任何事情的子查询级别。

  • 使用json_agg() 代替array_to_json(array_agg())

  • COALESCE() 添加到输出0NULL 以无折扣。

由于折扣适用于模型中的购买,而不适用于单个商品,因此为每个商品输出discount_amount 没有意义。考虑使用此查询来返回一个项目数组和一个单独的 discount_amount

SELECT json_build_object(
         'items'
       , json_agg(items)
       , 'discount_amount'
       , (SELECT COALESCE(sum(pd.discount_amount), 0)
          FROM   purchase_discounts pd
          WHERE  pd.purchase_id = 200)
       )
FROM  (
   SELECT pi.item_id
        , array_agg(pi.id) AS purchase_items_ids
        , sum(pi.sold) AS total_sold
   FROM   purchase_items pi
   WHERE  pi.purchase_id = 200
   GROUP  BY 1
   ) AS items;

结果:

{"items" : [{"item_id":1,"purchase_items_ids":[1234],"total_sold":1}], "discount_amount" : 12}

db小提琴here

使用json_build_object() 组装JSON 对象。

您购买单件商品的示例并不太具有启发性。我为我的小提琴添加了一个包含多个项目且没有折扣的购买。

【讨论】:

    【解决方案2】:

    首先我建议从没有理由存在的查询中删除INNER JOIN items i ON i.id = t.item_id

    然后左加入 Purchase_Discounts 表使用子查询来获取 Discount_amount(如 Lukasz Szozda 的回答中所述)

    如果任何产品没有折扣,则Discount_amount 列将显示NULL。如果你想避免它,那么你可以使用COALESCE(),如下所示:

    COALESCE(SUM((select sum(discount_amount) from purchase_discounts 
                     where purchase_discounts.purchase_id = purchase.id)),0) as discount_amount
    

    Db-Fiddle:

      SELECT array_to_json(array_agg(p_values)) FROM 
         ( 
           SELECT t.item_id, t.purchase_items_ids, t.total_sold, t.discount_amount FROM 
             ( 
               SELECT purchase_items.item_id AS item_id,
                      ARRAY_AGG(purchase_items.id) AS purchase_items_ids,
                      SUM(purchase_items.sold) as total_sold,
                      SUM((select sum(discount_amount) from purchase_discounts 
                          where purchase_discounts.purchase_id = purchase.id)) as discount_amount
                       FROM items
                       INNER JOIN purchase_items ON purchase_items.item_id = items.id
                       INNER JOIN purchase ON purchase.id = purchase_items.purchase_id              
                      WHERE 
                       purchase.id = 200
                      GROUP by 
                       purchase_items.item_id
             ) as t 
           
         ) AS p_values;
    

    输出:

    array_to_json
    [{"item_id":1,"purchase_items_ids":[1234],"total_sold":1,"discount_amount":12}]

    db小提琴here

    【讨论】:

    • @Developer,你试过这个解决方案吗?请告诉我结果。
    【解决方案3】:

    我认为左连接不会导致,因为 Inner Join 查询结果与左连接相同,在使用 purchase_id=200 查询的折扣中,您可以使用 row_number 和 partion_by 相同的 2 个结果:

    ROW_NUMBER() OVER(PARTITION BY purchase_items.id order by purchase_items.id) rn
    

    然后选择 rn=1。 您更改 sum 函数的查询,我认为您可以使用 from partion_by。

    【讨论】:

      【解决方案4】:

      使用相关子查询而不是 LEFT JOIN:

      SELECT array_to_json(array_agg(p_values)) FROM 
      ( 
        SELECT t.item_id, t.purchase_items_ids, t.total_sold, t.discount_amount FROM 
          ( 
            SELECT purchase_items.item_id AS item_id,
                   ARRAY_AGG(purchase_items.id) AS purchase_items_ids,
                   SUM(purchase_items.sold) as total_sold,
                   SUM((SELECT SUM(pd.discount_amount) FROM purchase_discounts pd
                        WHERE pd.purchase_id = purchase.id)) as discount_amount
             FROM items
             INNER JOIN purchase_items ON purchase_items.item_id = items.id
             INNER JOIN purchase ON purchase.id = purchase_items.purchase_id
             WHERE purchase.id = 200
             GROUP by purchase_items.item_id
          ) as t 
        INNER JOIN items i ON i.id = t.item_id 
      ) AS p_values;
      

      db<>fiddle demo

      输出:

      [{"item_id":1,"purchase_items_ids":[1234],"total_sold":1,"discount_amount":12}]
      

      【讨论】:

        【解决方案5】:

        LEFT JOIN 不会导致您的重复,我理解您为什么需要它,因为可能没有任何折扣,但是对于提供的数据更改为内部连接会产生相同的结果。因为您使用ARRAY_AGG(purchase_items.id),您收到重复条目。此外,在提供数据的情况下,表itempurchase 是不必要的。可以使用sum和distinct on的window版本,减少purchase_id的重复,消除上面提到的表格。最后中间的select ... ) t可以完全去掉。结果:(见演示)

        select array_to_json(array_agg(p_values)) 
          from (select distinct on (pi.item_id, pi.id)
                        pi.item_id
                      , pi.id purchase_items_ids
                      , sum(pi.sold) over (partition by pi.item_id) total_sold         
                      , sum(pd.discount_amount) over(partition by  pi.item_id)  discount_amount
                   from purchase_items pi  
                   left join purchase_discounts pd 
                     on pd.purchase_id = pi.purchase_id 
                  order by pi.item_id, pi.id           
               ) as p_values; 
        

        【讨论】:

        • 感谢@belayer,我仍然可以看到总销量是 2,但我们只购买了一次。
        【解决方案6】:

        如果您只能在purchase_discounts 表中拥有多个值,那么将多个purchase_discounts 行聚合到一个之前连接的子查询可以解决问题:

        SELECT array_to_json(array_agg(p_values)) FROM 
        ( 
          SELECT t.item_id, t.purchase_items_ids, t.total_sold, t.discount_amount FROM 
            ( 
              SELECT purchase_items.item_id AS item_id,
                     ARRAY_AGG(purchase_items.id) AS purchase_items_ids,
                     SUM(purchase_items.sold) as total_sold,
                     X.discount_amount
                     FROM items
                      INNER JOIN purchase_items ON purchase_items.item_id = items.id
                      INNER JOIN purchase ON purchase.id = purchase_items.purchase_id
                      LEFT JOIN (SELECT purchase_id, sum(purchase_discounts.discount_amount) AS discount_amount FROM purchase_discounts GROUP BY purchase_id) X ON X.purchase_id = purchase.id
                     WHERE 
                      purchase.id = 200
                     GROUP by 
                      purchase_items.item_id,
                      X.discount_amount
            ) as t 
          INNER JOIN items i ON i.id = t.item_id 
        ) AS p_values;
        

        【讨论】:

        • 感谢@fog,这里实际上不能对discount_amount进行分组,只能对item_id进行分组
        • 另外,我发现这需要更多时间来执行。
        • @Developer 您需要澄清您正在寻找没有 DISTINCT 的性能更高的查询。因为正式的“没有 DISTINCT”可以用 GROUP BY
        猜你喜欢
        • 2015-08-05
        • 2021-07-17
        • 1970-01-01
        • 1970-01-01
        • 2021-09-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-04-02
        相关资源
        最近更新 更多