通过基于传入的数组进行过滤,允许来自 plpgsql 函数的动态结果集?

Wil*_*ard 5 postgresql aggregate plpgsql array postgresql-10

我想我已经避免了这里的 XY 问题,因为我正在为真正的潜在问题(以动态方式总结多个表)列出我的解决方案,而且我只询问我卡住的最后一个部分。因此,首先有一些背景知识。我提供了一个最小的示例数据集,以及用于以我描述的方式汇总数据的工作代码。


考虑如下设置:

create temp table tbl1 (id int primary key, category text, passing boolean);

insert into tbl1 values
(1, 'A', 't'),
(2, 'A', 't'),
(3, 'A', 't'),
(4, 'A', 'f'),
(5, 'B', 't'),
(6, 'B', 'f'),
(7, 'C', 't'),
(8, 'C', 't'),
(9, 'C', 'f'),
(10, 'C', 'f'),
(11, 'C', 'f'),
(12, 'C', 'f'),
(13, 'B', 't'),
(14, 'B', 'f'),
(15, 'B', 't'),
(16, 'B', 'f'),
(17, 'B', 't'),
(18, 'B', 'f'),
(19, 'B', 't'),
(20, 'B', 'f');
Run Code Online (Sandbox Code Playgroud)

然后我可以生成以下摘要:

postgres=> select category, passing, count(*) from tbl1 group by category, passing order by category, passing;
 category | passing | count
----------+---------+-------
 A        | f       |     1
 A        | t       |     3
 B        | f       |     5
 B        | t       |     5
 C        | f       |     4
 C        | t       |     2
(6 rows)
Run Code Online (Sandbox Code Playgroud)

但是,我有多个要汇总的此类表(均使用相同的类别 A、B、C),因此我希望显示的最终结果只需要一行来汇总一个表,如下所示:

 Table Name | Overall passing rate | A passing rate | B passing rate | C passing rate
------------+----------------------+----------------+----------------+----------------
 tbl1       | 50% (10/20)          | 75% (3/4)      | 50% (5/10)     | 33% (2/6)
Run Code Online (Sandbox Code Playgroud)

有时我还需要能够过滤,例如只返回有关类别 A 和 B 的信息而忽略 C,如下所示:

 Table Name | Overall passing rate | A passing rate | B passing rate
------------+----------------------+----------------+----------------
 tbl1       | 57% (8/14)           | 75% (3/4)      | 50% (5/10)
Run Code Online (Sandbox Code Playgroud)

我可以使用count(*) filter (where...)有点笨拙的 CTE 中的语法通过查询生成上面显示的第一个输出,如下所示:

with tallies as (
select
count(*) filter (where category in ('A', 'B', 'C') and passing) as abc_pass,
count(*) filter (where category in ('A', 'B', 'C')) as abc_all,
count(*) filter (where category = 'A' and passing) as a_pass,
count(*) filter (where category = 'A') as a_all,
count(*) filter (where category = 'B' and passing) as b_pass,
count(*) filter (where category = 'B') as b_all,
count(*) filter (where category = 'C' and passing) as c_pass,
count(*) filter (where category = 'C') as c_all
from tbl1
)
select 'tbl1' as "Table Name",
format('%s%% (%s/%s)', 100*abc_pass/abc_all, abc_pass, abc_all) as "Overall passing rate",
format('%s%% (%s/%s)', 100*a_pass/a_all, a_pass, a_all) as "A passing rate",
format('%s%% (%s/%s)', 100*b_pass/b_all, b_pass, b_all) as "B passing rate",
format('%s%% (%s/%s)', 100*c_pass/c_all, c_pass, c_all) as "C passing rate"
from tallies;
Run Code Online (Sandbox Code Playgroud)

我可以修改它以毫不费力地省略类别 C,以生成上面的第二个示例输出。(这里没有显示,因为它主要是重复的。)

问题是,有这么多表格要汇总(实际上是视图,而不是表格,但这无关紧要)并且要求我能够轻松地临时汇总任何表格组,并随意包含或省略类别(例如“汇总 tbl1、tbl2 和 tbl3,但仅汇总类别 B 和 C”,或“仅汇总所有表的类别 B”)上述 SQL 不够灵活。

我可以使用接受任意数量的 type 参数的 plpgsql 函数来完成“临时汇总任何一组表”的要求name,并将我想要汇总的所有表的名称提供给它,如下所示:

