当行丢失时,用 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
无法做到这一点。我能做的最好的就是用外连接添加缺失的行,并在外连接返回的真实NULL
s 上添加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的主要内容,如果未能解决你的问题,请参考以下文章