当行丢失时,用 Oracle SQL PIVOT 结果中的自定义值替换 NULL

Posted

技术标签:

【中文标题】当行丢失时,用 Oracle SQL PIVOT 结果中的自定义值替换 NULL【英文标题】:replacing NULLs with custom values in Oracle SQL PIVOT results, when the rows are missing 【发布时间】:2021-02-18 10:17:35 【问题描述】:

当没有要透视的行时,透视查询返回NULL

在以下示例中,id=2 缺少 COLOR 属性。

with src_data (id, attr_name, attr_id, attr_type) as (
    select 1, 'ITALY', 'IT', 'COUNTRY' FROM DUAL UNION ALL --
    select 1, 'GREEN', 'G', 'COLOR' FROM DUAL UNION ALL --
    select 1, 'BIG', 'B', 'SIZE' FROM DUAL UNION ALL --
    select 2, 'FRANCE', 'FR', 'COUNTRY' FROM DUAL UNION ALL --
    select 2, 'SMALL', 'S', 'SIZE' FROM DUAL  --
)
select * from src_data
    PIVOT (MAX(ATTR_NAME) AS NAME, MAX(ATTR_ID) AS ID --
    FOR attr_type IN ('COUNTRY' AS "COUNTRY", 'COLOR' AS "COLOR", 'SIZE' AS "SIZE"));

结果是

ID COUNTRY_NAME COUNTRY_ID COLOR_NAME COLOR_ID SIZE_NAME SIZE_ID
1 ITALY IT GREEN G BIG B
2 FRANCE FR NULL NULL SMALL S

see dbfiddle

我想用特定值替换那些空值(例如,使用 N/D 作为名称,使用 -1 作为 ID)。

天真的尝试不起作用