create function summarize_tables(variadic tbls name[])
returns table ("Table Name" text, "Overall pass rate" text, "A passing rate" text, "B passing rate" text, "C passing rate" text)
language plpgsql
as $funcdef$
declare
  tbl name;
begin
  foreach tbl in array tbls
  loop
    return query execute
      format(
        $query$
          with tallies as (
            select
              count(*) filter (where category in ('A', 'B', 'C') and passing) as abc_pass,
              count(*) filter (where category in ('A', 'B', 'C')) as abc_all,
              count(*) filter (where category = 'A' and passing) as a_pass,
              count(*) filter (where category = 'A') as a_all,
              count(*) filter (where category = 'B' and passing) as b_pass,
              count(*) filter (where category = 'B') as b_all,
              count(*) filter (where category = 'C' and passing) as c_pass,
              count(*) filter (where category = 'C') as c_all
            from %I
          )
          select
            %L as "Table Name",
            format('%%s%%%% (%%s/%%s)', 100*abc_pass/abc_all, abc_pass, abc_all) as "Overall passing rate",
            format('%%s%%%% (%%s/%%s)', 100*a_pass/a_all, a_pass, a_all) as "A passing rate",
            format('%%s%%%% (%%s/%%s)', 100*b_pass/b_all, b_pass, b_all) as "B passing rate",
            format('%%s%%%% (%%s/%%s)', 100*c_pass/c_all, c_pass, c_all) as "C passing rate"
          from tallies;
        $query$,
        tbl,
        tbl
      );
  end loop;
  return;
end
$funcdef$
;
Run Code Online (Sandbox Code Playgroud)

可以调用它select * from summarize_tables('tbl1');来汇总上面的示例数据集,或select * from summarize_tables('tbl1', 'tbl2');汇总其他表。

但是,这根本无法满足第二个要求——我能够计算不同的结果列以任意包含或排除 A、B 或 C。

我想也许有一种方法可以使用如下所示的函数签名来做到这一点:

create function summarize_tables(categories text[], variadic tbls name[])
Run Code Online (Sandbox Code Playgroud)

然后像这样调用它:

select * from summarize_tables('{A,B}', 'tbl1', 'tbl2');
Run Code Online (Sandbox Code Playgroud)

但我不知道如何在我的 SQL 中使用“类别”数组。这甚至可能根据传入的类别以这样的过滤方式总结结果吗?


在相关说明中,我找到了/sf/answers/822609021/所以我知道如果我想要返回真正的动态列,我将不得不使用returns setof record 并且必须指定每次调用该函数时要返回的列的全名和类型。如果有解决方法,我会对此感兴趣。

可能这两个因素的组合意味着我应该接受我必须为我想要总结的类别 A、B 和 C 的每个组合拥有一个单独的函数 - 总共七个函数。

但在这种情况下,如果以后添加类别 D 和类别 E,我就倒霉了!

这可能组合让我觉得它可能是值得的,必须指定每次我调用该函数返回列名和类型,作为代价,只需要有一个单一的功能。换句话说,returns table (...)将函数定义中的returns setof record更改为,然后将调用更改select * from summarize_tables(...);为:

select * from summarize_tables('{A,C,D}', ...)
as x ("Table Name" text, "Overall pass rate" text, "A passing rate" text, "C passing rate" text, "D passing rate" text)
;
Run Code Online (Sandbox Code Playgroud)

然而,这种权衡甚至是不可能的,除非有一种方法可以使过滤比当前 CTE 中的更动态 - 即一种利用categories text[]传入参数的方法。 就是我的问题。

(不过,也欢迎提出有关上述设计的任何指示。)

出于这个问题的目的,我省略了对空“传递”值的处理,这将通过将“传递的地方”更改为“传递的地方为真”来处理 - 并省略了大小写切换以避免在某些特定情况下除以零错误表不包含特定类别。

Lau*_*lbe 0

你把问题描述得很好。我无法给出完整的解决方案,但我可以给你一些想法。

正如您所发现的,结果列数可变的函数无法按照您想要的方式完成;如果将其定义为RETURNS SETOF record,则必须在函数的每个查询中指定实际结果列。这是因为在查询解析时必须知道这些列。

您必须编写一个函数,根据categoriestbls参数组成查询字符串;这称为动态 SQL。无论您在客户端使用 PL/pgSQL 还是其他语言编写该函数并不重要 - 使用最适合您的任务的语言。

然后,在第二步中,对数据库运行生成的查询。