web-dev-qa-db-ja.com

CASEおよびGROUP BYを使用したピボットの動的な代替

次のような表があります。

id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D

そして、私はそれがこのように見えることを望みます:

bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8

私はこれを行うこのクエリを持っています:

SELECT bar, 
   MAX(CASE WHEN abc."row" = 1 THEN feh ELSE NULL END) AS "val1",
   MAX(CASE WHEN abc."row" = 2 THEN feh ELSE NULL END) AS "val2",
   MAX(CASE WHEN abc."row" = 3 THEN feh ELSE NULL END) AS "val3"
FROM
(
  SELECT bar, feh, row_number() OVER (partition by bar) as row
  FROM "Foo"
 ) abc
GROUP BY bar

これは非常に巧妙なアプローチであり、作成する新しい列が多数ある場合は扱いにくくなります。このクエリをより動的にするためにCASEステートメントを改善できるかどうか疑問に思っていました。また、私はこれを行うための他のアプローチを見てみたいです。

25
flipflop99

追加モジュール tablefunc をインストールしていない場合は、データベースごとに次のコマンドonceを実行します。

_CREATE EXTENSION tablefunc;
_

質問への回答

あなたの場合の非常に基本的なクロス集計ソリューション:

_SELECT * FROM crosstab(
  'SELECT bar, 1 AS cat, feh
   FROM   tbl_org
   ORDER  BY bar, feh')
 AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?
_

特別な難易度は、ベーステーブルにcategorycat)がないことです。基本的な1-parameter formの場合は、ダミー値をカテゴリとして提供するダミー列を提供できます。とにかく値は無視されます。

これは、まれなケースの1つです。ここで、crosstab()関数の2番目のパラメーター不要です。これは、すべてのNULL値がこの問題の定義による権利。そして、順序はvalueによって決定できます。

結果の値の順序を決定する名前を持つ実際のcategory列がある場合、crosstab()2-parameter formが必要です。ここで、ウィンドウ関数 row_number() を使用して、カテゴリ列を合成し、crosstab()をベースにします。

_SELECT * FROM crosstab(
   $$
   SELECT bar, val, feh
   FROM  (
      SELECT *, 'val' || row_number() OVER (PARTITION BY bar ORDER BY feh) AS val
      FROM tbl_org
      ) x
   ORDER BY 1, 2
   $$
 , $$VALUES ('val1'), ('val2'), ('val3')$$         -- more columns?
) AS ct (bar text, val1 int, val2 int, val3 int);  -- more columns?
_

残りはほとんど何でもありです。これらの密接に関連する回答で、より多くの説明とリンクを見つけてください。

基本:
crosstab()関数に精通していない場合は、まずこれをお読みください!

詳細:

適切なテスト設定

それが、最初からテストケースを提供する方法です。

_CREATE TEMP TABLE tbl_org (id int, feh int, bar text);
INSERT INTO tbl_org (id, feh, bar) VALUES
   (1, 10, 'A')
 , (2, 20, 'A')
 , (3,  3, 'B')
 , (4,  4, 'B')
 , (5,  5, 'C')
 , (6,  6, 'D')
 , (7,  7, 'D')
 , (8,  8, 'D');
_

動的なクロス集計?

@ Clodoaldoがコメントした のように、あまりdynamicではありません。 plpgsqlでは、動的な戻り型を実現するのは困難です。しかし、そこに方法があります-いくつかの制限があります

残りをさらに複雑にしないために、simplerテストケースで説明します:

_CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);
_

コール:

_SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2')
AS ct (row_name text, val1 int, val2 int, val3 int);
_

戻り値:

_ row_name | val1 | val2 | val3
----------+------+------+------
 A        | 10   | 20   |
 B        |  3   |  4   |
 C        |  5   |      |
 D        |  6   |  7   |  8
_

tablefuncモジュールの組み込み機能

Tablefuncモジュールは、列定義リストを提供せずに、一般的なcrosstab()呼び出しのシンプルなインフラストラクチャを提供します。 Cで記述された多くの関数(通常非常に高速):

crosstabN()

crosstab1()-crosstab4()は事前に定義されています。 1つの小さな点:それらはすべてtextを必要とし、返します。したがって、integer値をキャストする必要があります。しかし、それは呼び出しを簡素化します:

