【问题标题】:How to merge SQL rows -- only when ALL rows meet criteria?如何合并 SQL 行——仅当所有行都符合条件时?
【发布时间】:2021-02-09 05:01:16
【问题描述】:

我有一个表 (Oracle SQL),其中包含每个商店位置的商品价格列表的详细信息。我想将多行合二为一——但只有当一个项目的所有行都符合条件时:该项目在所有位置的价格都相同。

数据表(简化)如下所示:

list_id, item_id, location_id, item_price
1        1        1            1.99
1        1        2            1.99
1        1        3            1.99
1        2        1            3.99
1        2        2            3.99
1        2        3            3.99
1        3        1            5.99
1        3        2            7.99
1        3        3            8.99

...我想要这个:

list_id, item_id, location_id, item_price
1        1        0            1.99
1        2        0            3.99
1        3        1            5.99
1        3        2            7.99
1        3        3            8.99

项目 1 和 2 的行已分别合并为一行,位置设置为零(全部)。第 3 项的行保持不变,因为所有位置的价格都不相同。

此查询帮助我确定何时不需要合并项目(存在具有相同 item_id 的两行):

select count(list_id), item_id, item_price 
from list_detail
group by item_id, item_price

...但我无法理解它如何适合更大的触发器、脚本或任何可以识别和组合行的东西。

注意:我无法更改表的结构,因为它被许多其他进程所依赖。

您将如何最好地识别并组合所有位置价格相同的行?脚本、触发器、计划的控制台应用程序?

【问题讨论】:

  • 任何输入值都可以为 NULL 吗?例如,某件商品在某个位置的价格可以为 NULL 吗?如果是这样,应该如何处理?例如:一个位置的价格为 3.99,而所有其他位置的价格为 NULL - 该组应该怎么办?
  • 有一些验证/约束可以防止这些列中的任何一个为 NULL ......但为了使代码健壮或为未来的谷歌用户,我会说这个实现不应该合并行您给出的示例,因为 NULL 3.99。这篇文章的重点是创建数据,后续流程可以使用 location_id==0 从一行中读取数据,然后简单地为所有位置设置相同的商品价格,而不是为该商品处理多个冗余行。这是一个相当大的数据集,因此以这种方式合并可能也会减少存储需求。
  • 好的,明白了。但是在某个地方,不知何故,您需要存储有关哪些位置携带该产品的信息;这将消耗(几乎)相同数量的空间。 (我希望你不会通过将一个项目的所有可用位置存储在一个逗号分隔的字符串中来打破第一个正常形式。)然后,回到问题:如果一个特定的价格应该如何处理问题项目(在一个特定列表上)是null 在所有位置?按照你的解释,应该等同于“处处同价”。对吗?

标签: sql oracle count distinct aggregate-functions


【解决方案1】:

一个选项使用窗口函数,然后distinct

select distinct list_id, item_id, location_id, item_price
from (
    select list_id, item_id, item_price,
        case when min(item_price) over(partition by list_id, item_id) = max(item_price) over(partition by list_id, item_id) 
                then 0
                else location_id
            end location_id
    from mytable t
) t

基本思想是比较具有相同list_iditem_id 的组中的最低和最高价格。当它们相等时,我们知道我们在组中只有一个不同的值,所以我们将location_id 转换为0,否则我们保持原样。剩下要做的就是保持不同的值。

【讨论】:

  • 按照我的阅读方式,OP 可能对查询以识别受影响的行感兴趣,但他更感兴趣的是如何使用该信息来更改数据(插入、更新,删除,合并.....)我在这个答案中没有看到任何类似的内容。
  • 是的。目标是“压缩”这些数据,以便稍后另一个进程可以读取一行(location_id==0)并知道所有位置的价格相同,而不是为该项目处理许多冗余行,所有行都相同价格。
【解决方案2】:

由于您必须在单个语句中更新某些行并删除其他行,因此最好使用 merge 语句,这正是为此目的。

s(ource) 行集是聚合的结果 - 用于标识必须修改的 (list_id, item_id)

请注意,我假设价格永远不会是null;如果可以是null,你必须说明应该如何处理。

将提供使用分析函数的解决方案。如果效率(速度)很重要,下面的解决方案会更好;当两者都做同样的工作时,聚合比分析函数快得多。

merge into sample_data t
  using (
          select list_id, item_id, min(location_id) as min_loc_id
          from   sample_data
          group  by list_id, item_id
          having min(item_price) = max(item_price)
        ) s
    on (t.list_id = s.list_id and t.item_id = s.item_id)
when matched then
  update
    set   t.location_id = case when t.location_id = s.min_loc_id then 0 end
  delete
    where t.location_id is null
;

目标(即您的基表)中的行只有在与源匹配 list_id, item_id 时才会受到影响;其他行将保持不变。 (这些未更改的行是价格在所有位置不同相同的商品的行 - 因此相应的 list_id, item_id 不会出现在源中。)

update 部分会将第一个位置 ID 更改为 0,其他所有位置 ID 更改为 null。然后delete 部分将删除位置id 为null 的所有行。在这一步中,位置 id 是修改后的,在 update 部分完成工作之后。因此,对于受影响的location_id, item_id,除一行之外的所有行都将被delete 步骤删除。

