【问题标题】:Oracle - Table function that returns dynamic column namesOracle - 返回动态列名的表函数
【发布时间】:2020-12-15 05:51:12
【问题描述】:

我正在构建一个报告,该报告应根据请求的年份(即 2000 年到 2002 年)生成动态列列表。用户应该能够通过 SELECT 语句运行报告,如下所示:

SELECT * FROM TABLE(myreport(2002, 3)); -- Get 3 report years starting with 2002

目前,我有一个生成此输出的表函数,但具有静态列名:

-- Row type
CREATE OR REPLACE TYPE typ_myreport AS OBJECT (
  myuser VARCHAR(260),
  yr1_p DECIMAL(5,2),
  yr1_t INTEGER,
  yr2_p DECIMAL(5,2),
  yr2_t INTEGER,
  ...
);

-- Table type
CREATE OR REPLACE TYPE tab_myreport AS TABLE OF typ_myreport;

-- Report
CREATE OR REPLACE FUNCTION myreport(pAsOfYear IN INTEGER) 
RETURN tab_myreport PIPELINED AS
BEGIN
  FOR vRec IN (
    SELECT user_name, yr1_p, yr1_t, y2_p, y2_t, ...
    FROM mytable
  ) 
  LOOP
    PIPE ROW (
      typ_myreport(vRec.user_name, vRec.yr1_p, vRec.yr1_t, vRec.yr2_p, vRec.year2_t, ...)
    );
  END LOOP;

  RETURN;
END;

它产生这样的输出:

User    yr1_p   yr1_t   yr2_p   yr2_t   yr3_p   yr3_t
-----------------------------------------------------
Bobby      25       2      33       2      20       4
Barry      50       4      66       4      50      10
Big Ben    25       2       0       0      30       6

但我希望根据请求的年份动态命名列:

User    2000_p  2000_t  2001_p  2001_t  2002_p  2002_t
------------------------------------------------------
Bobby       25       2      33       2      20       4
Barry       50       4      66       4      50      10
Big Ben     25       2       0       0      30       6

输出结果集应该保持完全相同——表结构、数据类型——只是列名应该改变。我的想法是通过动态构建的SELECT 语句来执行此操作,我可以执行该语句然后返回结果。我摸索着试图让它工作,但没有运气。

有没有办法在具有动态列数和列名的函数/过程中编写动态 SQL,然后将该结果集作为 (不是脚本)输出?

更新
这是生成 yr 字段的实际查询的简化 sn-p:

WITH rpt_years AS (SELECT * FROM TABLE(GetReportyears(pAsOfYear)))
SELECT user_name,
  SUM(year1_cnt) AS yr1_t,
  SUM(COALESCE((year1_cnt / NULLIF(year1_tot,0)),0)) AS yr1_p,
  SUM(year2_cnt) AS yr2_t, 
  SUM(COALESCE((year2_cnt / NULLIF(year2_tot,0)),0)) AS yr2_p,
  ...
FROM (
  SELECT DISTINCT 
    ui.user_name, 
    SUM(CASE WHEN s.dist_year = y.year1 THEN 1 END) AS year1_cnt,
    COUNT(CASE WHEN s.dist_year = y.year1 THEN 1 END) AS year1_tot,
    SUM(CASE WHEN s.dist_year = y.year2 THEN 1 END) AS year2_cnt,
    COUNT(CASE WHEN s.dist_year = y.year2 THEN 1 END) AS year2_tot,
    ...
  FROM rpt_years y -- Get report years as single row
  INNER JOIN src_table s
  ...
) src
ORDER BY src.user_name

【问题讨论】:

  • 我真的不知道为什么不在select 语句中使用别名,而是尝试在您的函数中添加alter table
  • 什么程序会运行报告?如果它是一个理解 ref cursors 的应用程序,您可以创建一个返回动态生成的 sys_refcursor 的函数。
  • 开发人员需要运行内部报告,因此 IntelliJ 或 Eclipse 的内置 SQL 编辑器或可能是 Oracle SQL Developer。
  • @ravioli 这可以做到,但你的表结构到底是什么?该表是否已经包含名称如 yr1_p、yr1_t 且始终与年份一致的列,或者这些列是否真的从其他来源聚合或转置?

