SQL 中的自引用 CASE WHEN 子句

Posted

技术标签:

【中文标题】SQL 中的自引用 CASE WHEN 子句【英文标题】:Self-referential CASE WHEN clause in SQL 【发布时间】:2016-04-04 18:51:38 【问题描述】:

我正在尝试将一些格式不正确的数据迁移到数据库中。数据来自 CSV,并首先加载到所有 varchar 列的临时表中(因为在这个阶段我无法强制类型安全)。

数据可能看起来像

COL1     | COL2 | COL3
Name 1   |      |     
2/11/16  | $350 | $230
2/12/16  | $420 | $387
2/13/16  | $435 | $727
Name 2   |      |     
2/11/16  | $121 | $144
2/12/16  | $243 | $658
2/13/16  | $453 | $214

第一列混合了公司名称作为伪标题,以及与第 2 列和第 3 列数据相关的日期。我想通过创建一个“品牌”列开始转换数据 - 如果 Col2 为 NULL,则“StoreBrand”是 Col1 的值,否则是前一行的 StoreBrand。像这样的:

COL1     | COL2 | COL3 | StoreBrand
Name 1   |      |      | Name 1
2/11/16  | $350 | $230 | Name 1
2/12/16  | $420 | $387 | Name 1
2/13/16  | $435 | $727 | Name 1
Name 2   |      |      | Name 2
2/11/16  | $121 | $144 | Name 2
2/12/16  | $243 | $658 | Name 2
2/13/16  | $453 | $214 | Name 2

这是我写的:

SELECT 
    t.*,
    CASE
        WHEN t.COL2 IS NULL THEN COL1
        ELSE                     LAG(StoreBrand) OVER ()
    END AS StoreBrand
FROM
(
    SELECT
        ROW_NUMBER() OVER () AS i,
        *
    FROM
        Staging_Data
) t;

但是数据库(在这种情况下为 postgres,但我们正在考虑替代方案,因此首选最多样化的答案)在 LAG(StoreBrand) 上阻塞,因为这是我正在创建的派生列。调用 LAG(Col1) 只会填充第一行的真实数据:

COL1     | COL2 | COL3 | StoreBrand
Name 1   |      |      | Name 1
2/11/16  | $350 | $230 | Name 1
2/12/16  | $420 | $387 | 2/11/16
2/13/16  | $435 | $727 | 2/12/16
Name 2   |      |      | Name 2
2/11/16  | $121 | $144 | Name 2
2/12/16  | $243 | $658 | 2/11/16
2/13/16  | $453 | $214 | 2/12/16

我的目标是 StoreBrand 列,它是下一个品牌名称之前所有日期值的 COL1 的第一个值:

COL1     | COL2 | COL3 | StoreBrand
Name 1   |      |      | Name 1
2/11/16  | $350 | $230 | Name 1
2/12/16  | $420 | $387 | Name 1
2/13/16  | $435 | $727 | Name 1
Name 2   |      |      | Name 2
2/11/16  | $121 | $144 | Name 2
2/12/16  | $243 | $658 | Name 2
2/13/16  | $453 | $214 | Name 2

当 Col2 和 Col3 为 null 时,StoreBrand 的值无关紧要 - 该行将作为转换过程的一部分被删除。重要的是将数据行(即带有日期的行)与其品牌相关联。

有没有办法引用我缺少的列的先前值?

【问题讨论】:

结果应该是什么样子? 您是否使用某种行号列(例如,serial)导入了保留原始顺序的数据。 Gordon,数据库正在维护订单,如果有帮助,可以使用类似 row_number() 的方法获取特定值 使用plpgsql代替纯sql会容易很多。当然,如果你没有反对的话。 【参考方案1】:

为通过搜索引擎找到此问题的人编辑:

诀窍是使用WITH,它允许在多个地方使用临时结果 (link)。


我认为这可以满足您的需求并同时丢弃空行(如果您愿意的话)。我们基本上选择了我们当前正在查看的行之前的所有品牌,如果它与当前行之间不存在“品牌行”,那么我们就取它。

WITH t AS
   (SELECT
      ROW_NUMBER() OVER () AS i,
      *
   FROM
      Staging_Data
   )
SELECT
   a.COL1,
   a.COL2,
   a.COL3,
   (SELECT b.COL1 FROM t b WHERE b.COL2 IS NULL AND b.i <= a.i AND NOT EXISTS(
      SELECT * FROM t c WHERE c.COL2 IS NULL AND c.i <= a.i AND c.i > b.i)
   ) StoreBrand
FROM
   t a
WHERE -- I don't think you need those rows? Otherwise remove it.
   a.COL2 IS NOT NULL

这可能有点令人困惑。 t 是我们定义的临时表 with 您的查询。而abct 的别名。我们也可以写FROM t AS a 让它更明显。

【讨论】:

OK 第一遍,这看起来不错!你能解释一下别名'a'的定义吗?据我所知,它仅存在于此查询中(可能只是我在为所有单个字符名称而苦苦挣扎) @J.Doe 为它添加了解释。您也可以选择比t 更长的名称,例如Stage_Two 或其他东西,使其更具可读性。【参考方案2】:

我想我明白你想要什么。从技术上讲,您需要lag() 上的ignore nulls 选项,所以它看起来像这样:

select lag(case when col1 not like '%/%/%' then col1 end ignore nulls) over (order by linenumber) as brandname

唯一的问题? Postgres 不支持ignore nulls

但是,您可以用子查询做几乎相同的事情。这个想法是为每个组分配一个分组标识符。这是有效品牌名称的累积计数。然后一个简单的max() 聚合就起作用了:

select t.*,
       max(case when col1 not like '%/%/%' then col1 end) over (partition by grp) as brand
from (select t.*,
             sum(case when col1 not like '%/%/%' then 1 end) over
                 (order by linenumber) as grp
      from t
     );

【讨论】:

不确定这应该如何工作 - col1 是 varchar,你打算如何总结它? Postgres 用“错误:函数总和(字符变化)不存在第 5 行:SUM(CASE WHEN Col1 NOT LIKE '%' THEN Col1 END)OVER(ORDER B ... ^ 提示:没有函数匹配给定名称和参数类型。您可能需要添加显式类型转换。' 我认为你想使用sum(... then 1 end)... 而不是col1

以上是关于SQL 中的自引用 CASE WHEN 子句的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL 分组聚合查询中 filter 子句替换 case when

在sql中case子句的两种形式

ORDER BY 子句的 CASE WHEN 语句

如何在 PySpark SQL when() 子句中使用聚合值?

CASE语句中WHEN子句的执行顺序

MySQL CASE WHEN where 子句导致失败