【讨论】:

    【解决方案3】:

    嗯。 . . .你可以使用窗口函数:

    select list_id, item_id,
           (case when min_price = max_price then 0
                 else location_id
            end) as location_id,
           price
    from (select t.*,
                 min(price) over (partition by list_id, item_id) as min_price,
                 max(price) over (partition by list_id, item_id) as max_price
          from t
         ) t
    group by list_id, item_id,
             (case when min_price = max_price then 0
                   else location_id
              end), price;
    

    另一种方法是使用existsunion all

    select list_id, item_id, location_id, price
    from t
    where exists (select 1
                  from t t2
                  where t2.list_id = t.list_id and
                        t2.item_id = t.item_id and
                        t2.price <> t.price
                 )
    union all
    select list_id, item_id, 0, max(price)
    from t
    group by list_id, item_id
    having min(price) = max(price);
    

    【讨论】:

    • 按照我的阅读方式,OP 可能对查询以识别受影响的行感兴趣,但他更感兴趣的是如何使用该信息来更改数据(插入、更新,删除,合并.....)我在这个答案中没有看到任何类似的内容。
    • 是的。目标是“压缩”这些数据,以便稍后另一个进程可以读取一行(location_id==0)并知道所有位置的价格相同,而不是为该项目处理许多冗余行,所有行都相同价格。
    【解决方案4】:

    您可以对UPDATEDELETE 使用MERGE 语句,如果您使用分析函数来识别受影响的行,那么您可以使用ROWID 伪列关联合并,它可以执行自联接(比比较值更有效):

    MERGE INTO table_name dst
    USING (
      SELECT ROWID AS rid,
             rn
      FROM   (
        SELECT ROW_NUMBER() OVER ( PARTITION BY list_id, item_id ORDER BY location_id )
                 AS rn,
               MIN( item_price ) OVER ( PARTITION BY list_id, item_id ) AS min_price,
               MAX( item_price ) OVER ( PARTITION BY list_id, item_id ) AS max_price
        FROM   table_name
      )
      WHERE min_price = max_price
    ) src
    ON ( src.rid = dst.ROWID )
    WHEN MATCHED THEN
      UPDATE SET location_id = 0
      DELETE WHERE src.rn > 1;
    

    其中,对于样本数据:

    CREATE TABLE table_name ( list_id, item_id, location_id, item_price ) AS
    SELECT 1, 1, 1, 1.99 FROM DUAL UNION ALL
    SELECT 1, 1, 2, 1.99 FROM DUAL UNION ALL
    SELECT 1, 1, 3, 1.99 FROM DUAL UNION ALL
    SELECT 1, 2, 1, 3.99 FROM DUAL UNION ALL
    SELECT 1, 2, 2, 3.99 FROM DUAL UNION ALL
    SELECT 1, 2, 3, 3.99 FROM DUAL UNION ALL
    SELECT 1, 3, 1, 5.99 FROM DUAL UNION ALL
    SELECT 1, 3, 2, 7.99 FROM DUAL UNION ALL
    SELECT 1, 3, 3, 8.99 FROM DUAL;
    

    更新 2 行并删除 4 行,将表格保留为:

    LIST_ID |项目 ID | LOCATION_ID | ITEM_PRICE ------: | ------: | ----------: | ---------: 1 | 1 | 0 | 1.99 1 | 2 | 0 | 3.99 1 | 3 | 1 | 5.99 1 | 3 | 2 | 7.99 1 | 3 | 3 | 8.99

    db小提琴here


    如果您可以为item_price 提供NULL 值,则可以将查询扩展为仅在行全部为非NULL 或全部为NULL 时进行过滤:

    MERGE INTO table_name dst
    USING (
      SELECT ROWID AS rid,
             rn
      FROM   (
        SELECT ROW_NUMBER() OVER ( PARTITION BY list_id, item_id ORDER BY location_id )
                 AS rn,
               MIN( item_price ) OVER ( PARTITION BY list_id, item_id ) AS min_price,
               MAX( item_price ) OVER ( PARTITION BY list_id, item_id ) AS max_price,
               COUNT(item_price)
                 OVER ( PARTITION BY list_id, item_id ) AS num_non_null,
               COUNT(*)
                 OVER ( PARTITION BY list_id, item_id ) AS num_locations
        FROM   table_name
      )
      WHERE ( min_price = max_price AND num_non_null = num_locations )
      OR    ( num_non_null = 0 )
    ) src
    ON ( src.rid = dst.ROWID )
    WHEN MATCHED THEN
      UPDATE SET location_id = 0
      DELETE WHERE src.rn > 1;
    

    其中,对于样本数据:

    CREATE TABLE table_name ( list_id, item_id, location_id, item_price ) AS
    SELECT 1, 1, 1, 1.99 FROM DUAL UNION ALL
    SELECT 1, 1, 2, 1.99 FROM DUAL UNION ALL
    SELECT 1, 1, 3, 1.99 FROM DUAL UNION ALL
    SELECT 1, 2, 2, 3.99 FROM DUAL UNION ALL
    SELECT 1, 2, 3, 3.99 FROM DUAL UNION ALL
    SELECT 1, 2, 4, 3.99 FROM DUAL UNION ALL
    SELECT 1, 3, 1, 5.99 FROM DUAL UNION ALL
    SELECT 1, 3, 2, 7.99 FROM DUAL UNION ALL
    SELECT 1, 3, 3, 8.99 FROM DUAL UNION ALL
    SELECT 1, 4, 8, 1.99 FROM DUAL UNION ALL
    SELECT 1, 4, 9, NULL FROM DUAL UNION ALL
    SELECT 1, 5, 1, NULL FROM DUAL UNION ALL
    SELECT 1, 5, 2, NULL FROM DUAL;
    

    输出:

    LIST_ID |项目 ID | LOCATION_ID | ITEM_PRICE ------: | ------: | ----------: | ---------: 1 | 1 | 0 | 1.99 1 | 2 | 0 | 3.99 1 | 3 | 1 | 5.99 1 | 3 | 2 | 7.99 1 | 3 | 3 | 8.99 1 | 4 | 8 | 1.99 1 | 4 | 9 | 1 | 5 | 0 |

    db小提琴here

    【讨论】:

      猜你喜欢
      • 2014-10-29
      • 1970-01-01
      • 2023-02-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多