web-dev-qa-db-ja.com

渡された配列に基づいてフィルタリングすることにより、plpgsql関数からの動的な結果セットを許可しますか?

私はここでXY問題を回避したと思います。これは、根本的な問題(動的な方法で複数のテーブルを要約する)の解決策を示しており、行き詰まった最後の1つの部分についてのみ尋ねています。したがって、最初にかなりの背景があります。最小限のサンプルデータセットと、説明した方法でデータを要約するためのworkingコードを提供しました。


次のような設定を検討してください。

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');

次に、次の要約を作成できます。

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)

しかし、要約したい複数のそのようなテーブル(すべて同じカテゴリA、B、Cを使用)があるので、最終的な結果displayedは、要約する単一の行である必要があります次のような1つのテーブル:

 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)

次のように、カテゴリAとBに関する情報のみを返し、Cを無視するなど、時々フィルタリングする必要もあります。

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

上記の最初の出力は、次のように、やや不器用なCTEでcount(*) filter (where...)構文を使用したクエリで生成できます。

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;

そして、これを変更して、カテゴリCを簡単に省略して、上記の2番目の出力例を生成できます。 (ほとんど繰り返しになるため、ここでは示していません。)

問題は、要約するテーブルが非常に多いため(実際にはテーブルではなくビューですが、それは問題ではありません)、テーブルの任意のグループを簡単に要約できるという要件ですad hoc、カテゴリを自由に含めたり省略したりする(たとえば、「tbl1、tbl2、tbl3をまとめるが、カテゴリBとCだけをまとめる」、または「すべてのテーブルでカテゴリBだけをまとめる」)と、上記のSQLは十分に柔軟ではありません。

タイプnameの任意の数の引数を受け入れるplpgsql関数を使用して、「テーブルの任意のグループをその場限りで要約する」要件を達成し、要約するすべてのテーブルの名前をフィードできます。 :

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$
;

これをselect * from summarize_tables('tbl1');で呼び出して、上記のサンプルデータセットを要約したり、select * from summarize_tables('tbl1', 'tbl2');を使用して追加のテーブルを要約したりできます。

ただし、これでは2番目の要件はまったく達成されません。つまり、A、B、またはCを任意に含めたり除外したりするためにさまざまな結果列を計算できるということです。

おそらく、次のような関数シグネチャでこれを行う方法があると思いました。

create function summarize_tables(categories text[], variadic tbls name[])

そして、それを次のように呼び出します:

select * from summarize_tables('{A,B}', 'tbl1', 'tbl2');

しかし、SQL内から "categories"配列をどのように利用できるのか理解できません。これは可能ですか、渡されたカテゴリに従ってこのようなフィルタリングされた方法で結果を要約することはできますか?


関連するメモで、私は https://stackoverflow.com/a/11751557/5419599 を見つけたので、本当に動的な列が返されるようにしたい場合、returns setof recordを使用する必要があることを認識していますand関数を呼び出すたびに返される列の完全な名前と型を指定する必要があります。もしあれば、その回避策に興味があります。

おそらく、これらの2つの要素の組み合わせは、要約したいカテゴリーA、B、Cの組み合わせごとに個別の関数が必要になることを受け入れる必要があることを意味します-合計7つの関数。

しかし、その場合、カテゴリDとカテゴリEが後で追加されると、私は悲しみます!

その組み合わせの可能性から、関数を呼び出すたびに返される列の名前と型を指定する必要があるのは価値があると思いますsingle関数があれば十分です。つまり、関数定義の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)
;

ただし、現在のCTEよりも動的なフィルタリングを行う方法、つまり、渡されたcategories text[]パラメーターを利用する方法がない限り、このトレードオフは不可能です。つまり私の質問は何ですか。

(ただし、上記の設計に関するポインタも歓迎します。)

この質問では、「通過する場所」を「通過する場所が真」に変更することで処理されるnullの「通過」値の処理を省略し、特定のケースでゼロエラーによる除算を回避するためにケーススイッチを省略しました。テーブルに特定のカテゴリが含まれていません。

5
Wildcard

私はunnest(...) with ordinalityを使用してそれを行う方法を考え出し、結果の一部として配列を返しました。

関数定義は次のとおりです。

create function summarize_tables(categories text[], variadic tbls name[])
returns table (tbl name, overall text, by_category text[])
language plpgsql
as $funcdef$
  begin
    foreach tbl in array tbls
    loop
      return query execute format(
        $query$
          with tallies as (
            select
              category,
              count(*) filter (where passing) as passcount,
              count(*) as allcount from %I group by category
          ),
          categories_passed as (
            select * from unnest(%L::text[]) with ordinality as x(category, rn)
          )
          select
            %1$L::name as tbl,
            format('%%s%%%% (%%s/%%s)', (sum(passcount)*100/sum(allcount))::int, sum(passcount), sum(allcount)) as overall,
            array_agg(format('%%s%%%% (%%s/%%s)', passcount*100/allcount, passcount, allcount) order by rn) as by_category
          from categories_passed natural left join tallies;
        $query$,
        tbl,
        categories
      );
    end loop;
    return;
  end
$funcdef$
;

生の結果(select *) のように見える:

postgres=> select * from summarize_tables('{A,B,C}', 'tbl1');
 tbl  |   overall   |              by_category
------+-------------+----------------------------------------
 tbl1 | 50% (10/20) | {"75% (3/4)","50% (5/10)","33% (2/6)"}
(1 row)

postgres=> select * from summarize_tables('{A,B}', 'tbl1');
 tbl  |  overall   |        by_category
------+------------+----------------------------
 tbl1 | 57% (8/14) | {"75% (3/4)","50% (5/10)"}
(1 row)

カテゴリーごとに分類された結果はアルファベット順にではなくソートされますが、渡された同じ順序で保持されることに注意してください。

