oracle sql中的动态数据透视

Posted

技术标签:

【中文标题】oracle sql中的动态数据透视【英文标题】:Dynamic pivot in oracle sql 【发布时间】:2022-01-18 19:56:52 【问题描述】:

...枢轴(总和(A)为(X)中的B)

现在 B 的数据类型为 varchar2,X 是由逗号分隔的 varchar2 值的字符串。 X 的值是从同一表的列(例如 CL)中选择不同的值。这种方式枢轴查询工作。

但问题是,每当 CL 列中有新值时,我必须手动将其添加到字符串 X 中。

我尝试用从 CL 中选择的不同值替换 X。但是查询没有运行。 我觉得的原因是因为要替换 X,我们需要用逗号分隔的值。 然后我创建了一个函数来返回与字符串 X 匹配的精确输出。但查询仍然没有运行。 显示的错误消息如“缺少正确的括号”、“文件通信通道结束”等。 我尝试了枢轴 xml 而不是枢轴,查询运行但给出了像 oraxxx 等根本没有值的值。

也许我没有正确使用它。 您能告诉我一些创建具有动态值的数据透视表的方法吗?

【问题讨论】:

【参考方案1】:

您不能在不使用 PIVOT XML 的情况下将动态语句放入 PIVOT 的 IN 语句中,它会输出一些不太理想的输出。但是,您可以创建一个 IN 字符串并将其输入到您的语句中。

首先,这是我的示例表;

  myNumber    myValue myLetter
---------- ---------- --------
         1          2 A        
         1          4 B        
         2          6 C        
         2          8 A        
         2         10 B        
         3         12 C        
         3         14 A      

首先设置要在 IN 语句中使用的字符串。在这里,您将字符串放入“str_in_statement”。我们使用COLUMN NEW_VALUE 和LISTAGG 来设置字符串。

clear columns
COLUMN temp_in_statement new_value str_in_statement
SELECT DISTINCT 
    LISTAGG('''' || myLetter || ''' AS ' || myLetter,',')
        WITHIN GROUP (ORDER BY myLetter) AS temp_in_statement 
    FROM (SELECT DISTINCT myLetter FROM myTable);

您的字符串将如下所示:

'A' AS A,'B' AS B,'C' AS C

现在在您的 PIVOT 查询中使用 String 语句。

SELECT * FROM 
    (SELECT myNumber, myLetter, myValue FROM myTable)
    PIVOT (Sum(myValue) AS val FOR myLetter IN (&str_in_statement));

这是输出:

  MYNUMBER      A_VAL      B_VAL      C_VAL
---------- ---------- ---------- ----------
         1          2          4            
         2          8         10          6 
         3         14                    12 

但有一些限制。您最多只能连接 4000 个字节的字符串。

【讨论】:

在尝试此操作时,我遇到了 oracle 错误:ORA-56900:pivot|unpivot 操作中不支持绑定变量 如何在Oracle程序中实现这个?请举个例子 如何在没有变量的情况下将第一个选择查询放在 IN 子句中?【参考方案2】:

您不能在枢轴子句的IN 子句中放置非常量字符串。 您可以为此使用 Pivot XML。

来自documentation:

子查询 子查询仅与 XML 关键字结合使用。 指定子查询时,将使用子查询找到的所有值 用于旋转

应该是这样的:

select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in(any)
) t;

您还可以使用子查询来代替 ANY 关键字:

select xmlserialize(content t.B_XML) from t_aa
pivot xml(
sum(A) for B in (select cl from t_bb)
) t;

Here is a sqlfiddle demo

【讨论】:

您好,您的方法实际上是有效的,但我得到的是 xml 格式的输出。我可以将输出作为包含行和列的表格吗? AFAIK,不是动态的...但是您打算如何使用您不知道其结构的结果? 我希望将它用于另一个动态 SQL,但您对了解结构很有好处。【参考方案3】:

对于以后的读者,这里是另一种解决方案 https://technology.amis.nl/2006/05/24/dynamic-sql-pivoting-stealing-antons-thunder/