_SELECT * FROM crosstab4('SELECT row_name, attrib, val::text  -- cast!
                         FROM tbl ORDER BY 1,2')
_

結果:

_ row_name | category_1 | category_2 | category_3 | category_4
----------+------------+------------+------------+------------
 A        | 10         | 20         |            |
 B        | 3          | 4          |            |
 C        | 5          |            |            |
 D        | 6          | 7          | 8          |
_

カスタムcrosstab()関数

more columnsまたはother data typesの場合、独自に作成します複合タイプおよび関数(1回)。
タイプ:

_CREATE TYPE tablefunc_crosstab_int_5 AS (
  row_name text, val1 int, val2 int, val3 int, val4 int, val5 int);
_

関数:

_CREATE OR REPLACE FUNCTION crosstab_int_5(text)
  RETURNS SETOF tablefunc_crosstab_int_5
AS '$libdir/tablefunc', 'crosstab' LANGUAGE c STABLE STRICT;
_

コール:

_SELECT * FROM crosstab_int_5('SELECT row_name, attrib, val   -- no cast!
                              FROM tbl ORDER BY 1,2');
_

結果:

_ row_name | val1 | val2 | val3 | val4 | val5
----------+------+------+------+------+------
 A        |   10 |   20 |      |      |
 B        |    3 |    4 |      |      |
 C        |    5 |      |      |      |
 D        |    6 |    7 |    8 |      |
_

Oneすべての多態性、動的関数

これはtablefuncモジュールでカバーされているものを超えています。
戻り値の型を動的にするために、この関連する回答で詳しく説明されている手法でポリモーフィック型を使用します。

1パラメータ形式:

_CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L) t(%s)'
                , _qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;
_

2パラメーター形式のこのバリアントでオーバーロードします。

_CREATE OR REPLACE FUNCTION crosstab_n(_qry text, _cat_qry text, _rowtype anyelement)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE 
   (SELECT format('SELECT * FROM crosstab(%L, %L) t(%s)'
                , _qry, _cat_qry
                , string_agg(quote_ident(attname) || ' ' || atttypid::regtype
                           , ', ' ORDER BY attnum))
    FROM   pg_attribute
    WHERE  attrelid = pg_typeof(_rowtype)::text::regclass
    AND    attnum > 0
    AND    NOT attisdropped);
END
$func$  LANGUAGE plpgsql;
_

pg_typeof(_rowtype)::text::regclass:すべてのユーザー定義の複合型に対して行型が定義されているため、属性(列)はシステムカタログ _pg_attribute_ にリストされます。取得する高速レーン:登録済みの型(regtype)をtextにキャストし、このtextregclassにキャストします。

複合型を1回作成します。

使用する戻り値の型ごとに1回定義する必要があります。

_CREATE TYPE tablefunc_crosstab_int_3 AS (
    row_name text, val1 int, val2 int, val3 int);

CREATE TYPE tablefunc_crosstab_int_4 AS (
    row_name text, val1 int, val2 int, val3 int, val4 int);

...
_

アドホックコールの場合は、temporary tableと同じ(一時的な)効果を作成することもできます。

_CREATE TEMP TABLE temp_xtype7 AS (
    row_name text, x1 int, x2 int, x3 int, x4 int, x5 int, x6 int, x7 int);
_

または、利用可能な場合は、既存のテーブル、ビュー、またはマテリアライズドビューのタイプを使用します。

コール

上記の行タイプの使用:

1パラメーター形式(欠損値なし):

_SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1,2'
 , NULL::tablefunc_crosstab_int_3);_

2パラメーター形式(一部の値が欠落している場合があります):

_SELECT * FROM crosstab_n(
   'SELECT row_name, attrib, val FROM tbl ORDER BY 1'
 , $$VALUES ('val1'), ('val2'), ('val3')$$
 , NULL::tablefunc_crosstab_int_3);_

この1つの関数はすべての戻り値の型に対して機能しますが、tablefuncモジュールによって提供されるcrosstabN()フレームワークには、それぞれに個別の関数が必要です。
上に示したような順序でタイプに名前を付けた場合、太字の数字を置き換えるだけで済みます。ベーステーブルのカテゴリの最大数を見つけるには:

_SELECT max(count(*)) OVER () FROM tbl  -- returns 3
GROUP  BY row_name
LIMIT  1;
_

これは、個々の列が必要な場合にこれとほぼ同じくらい動的です。 @ Clocoaldoで示される のような配列、または単純なテキスト表現、またはjsonhstoreのようなドキュメントタイプにラップされた結果は、任意の数のカテゴリに対して動的に機能します。