标签: oracle dynamic-sql dynamic-columns table-functions


【解决方案1】:

您可以使用开源程序 Method4 在 SQL 上下文中返回动态列数。

要准确地构建您想要的,所有内容都在一行代码中,需要创建一个新类型。由于这些类型很复杂,我建议将该任务保存以备后用。第一步,如果只使用现有的类型,只需要编写一条生成正确SQL语句的SQL语句即可。

例如,让我们使用此表来表示您现有的大型查询的结果:

create table MyTable as
select 'Bobby'   user_name, 25 yr1_p, 2 yr1_t, 33 yr2_p, 2 yr2_t, 20 yr3_p, 4  yr3_t from dual union all
select 'Barry'   user_name, 50 yr1_p, 4 yr1_t, 66 yr2_p, 4 yr2_t, 50 yr3_p, 10 yr3_t from dual union all
select 'Big Ben' user_name, 25 yr1_p, 2 yr1_t,  0 yr2_p, 0 yr2_t, 30 yr3_p,  6 yr3_t from dual;

下面的静态查询会将列名转换成你想要的格式:

select user_name, yr1_p "2000_P", yr1_t "2000_T", yr2_p "2001_P", yr2_t "2001_T", yr3_p "2002_P", yr3_t "2002_T"
from myTable;

您可以使用以下查询生成上述查询:

select
    'select user_name, ' ||
    listagg('yr'||level||'_p "'||(start_year + level - 1)||'_P", yr'||level||'_t "'||(start_year + level - 1)||'_T"', ', ') within group (order by start_year) || 
    ' from myTable' v_sql
from
(
    select 2000 start_year, 3 number_of_years from dual
)
connect by level <= number_of_years;

下一个查询将它们放在一起。安装 Method4 后,DYNAMIC_QUERY 函数可以运行另一个查询生成的查询,生成您想要的结果和列名。您只需更改值 2000 和 3 即可调整结果。

select * from table(method4.dynamic_query(q'[
    select
        'select user_name, ' ||
        listagg('yr'||level||'_p "'||(start_year + level - 1)||'_P", yr'||level||'_t "'||(start_year + level - 1)||'_T"', ', ') within group (order by start_year) || 
        ' from myTable' v_sql
    from
    (
        --Only change the values in here:
        select 2000 start_year, 3 number_of_years from dual
    )
    connect by level <= number_of_years
]'));

您还需要将myTable 替换为生成中间结果的查询。这并不是您想要的——尽管用户只需要更改两个数字,但他们必须复制并粘贴一个庞大的查询。

如果您无法忍受复制和粘贴,还有一种方法可以创建只需要一行 SQL 语句的解决方案。对于该解决方案,您需要创建自己的类型并将其添加到包规范中。大部分工作只是复制文件 method4_dynamic_ot.tpb 和 method4_dynamic_ot.tps,更改名称和参数,修改函数 re_evaluate_statement,然后将新类型添加到包规范中。我可以提供帮助,但在使用该高级解决方案之前,您应该检查以确保 DYAMIC_QUERY 方法首先有效。

【讨论】:

  • 谢谢乔恩,我会玩这个。要生成报告,我需要运行 select * from table(method4.dynamic_query(q'... 语句吗?或者这可以嵌入到函数/过程中,然后返回结果?理想情况下,我想让最终用户尽可能保持干净和简单,所以他们需要做的就是运行一个简单的函数调用——即SELECT * FROM TABLE(myreport(2002, 3))
  • @ravioli 查询可以嵌入到函数中,但它需要我的答案最后一段中描述的包和类型更改。您不妨先尝试构建method4.dynamic_query 版本,因为稍后您将需要相同的逻辑将查询嵌入到函数中。
  • 好的。我正在与我们的 DBA 核实是否可以安装它。我的感觉是这将是不可能的,因为他们对安装外部软件包非常谨慎,但手指交叉。
猜你喜欢
  • 1970-01-01
  • 2017-05-29
  • 2012-09-02
  • 1970-01-01
  • 2020-10-22
  • 1970-01-01
  • 2018-04-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多