PIVOT (NVL(MAX(ATTR_NAME), 'N/D') AS NAME ... ORA-56902: expect aggregate function inside pivot operation PIVOT (MAX(NVL(ATTR_NAME, 'N/D')) AS NAME ... 仍然给出空结果。我的解释是,甚至从未调用过 NVL,因为根本没有要调用的行(ATTR_TYPE = 'COLOR' AND ID = 2

我看到的非常丑陋的解决方案是

PIVOT 生成的所有列上添加特定的NVL 逻辑。我的真实案例有 14 个这样的列。 向PIVOT 输入添加虚假行以涵盖此类情况

有更好的想法吗?

--- 编辑---

看起来本机 pivot 无法做到这一点。我能做的最好的就是用外连接添加缺失的行,并在外连接返回的真实NULLs 上添加nvl

with src_data (id, attr_name, attr_id, attr_type) as (
    select 1, 'ITALY', 'IT', 'COUNTRY' FROM DUAL UNION ALL
    select 1, 'GREEN', 'G', 'COLOR' FROM DUAL UNION ALL
    select 1, 'BIG', 'B', 'SIZE' FROM DUAL UNION ALL
    select 2, 'FRANCE', 'FR', 'COUNTRY' FROM DUAL UNION ALL
    select 2, 'SMALL', 'S', 'SIZE' FROM DUAL
),
     src_ids_types as (
         select src_ids.id, src_types.attr_type
         from (select distinct id from src_data) src_ids
                  cross join (select distinct attr_type from src_data) src_types
     ),
     full_data as (
         select sit.id, sit.attr_type, d.attr_name, d.attr_id
         from src_ids_types sit
                  left outer join src_data d on d.id = sit.id and d.attr_type = sit.attr_type
     )
select *
from full_data d
    PIVOT (MAX(NVL(ATTR_NAME, 'N/D')) AS NAME, MAX(NVL(ATTR_ID, -1)) AS ID --
    FOR attr_type IN ('COUNTRY' AS "COUNTRY", 'COLOR' AS "COLOR", 'SIZE' AS "SIZE"))

db-fiddle

【问题讨论】:

在外部SELECT 子句中使用NVL() 有什么问题?无论如何,SELECT * 通常是一种不好的做法。明确命名列,并根据需要使用NVL() @mathguy,这很难看,因为您必须为枢轴返回的所有列重复 NVL 特定逻辑。如果你添加一个新的属性类型,你需要记住在最终选择中再添加一次。 抱歉,您的理由没有道理。你说你想对name 列使用N/D,对id 列使用-1,所以“占位符”是与列相关的。如果您添加一个新的“属性类型”(无论这意味着什么),您将必须为该列说明您想要的 null 的“占位符” - 您将在哪里执行此操作?甚至在知道您是否、何时以及添加什么“属性类型”之前?另一方面,如果您只想在 all 列中为 null 显示 N/D,那么在客户端程序中执行此操作要容易得多( SQL Developer、SQL*Plus 等) @mathguy 我不确定我是否理解您的评论。每个属性类型(例如国家/地区)都有一个 ID 和一个 NAME,它们是具有不同语义的不同列。当我旋转它们时,我希望将生成的 NULL 值转换为名称的 N/D 或 ID 的 -1(数据仓库设计有要求)。当给定 ID 的属性不存在时,我们如何应用此逻辑?在ID=2 的示例中,我们没有COLOR 属性。我天真地认为PIVOT (NVL(MAX(ATTR_NAME), 'N/D') AS NAME 会给出正确的结果,但这不起作用。 您的尝试很容易得到纠正。您所旋转的必须始终是一个聚合函数,而nvl 不是。诀窍是将nvl 移动到max 中(效率有点低,但它会起作用):pivot( max(nvl(attr_name, 'N/D'/)) for ...) 但这会将'N/D' 而不是null 放在every 列中输出。我的评论是,你似乎不想要那样。您希望 null 的替换因列而异。将来某个时候,您将添加一个新属性(它是旋转后的新列),并带有一个全新的 null 替换。您现在如何编写代码? 【参考方案1】:

您可以这样做:

生成所有attr_types 的列表(如果您有一个包含这些列表的表格,效果最好。您可以从源数据生成distinct 列表,但在大型数据集上这可能会很慢) 在 ID 上使用partitioned outer join 将数据外部连接到此。这将为您为上面列表中的每个 id 的每个属性提供一行 在子查询中根据需要将 null 名称/ID 转换为 N/A、-1 将输出传递给pivot

这给出了:

with src_data (id, attr_name, attr_id, attr_type) as (
  select 1, 'ITALY', 'IT', 'COUNTRY' FROM DUAL UNION ALL --
  select 1, 'GREEN', 'G', 'COLOR' FROM DUAL UNION ALL --
  select 1, 'BIG', 'B', 'SIZE' FROM DUAL UNION ALL --
  select 2, 'FRANCE', 'FR', 'COUNTRY' FROM DUAL UNION ALL --
  select 2, 'SMALL', 'S', 'SIZE' FROM DUAL  --
), attrs as (
  select distinct attr_type from src_data
), id_attrs as (
  select id, attr_type,
         nvl ( attr_name, 'N/A' ) attr_name,
         nvl ( attr_id, -1 ) attr_id
  from   attrs a
  left   join src_data d
    partition by ( id ) 
  using ( attr_type ) 
)
select * from id_attrs
pivot (
  max(attr_name) as name, max(attr_id) as id --
  for attr_type in (
    'COUNTRY' AS "COUNTRY", 'COLOR' AS "COLOR", 'SIZE' AS "SIZE"
  )
);

ID    COUNTRY_NAME   COUNTRY_ID   COLOR_NAME   COLOR_ID   SIZE_NAME   SIZE_ID   
    1 ITALY          IT           GREEN        G          BIG         B          
    2 FRANCE         FR           N/A          -1         SMALL       S    

【讨论】:

【参考方案2】:

我会使用 CASE WHEN 的聚合函数来做数据透视。

with src_data (id, attr_name, attr_id, attr_type) as (
    select 1, 'ITALY', 'IT', 'COUNTRY' FROM DUAL UNION ALL --
    select 1, 'GREEN', 'G', 'COLOR' FROM DUAL UNION ALL --
    select 1, 'BIG', 'B', 'SIZE' FROM DUAL UNION ALL --
    select 2, 'FRANCE', 'FR', 'COUNTRY' FROM DUAL UNION ALL --
    select 2, 'SMALL', 'S', 'SIZE' FROM DUAL  --
)
SELECT ID,
       NVL(MAX(CASE WHEN attr_type = 'COUNTRY' THEN attr_name  END),'N/A') "COUNTRY_NAME",
       NVL(MAX(CASE WHEN attr_type = 'COUNTRY' THEN attr_id END),'N/A') "COUNTRY_ID",
       NVL(MAX(CASE WHEN attr_type = 'COLOR' THEN attr_name END),'N/A') "COLOR_NAME",
       NVL(MAX(CASE WHEN attr_type = 'COLOR' THEN attr_id END),'N/A') "COLOR_ID",
       NVL(MAX(CASE WHEN attr_type = 'SIZE' THEN attr_name END),'N/A') "SIZE_NAME",
       NVL(MAX(CASE WHEN attr_type = 'SIZE' THEN attr_id END),'N/A') "SIZE_ID"
FROM src_data
GROUP BY ID

sqlfiddle

【讨论】:

我认为这种情况违反了规则在 PIVOT 生成的所有列上添加特定的 NVL 逻辑。我的真实案例有 14 个这样的专栏 OP 提到的。 有趣。所以你基本上用更重但更灵活的语法重新实现了pivot。原生pivot有什么可能的解决方案吗? @user103716 。 . .我投票认为这种语法比pivot 更简单——而且更灵活。 pivot 在我看来一直很神秘,除了不是 SQL 标准的一部分。 @user103716 - 这里的响应者没有“重新实现”枢轴。根据定义,透视是“条件聚合”,这个答案显示的是,在 Oracle 在 11.1 版中引入 pivot 运算符之前,透视始终是如何完成的,以及在任何其他数据库方言中是如何完成透视的。【参考方案3】:

如果您想要更具弹性的结构而不是手动编写多个 NVL()s,最好创建一个存储函数,该函数将返回 SYS_REFCURSOR 作为数据类型,例如

CREATE OR REPLACE FUNCTION Pivot_Src_Data RETURN SYS_REFCURSOR IS
  v_recordset SYS_REFCURSOR;
  v_sql       VARCHAR2(32767);
  v_cols1     VARCHAR2(32767);
  v_cols2     VARCHAR2(32767);  
BEGIN
  SELECT LISTAGG( 'NVL('||attr_type||'_'||typ||',''N/D'') AS "'||attr_type||'_'||typ||'"' , ',' )
          WITHIN GROUP ( ORDER BY attr_type ) 
    INTO v_cols1
    FROM ( SELECT DISTINCT attr_type, typ 
             FROM src_data
            CROSS JOIN (SELECT 'ID' AS typ FROM dual UNION ALL SELECT 'NAME' FROM dual) );

  SELECT LISTAGG( ''''||attr_type||''' AS "'||attr_type||'"' , ',' )
          WITHIN GROUP ( ORDER BY attr_type )
    INTO v_cols2
    FROM ( SELECT DISTINCT attr_type FROM src_data );
                
  v_sql := 'SELECT NVL(ID,-1) AS ID, '|| v_cols1 ||
           '  FROM src_data s
             PIVOT (MAX(attr_name) AS name, MAX(attr_id) AS id 
               FOR attr_type IN ('|| v_cols2 ||'))';

  OPEN v_recordset FOR v_sql;
  RETURN v_recordset;
END;
/

将从SQL Developer的控制台调用

SQL> DECLARE
    res SYS_REFCURSOR;
BEGIN
   :res := Pivot_Src_Data;
END;
/

SQL> PRINT res;

这样,无论您向表中添加了多少新属性类型,您都会在透视结果集中看到所有这些属性类型,除非作为 LISTAGG() 函数的参数呈现的字符串长度超过函数的阈值,4000

【讨论】:

【参考方案4】:

MAX(NVL()) 语法实际上似乎提供了正确的结果,即使对于没有相应行的列也可以替换空值。

https://dbfiddle.uk/?rdbms=oracle_18&fiddle=34a2036988e05da6d9ddcc46c41fdef8

【讨论】:

以上是关于当行丢失时,用 Oracle SQL PIVOT 结果中的自定义值替换 NULL的主要内容,如果未能解决你的问题,请参考以下文章

Oracle SQL - 在 PIVOT 之前更新值

Oracle SQL - 如何使用 PIVOT 进行计数

Oracle sql 中的 PIVOT

在 Oracle SQL 中按报告日期的 PIVOT 值

当行数大于 1.000.000 时,Oracle APEX 呈现数据的速度变慢

Oracle SQL Developer:如何使用 PIVOT 函数将行转置为列