免責事項:
ユーザー入力がコードに変換されると、常に潜在的に危険です。これがSQLインジェクションに使用できないことを確認してください。信頼できないユーザーからの入力を(直接)受け入れないでください。

元の質問をする:

_SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);
_
51

これは古い質問ですが、PostgreSQLの最近の改善によって可能になった別のソリューションを追加したいと思います。このソリューションは、クロス集計関数をまったく使用せずに、動的データセットから構造化された結果を返すという同じ目標を達成します。つまり、これは良いことです古い問題の新しい解決策を発見することを妨げる、意図的でない暗黙の仮定を再検討する例。 ;)

説明のために、次の構造を持つデータを転置する方法を求めました。

_id    feh    bar
1     10     A
2     20     A
3      3     B
4      4     B
5      5     C
6      6     D
7      7     D
8      8     D
_

この形式に:

_bar  val1   val2   val3
A     10     20 
B      3      4 
C      5        
D      6      7     8
_

従来のソリューションは、Erwin Brandstetterの回答で詳細に説明されている動的なクロス集計クエリを作成するための巧妙な(そして非常に知識のある)アプローチです。

ただし、特定のユースケースがわずかに異なる結果形式を受け入れるのに十分な柔軟性がある場合は、動的ピボットを美しく処理する別のソリューションが可能です。ここで学んだこのテクニック

postgreSQLの新しい _jsonb_object_agg_ 関数を使用して、ピボットされたデータをその場でJSONオブジェクトの形式で構築します。

Brandstetter氏の「シンプルなテストケース」を使用して、以下を説明します。

_CREATE TEMP TABLE tbl (row_name text, attrib text, val int);
INSERT INTO tbl (row_name, attrib, val) VALUES
   ('A', 'val1', 10)
 , ('A', 'val2', 20)
 , ('B', 'val1', 3)
 , ('B', 'val2', 4)
 , ('C', 'val1', 5)
 , ('D', 'val3', 8)
 , ('D', 'val1', 6)
 , ('D', 'val2', 7);
_

_jsonb_object_agg_関数を使用して、この簡潔な美しさで必要なピボットされた結果セットを作成できます。

_SELECT
  row_name AS bar,
  json_object_agg(attrib, val) AS data
FROM tbl
GROUP BY row_name
ORDER BY row_name;
_

どの出力:

_ bar |                  data                  
-----+----------------------------------------
 A   | { "val1" : 10, "val2" : 20 }
 B   | { "val1" : 3, "val2" : 4 }
 C   | { "val1" : 5 }
 D   | { "val3" : 8, "val1" : 6, "val2" : 7 }
_

ご覧のとおり、この関数は、すべてが_row_name_でグループ化されたサンプルデータのattrib列とvalue列からJSONオブジェクトにキー/値のペアを作成することで機能します。

この結果セットは明らかに異なって見えますが、実際には多くの(ほとんどではないにしても)現実世界のユースケース、特にデータが動的に生成されたピボットを必要とする場合、または結果データが親アプリケーションによって消費される場合(例えば、 http応答で送信するために再フォーマットする必要があります)。

このアプローチの利点:

  • より簡潔な構文このアプローチの構文は、最も基本的なクロス集計の例よりもはるかに簡潔で理解しやすいことに誰もが同意すると思います。

  • 完全に動的です。基礎となるデータに関する情報を事前に指定する必要はありません。列名もそのデータ型も事前に知る必要はありません。

  • 多数の列を処理します。ピボットされたデータは単一のjsonb列として保存されるため、PostgreSQLの列制限(≤1,600列、私は信じている)。まだ制限はありますが、テキストフィールドの場合と同じだと思います:作成されたJSONオブジェクトごとに1 GB(間違っている場合は修正してください)。それは多くのキー/値ペアです!

  • データ処理の簡素化。DBでのJSONデータの作成は、親アプリケーションでのデータ変換プロセスを単純化する(おそらく高速化する)と信じています。 (サンプルテストケースの整数データは、生成されたJSONオブジェクトにそのまま保存されていることに注意してください。PostgreSQLは、JSON仕様に従って、固有のデータ型をJSONに自動的に変換することでこれを処理します。)親アプリケーションに渡されたデータを手動でキャストするには、すべてアプリケーションのネイティブJSONパーサーに委任できます。