允许类似的查询

select * from table( pivot(  'select deptno,  job, count(*) c from scott.emp group by deptno,job' ) )

【讨论】:

此解决方案是否适用于 Oracle 11g?因为我无法以这种格式执行子查询。 鉴于所涉及的日期,是的,应该。那时还没有 12c。仔细检查权限。【参考方案4】:

使用动态查询

测试代码如下


--  DDL for Table TMP_TEST
--------------------------------------------------------

  CREATE TABLE "TMP_TEST" 
   (    "NAME" VARCHAR2(20), 
    "APP" VARCHAR2(20)
   );
/
SET DEFINE OFF;
Insert into TMP_TEST (NAME,APP) values ('suhaib','2');
Insert into TMP_TEST (NAME,APP) values ('suhaib','1');
Insert into TMP_TEST (NAME,APP) values ('shahzad','3');
Insert into TMP_TEST (NAME,APP) values ('shahzad','2');
Insert into TMP_TEST (NAME,APP) values ('shahzad','5');
Insert into TMP_TEST (NAME,APP) values ('tariq','1');
Insert into TMP_TEST (NAME,APP) values ('tariq','2');
Insert into TMP_TEST (NAME,APP) values ('tariq','6');
Insert into TMP_TEST (NAME,APP) values ('tariq','4');
/
  CREATE TABLE "TMP_TESTAPP" 
   (    "APP" VARCHAR2(20)
   );

SET DEFINE OFF;
Insert into TMP_TESTAPP (APP) values ('1');
Insert into TMP_TESTAPP (APP) values ('2');
Insert into TMP_TESTAPP (APP) values ('3');
Insert into TMP_TESTAPP (APP) values ('4');
Insert into TMP_TESTAPP (APP) values ('5');
Insert into TMP_TESTAPP (APP) values ('6');
/
create or replace PROCEDURE temp_test(
  pcursor out sys_refcursor,
    PRESULT                   OUT VARCHAR2
    )
AS
V_VALUES VARCHAR2(4000);
V_QUERY VARCHAR2(4000);
BEGIN
 PRESULT := 'Nothing';

-- concating activities name using comma, replace "'" with "''" because we will use it in dynamic query so "'" can effect query.
  SELECT DISTINCT 
         LISTAGG('''' || REPLACE(APP,'''','''''') || '''',',')
         WITHIN GROUP (ORDER BY APP) AS temp_in_statement 
    INTO V_VALUES
    FROM (SELECT DISTINCT APP 
            FROM TMP_TESTAPP);

-- designing dynamic query  

  V_QUERY := 'select * 
                from (  select NAME,APP 
                          from TMP_TEST   )   
               pivot (count(*) for APP in 
                     (' ||V_VALUES|| '))  
           order  by NAME' ;

    OPEN PCURSOR
     FOR V_QUERY;


 PRESULT := 'Success';

Exception
WHEN OTHERS THEN
 PRESULT := SQLcode || ' - ' || SQLERRM;
END temp_test;

【讨论】:

【参考方案5】:

我使用了上述方法(Anton PL/SQL 自定义函数 pivot()),它完成了工作!由于我不是专业的 Oracle 开发人员,因此我已经完成了以下简单步骤:

1) 下载 zip 包以在其中找到 pivotFun.sql。 2) 运行一次pivotFun.sql 以创建一个新函数 3) 在普通SQL中使用该函数。

请注意动态列名。在我的环境中,我发现列名限制为 30 个字符,并且不能包含单引号。所以,我的查询现在是这样的:

SELECT 
  *