postgres=> select * from summarize_tables('{B,A}', 'tbl1');
 tbl  |  overall   |        by_category
------+------------+----------------------------
 tbl1 | 57% (8/14) | {"50% (5/10)","75% (3/4)"}
(1 row)

質問に示されている正確な結果を取得するには、列名を指定し、結果を配列から引き出す必要があります。

select
  tbl as "Table Name",
  overall as "Overall passing rate",
  by_category[1] as "A passing rate",
  by_category[2] as "B passing rate",
  by_category[3] as "C passing rate"
from summarize_tables('{A,B,C}', 'tbl1');

 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)
(1 row)

select
  tbl as "Table Name",
  overall as "Overall passing rate",
  by_category[1] as "A passing rate",
  by_category[2] as "B passing rate"
from summarize_tables('{A,B}', 'tbl1');

 Table Name | Overall passing rate | A passing rate | B passing rate
------------+----------------------+----------------+----------------
 tbl1       | 57% (8/14)           | 75% (3/4)      | 50% (5/10)
(1 row)

質問では呼び出されませんが、関数の呼び出し元が生の数値にアクセスできるようにしたい場合(並べ替えや数値条件など)、代わりにネストされた配列を返し、呼び出し元がフォーマットを実行できます。 :

create function summarize_tables(categories text[], variadic tbls name[])
returns table (tbl name, overall numeric[], by_category numeric[][])
language plpgsql
as $funcdef$
  begin
    foreach tbl in array tbls
    loop
      return query execute format(
        $query$
          with tallies as (
            select
              category,
              count(*) filter (where passing)::numeric as passcount,
              count(*)::numeric as allcount from %I group by category
          ),
          categories_passed as (
            select * from unnest(%L::text[]) with ordinality as x(category, rn)
          )
          select
            %1$L::name as tbl,
            array[sum(passcount), sum(allcount)] as overall,
            array_agg(array[passcount, allcount] order by rn) as by_category
          from categories_passed natural left join tallies;
        $query$,
        tbl,
        categories
      );
    end loop;
    return;
  end
$funcdef$
;

ネストされた配列から値を取り出すSQLはきれいではありませんが、機能します。

with x as (
  select
    tbl,
    overall as o,
    by_category[1:1] as a,
    by_category[2:2] as b,
    by_category[3:3] as c
  from summarize_tables('{A,B,C}', 'tbl1')
)
select
  tbl,
  format('%s%% (%s/%s)', (100*x.o[1]/x.o[2])::int, x.o[1], x.o[2]) as "Overall passing rate",
  format('%s%% (%s/%s)', (100*x.a[1][1]/x.a[1][2])::int, x.a[1][1], x.a[1][2]) as "A passing rate",
  format('%s%% (%s/%s)', (100*x.b[1][1]/x.b[1][2])::int, x.b[1][1], x.b[1][2]) as "B passing rate",
  format('%s%% (%s/%s)', (100*x.c[1][1]/x.c[1][2])::int, x.c[1][1], x.c[1][2]) as "C passing rate"
from x
;

Postgres配列がどのように機能するかに関する関連資料: https://stackoverflow.com/a/34408253/5419599

1
Wildcard

素晴らしいポスト、

列の数を増減できるテーブルの問題に対処する必要がありました。

私はあなたがしたのと同じことをしました、そしてそれは新しいテーブルを作成するために動的にSQLを構築します。問題は、関数から単純なクエリでテーブルを返す方法になります。フラットアウトはそれを行うことはできません。そのため、ソリューションは非常に単純であり、宛先テーブル名を関数に渡してから、宛先名から*を選択します。

Select build_dynamic_table( MyRANDOM_Table_Name, rest_of_args);

--table build and data is put in

Select * from Myrandom_Table_Name  
   [any join conditions]
   [where filter_conditions];  

結果は、他のユーザーからアクセスできる実際のテーブルか、破棄された一時テーブルになります。

この方法で作成されたテーブルでは、ほとんどの場合、ログに記録したり一時ファイルを作成したりしません

他の人がアクセスするテーブルを作成するときは、まずステージングテーブルを作成します。それが完了したら、古いテーブルを「ドロップ」してから発行します

begin ;
drop Shared_table_name 
create table like Myrandom_table_Name;
commit;  --done this way to limit errors the clients can see
Select * from Myrandom_table_Name into Shared_Table_Name;

カラムが拡大または縮小しても、これは機能します。

大きな欠点は、このテーブルは決して更新できず、他のユーザーはテーブルの構造を知らないため、Select column_listやwhere句などを発行できず、Select * fromに制限されることです。この問題を回避するには、pg_catalogに移動してクライアント側で動的クエリを作成し、新しいテーブルの構造を把握する必要があります。私もそうします...

ドロップ中にデータをshared_table_nameに移動すると、ユーザーがロックされているためにアクセス拒否、またはオブジェクトが見つからないかデータが返されないためにテーブルをクエリしようとすると問題が発生します。

0
zsheep

あなたは問題をうまく説明しました。完全なソリューションを提供することはできませんが、いくつかのアイデアを紹介します。

ご存じのとおり、結果列の数が可変である関数は、希望どおりに実行できません。 RETURNS SETOF recordとして定義する場合、関数へのすべてのクエリで実際の結果列を指定する必要があります。これは、クエリの解析時に列を認識している必要があるためです。

categoriesおよびtblsパラメータに基づいてクエリ文字列を作成する関数を作成する必要があります。これは動的SQLとして知られています。その関数をPL/pgSQLで記述するか、クライアント側の他の言語で記述するかは重要ではありません。タスクに最適な言語を使用してください。

次に、2番目のステップで、結果のクエリをデータベースに対して実行します。

0
Laurenz Albe