違い(および考えられる欠点):

  • 見た目が異なります。このアプローチの結果が異なっていることは否定できません。 JSONオブジェクトは、クロス集計結果セットほどきれいではありません。ただし、違いは純粋に表面的なものです。同じ情報が生成されますが、おそらくmoreの形式で、親アプリケーションでの使用に適しています。

  • キーがありません。クロスタブアプローチの欠損値はnullで埋められますが、JSONオブジェクトには該当するキーがありません。これがユースケースの許容可能なトレードオフであるかどうかは、自分で判断する必要があります。 PostgreSQLでこの問題に対処しようとすると、プロセスが非常に複雑になり、追加のクエリの形式で内省が行われる可能性があります。

  • キーの順序は保持されません。PostgreSQLでこれに対処できるかどうかはわかりませんが、親アプリケーションはキーの順序に依存する可能性が低いか、他の方法で適切なキーの順序を決定する能力を持っています。最悪の場合、おそらくデータベースの追加クエリのみが必要になります。

結論

特にパフォーマンスに関係するこのアプローチについて、他の人(特に@ErwinBrandstetterの意見)の意見を聞きたいと思います。 Andrew Benderのブログでこのアプローチを発見したとき、頭の中でぶつかるようなものでした。 PostrgeSQLの難しい問題に新鮮なアプローチをとるなんて素晴らしい方法でしょう。それは私のユースケースを完全に解決し、他の多くの人にも同様に役立つと信じています。

15

これは @ Damian 良い答えを完成させることです。 9.6の便利なjson_object_agg 関数。以前のツールセットでは、さらに多くの作業が必要です。

引用されている可能性のある2つの欠点は、実際にはそうではありません。ランダムキーの順序は、必要に応じて簡単に修正されます。欠落しているキーは、関連する場合、対処するためにほとんど取るに足らない量のコードを取ります。

select
    row_name as bar,
    json_object_agg(attrib, val order by attrib) as data
from
    tbl
    right join
    (
        (select distinct row_name from tbl) a
        cross join
        (select distinct attrib from tbl) b
    ) c using (row_name, attrib)
group by row_name
order by row_name
;
 bar |                     data                     
-----+----------------------------------------------
 a   | { "val1" : 10, "val2" : 20, "val3" : null }
 b   | { "val1" : 3, "val2" : 4, "val3" : null }
 c   | { "val1" : 5, "val2" : null, "val3" : null }
 d   | { "val1" : 6, "val2" : 7, "val3" : 8 }

JSONを理解する最終クエリコンシューマには、欠点はありません。唯一の方法は、テーブルソースとして使用できないことです。

6
Clodoaldo Neto

あなたの場合、配列が良いと思います。 SQL Fiddle

select
    bar,
    feh || array_fill(null::int, array[c - array_length(feh, 1)]) feh
from
    (
        select bar, array_agg(feh) feh
        from foo
        group by bar
    ) s
    cross join (
        select count(*)::int c
        from foo
        group by bar
        order by c desc limit 1
    ) c(c)
;
 bar |      feh      
-----+---------------
 A   | {10,20,NULL}
 B   | {3,4,NULL}
 C   | {5,NULL,NULL}
 D   | {6,7,8}
5
Clodoaldo Neto

過去に戻ってきてすみませんが、ソリューション「ダイナミッククロスタブ」は誤った結果テーブルを返します。したがって、valN値は誤って「左揃え」になり、列名に対応しません。入力テーブルの値に「穴」がある場合、たとえば「C」にはval1とval3がありますが、val2はありません。これによりエラーが発生します:val3値は、最終テーブルの列val2(つまり、次の空き列)の範囲になります。

_CREATE TEMP TABLE tbl (row_name text, attrib text, val int); 
INSERT INTO tbl (row_name, attrib, val) VALUES ('C', 'val1', 5) ('C', 'val3', 7);

SELECT * FROM crosstab('SELECT row_name, attrib, val FROM tbl 
ORDER BY 1,2') AS ct (row_name text, val1 int, val2 int, val3 int);

row_name|val1|val2|val3
 C      |   5|  7 |
_

右側の列に「穴」がある正しいセルを返すために、クロス集計クエリでは、クロス集計で2番目のSELECTが必要です。このような"crosstab('SELECT row_name, attrib, val FROM tbl ORDER BY 1,2', 'select distinct row_name from tbl order by 1')"

2
vsinceac