FROM   
  table( 
        pivot('
                SELECT DISTINCT
                    P.proj_id,
                    REPLACE(substr(T.UDF_TYPE_LABEL, 1, 30), '''''''','','') as Attribute,
                    CASE
                      WHEN V.udf_text is null     and V.udf_date is null and      V.udf_number is NOT null  THEN to_char(V.udf_number)
                      WHEN V.udf_text is null     and V.udf_date is NOT null and  V.udf_number is null      THEN to_char(V.udf_date)
                      WHEN V.udf_text is NOT null and V.udf_date is null and      V.udf_number is null      THEN V.udf_text
                      ELSE NULL END
                    AS VALUE
                FROM
                    project   P
                LEFT JOIN UDFVALUE V ON P.proj_id     = V.proj_id 
                LEFT JOIN UDFTYPE  T ON V.UDF_TYPE_ID = T.UDF_TYPE_ID
                WHERE 
                    P.delete_session_id  IS NULL AND
                    T.TABLE_NAME = ''PROJECT''
    ')
)

适用于多达 100 万条记录。

【讨论】:

【参考方案6】:

对于 OP 提出的问题,我不会给出确切的答案,相反,我将仅描述如何完成动态枢轴。

这里我们必须使用动态 sql,首先将列值检索到一个变量中,然后在动态 sql 中传递该变量。

示例

假设我们有一个如下表。

如果我们需要将YR 列中的值显示为列名,并将这些列中的值显示为QTY,那么我们可以使用下面的代码。

declare
  sqlqry clob;
  cols clob;
begin
  select listagg('''' || YR || ''' as "' || YR || '"', ',') within group (order by YR)
  into   cols
  from   (select distinct YR from EMPLOYEE);


  sqlqry :=
  '      
  select * from
  (
      select *
      from EMPLOYEE
  )
  pivot
  (
    MIN(QTY) for YR in (' || cols  || ')
  )';

  execute immediate sqlqry;
end;
/

结果

如果需要,您还可以创建一个临时表并在该临时表中执行选择查询以查看结果。很简单,只需在上面的代码中添加CREATE TABLE TABLENAME AS即可。

sqlqry :=
'    
  CREATE TABLE TABLENAME AS
  select * from

【讨论】:

错误报告 - ORA-06550:第 5 行,第 74 列:PL/SQL:ORA-00923:在预期的位置找不到 FROM 关键字 ORA-06550:第 5 行,第 3 列:PL/SQL:SQL语句忽略 06550。00000 - “行 %s,列 %s:\n%s” *原因:通常是 PL/SQL 编译错误。 *行动:【参考方案7】:

你不能在不使用PIVOT XML的情况下在PIVOT的IN语句中放入动态语句,但是你可以使用small Technic来使用动态语句在枢轴。在 PL/SQL 中,在一个字符串值内,两个撇号等于一个撇号。

declare
  sqlqry clob;   
  search_ids  varchar(256) := '''2016'',''2017'',''2018'',''2019''';
begin
  search_ids := concat( search_ids,'''2020''' ); -- you can append new search id dynamically as you wanted
  sqlqry :=
  '      
  select * from
  (
      select *
      from EMPLOYEE
  )
  pivot
  (
    MIN(QTY) for YR in (' || search_ids   || ')
  )';

  execute immediate sqlqry;
end;

【讨论】:

【参考方案8】:

在 Oracle 的 SQL 中没有直接的动态透视方法,除非它返回 XML 类型的结果。 对于非 XML 结果,可以通过创建 SYS_REFCURSOR 返回类型的函数来使用 PL/SQL

使用条件聚合

CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
  v_recordset SYS_REFCURSOR;
  v_sql       VARCHAR2(32767);
  v_cols      VARCHAR2(32767); 
BEGIN
  SELECT LISTAGG( 'SUM(  CASE  WHEN job_title =  '''||job_title||'''  THEN  1  ELSE  0  END )  AS  "'||job_title||'"' , ',' )
                 WITHIN GROUP ( ORDER BY job_title )
    INTO v_cols
    FROM ( SELECT DISTINCT job_title
             FROM jobs j );

  v_sql :=
  'SELECT "HIRE YEAR",'|| v_cols ||
  '  FROM
     (
      SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
        FROM employees e
        JOIN jobs j
          ON j.job_id = e.job_id
     )
    GROUP BY "HIRE YEAR"
    ORDER BY "HIRE YEAR"';

  OPEN v_recordset FOR v_sql;
  DBMS_OUTPUT.PUT_LINE(v_sql);
  RETURN v_recordset;
END;
/

带有PIVOT子句

CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
  v_recordset SYS_REFCURSOR;
  v_sql       VARCHAR2(32767);
  v_cols      VARCHAR2(32767);
BEGIN
  SELECT LISTAGG( ''''||job_title||''' AS "'||job_title||'"' , ',' )
                 WITHIN GROUP ( ORDER BY job_title )
    INTO v_cols
    FROM ( SELECT DISTINCT job_title
             FROM jobs j  );

  v_sql :=
  'SELECT *
     FROM
     (
      SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
        FROM employees e
        JOIN jobs j
          ON j.job_id = e.job_id  
     )
    PIVOT
    (
     COUNT(*) FOR job_title IN ( '|| v_cols ||' )
    )
    ORDER BY "HIRE YEAR"';

  OPEN v_recordset FOR v_sql;
  DBMS_OUTPUT.PUT_LINE(v_sql);
  RETURN v_recordset;
END;
/

但是LISTAGG() 有一个缺点,编码为 ORA-01489: 字符串连接的结果太长 每当连接的字符串时都会引发第一个参数内的长度超过 4000 个字符。在这种情况下,返回v_cols 变量值的查询可能会替换为嵌套在XMLAGG() 中的XMLELEMENT() 函数,例如

CREATE OR REPLACE FUNCTION Get_Jobs_ByYear RETURN SYS_REFCURSOR IS
  v_recordset SYS_REFCURSOR;
  v_sql       VARCHAR2(32767);
  v_cols      VARCHAR2(32767); 
BEGIN
  SELECT RTRIM(DBMS_XMLGEN.CONVERT(
         XMLAGG(
                XMLELEMENT(e, 'SUM(  CASE  WHEN job_title =  '''||job_title||
                            '''  THEN  1  ELSE  0  END )  AS  "'||job_title||'",')
               ).EXTRACT('//text()').GETCLOBVAL() ,1),',') AS "v_cols"
    FROM ( SELECT DISTINCT job_title
               FROM jobs j);
           
  v_sql :=
  'SELECT "HIRE YEAR",'|| v_cols ||
  '  FROM
     (
      SELECT TO_NUMBER(TO_CHAR(hire_date,''YYYY'')) AS "HIRE YEAR", job_title
        FROM employees e
        JOIN jobs j
          ON j.job_id = e.job_id
     )
    GROUP BY "HIRE YEAR"
    ORDER BY "HIRE YEAR"';
  DBMS_OUTPUT.put_line(LENGTH(v_sql));
  OPEN v_recordset FOR v_sql;
  RETURN v_recordset;
END;
/

除非超出 VARCHAR2 类型的上限 32767。最后一种方法也可能适用于版本 Oracle 11g 第 2 版的数据库,因为它们不包含 LISTAGG() 函数。

顺便说一句,LISTAGG() 函数可以在签出v_cols 期间使用,即使在字符串的尾随部分被截断时生成的非常长的连接字符串也不会出现 ORA-01489 错误如果数据库的版本是 12.2+ 例如

,则通过使用 ON OVERFLOW TRUNCATE 子句
LISTAGG( <concatenated string>,',' ON OVERFLOW TRUNCATE 'THE REST IS TRUNCATED' WITHOUT COUNT )

函数可以调用为

VAR rc REFCURSOR
EXEC :rc := Get_Jobs_ByYear;
PRINT rc

来自 SQL Developer 的命令行

BEGIN
  :result := Get_Jobs_ByYear;
END;

PL/SQL DeveloperTest 窗口中获取结果 设置。

Demo for generated queries

【讨论】:

【参考方案9】:

您可以使用开源程序Method4.Pivot 在单个 SQL 语句中动态透视数据。

安装包后,调用该函数并以字符串形式传入一条SQL语句。 SQL 语句的最后一列定义值,倒数第二列定义列名。默认聚合函数是 MAX,它适用于常见的实体-属性-值查询,例如:

select * from table(method4.pivot(
    q'[
        select 'A' name, 1 value from dual union all
        select 'B' name, 2 value from dual union all
        select 'C' name, 3 value from dual
    ]'
));

A   B   C
-   -   -
1   2   3

该程序还通过参数 P_AGGREGATE_FUNCTION 支持不同的聚合函数,如果添加名为 PIVOT_COLUMN_ID 的列,则允许自定义列名顺序。

该包使用类似于 Anton 的 pivot 的 Oracle Data Cartridge 方法,但 Method4.Pivot 有几个重要优点:

    具有存储库、安装说明、许可证、单元测试、文档和 cmets 的常规开源程序 - 而不仅仅是博客上的 Zip 文件。 处理不寻常的列名。 处理不寻常的数据类型,例如浮点数。 最多可处理 1000 列。 为常见错误提供有意义的错误消息。 处理 NULL 列名。 处理 128 个字符的列名。 防止误导性的隐式转换。 每次都硬解析语句以捕获基础表更改。

但大多数用户最好还是在应用层创建动态数据透视或使用数据透视 XML 选项。

【讨论】:

【参考方案10】:

自从 Oracle 19c 引入 SQL_MACRO(可能还有 Polymorphic Table Functions,我还没有使用)以来,似乎无需额外的开发工作就可以实现。

create table t as
select
  trunc(level/5) as id
  , chr(65+mod(level, 5)) as code
  , level as val
from dual
connect by level < 10
create function f_pivot
return varchar2 SQL_MACRO(TABLE)
is
  l_codes varchar2(1000);
begin
  select listagg(
    distinct '''' || code
    || ''' as ' || code, ',')
    into l_codes
  from t;
  
  return
    'select *
    from t
    pivot (
      max(val) for code in (
      ' || l_codes || '))';
end;
/
select *
from f_pivot()
身份证 |乙| C | D | E |一种 -: | -: | -: | -: | -: | ---: 0 | 1 | 2 | 3 | 4 | 1 | 6 | 7 | 8 | 9 | 5

唯一的问题(在SQL_MACRO 方法的情况下)是结果集在一个会话期间不会改变其结构:

insert into t
values(1, 'Q', 100);

commit;

select *
from f_pivot()
身份证 |乙| C | D | E |一种 -: | -: | -: | -: | -: | ---: 0 | 1 | 2 | 3 | 4 | 1 | 6 | 7 | 8 | 9 | 5

但在单独的会话中它工作正常:

select dbms_xmlgen.getxml('select * from f_pivot()') as v
from dual
V
<?xml version="1.0"?><ROWSET> <ROW>  <ID>0</ID>  <B>1</B>  <C>2</C>  <D>3</D>  <E>4</E> </ROW> <ROW>  <ID>1</ID>  <B>6</B>  <C>7</C>  <D>8</D>  <E>9</E>  <A>5</A><Q>100</Q> </ROW></ROWSET>

使用with function 功能动态枢轴可以在没有预定义功能的情况下就地使用:

with function f_pivot1
return varchar2 SQL_MACRO(TABLE)
is
  l_codes varchar2(1000);
begin
  select listagg(distinct '''' || code || ''' as ' || code, ',')
    into l_codes
  from t;
  
  return
    'select *
    from t
    pivot (
      max(val) for code in (
      ' || l_codes || '))';
end;

select *
from f_pivot1()
身份证 |乙| C | D | E |一个 |问 -: | -: | -: | -: | -: | ---: | ---: 0 | 1 | 2 | 3 | 4 | | 1 | 6 | 7 | 8 | 9 | 5 | 100

db小提琴here

【讨论】:

以上是关于oracle sql中的动态数据透视的主要内容,如果未能解决你的问题,请参考以下文章

使用 Sql Developer Oracle 的动态数据透视查询

SQL - Oracle - 具有动态数据的数据透视表

Oracle 组和数据透视 - Oracle 中的动态数据透视

用于动态 sql 透视的 Oracle 函数

SQL Server 数据库中的动态数据透视

MS SQL Server 中的动态数据透视