31

私はこのようなテーブルを持っています:

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このクエリをより動的にするためにステートメントを改善できるかどうか疑問に思いましたか?また、これを行うための他のアプローチを見たいと思います。

4

6 に答える 6

62

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

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?

ここでの特別な難しさは、ベーステーブルにカテゴリ( )がないことです。cat基本的な1パラメータ形式の場合、カテゴリとして機能するダミー値を持つダミー列を提供できます。とにかく値は無視されます。

これは、関数の2番目のパラメーターが不要なまれなケースの1つです。これは、この問題の定義により、すべての値が右側のぶら下がっている列にのみ表示されるためです。そして、順序は値によって決定できます。crosstab()NULL

結果の値の順序を決定する名前を持つ実際のカテゴリ列がある場合は、 2パラメータ形式のが必要になりますcrosstab()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がコメントしたように、まだあまり動的ではありません。動的リターンタイプは、plpgsqlでは実現が困難です。しかし、それを回避する方法があります-いくつかの制限があります。

したがって、残りの部分をさらに複雑にしないために、より単純なテストケースでデモンストレーションします。

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()機能

より多くの列または他のデータ型については、独自の複合型関数を(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 |      |

すべての人に1つの多形で動的な関数

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にキャストし、これtextをにキャストしregclassます。

複合型を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);

...

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

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つの関数はすべての戻り型に対して機能しますが、モジュールによって提供されるフレームワークには、それぞれに個別の関数が必要です。 上記のようにタイプに順番に名前を付けた場合は、太字の数字を置き換えるだけで済みます。基本テーブルでカテゴリの最大数を見つけるには、次のようにします。crosstabN()tablefunc

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

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

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

元の質問を求める:

SELECT * FROM crosstab_n('SELECT bar, 1, feh FROM tbl_org ORDER BY 1,2'
                       , NULL::tablefunc_crosstab_int_3);
于 2013-03-20T02:42:31.057 に答える
21

これは古い質問ですが、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

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

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

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 }

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

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

このアプローチの利点:

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

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

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

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

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

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

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

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

結論

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

于 2016-10-17T18:57:48.357 に答える
8

これは@Damianの良い答えを完成させることです。json_object_agg9.6の便利な関数の前に、他の回答でJSONアプローチをすでに提案しました。以前のツールセットでは、さらに多くの作業が必要です。

引用された考えられる欠点のうちの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を理解する最終的なクエリコンシューマーの場合、欠点はありません。唯一の1つは、テーブルソースとして使用できないことです。

于 2017-02-04T14:46:44.067 に答える
5

あなたの場合、配列は良いと思います。SQLフィドル

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}
于 2013-03-20T00:51:25.933 に答える
2

過去に戻って申し訳ありませんが、ソリューション「動的クロス集計」は誤った結果テーブルを返します。したがって、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')"

于 2015-05-29T06:58:52.090 に答える
0

これは、予想している値と同じ数の列を列挙する必要があるという意味で実際には動的ではありませんが、この方法で行うのは簡単です。1つの落とし穴は、列が途切れることなく一致するために序数キーを必要とすることです。また、重複するキーがある場合は、すべてが失敗するため、重複排除も必要になります。Nの適切なセットに対応するには、すべてのセットを事前にパーティション化する必要があります。

私にはそれはぎこちなく見えるので、それが大金のために多くの価値を得るかどうかはわかりません。しかし、私はこれをコミュニティのドッグパイルに追加して、他の誰かがより良いアプローチで思い付くための何らかの刺激を提供することを期待しています。

/** build a dataset **/
DROP TABLE IF EXISTS tmpT ;
CREATE TEMP TABLE tmpT AS
SELECT
 NULL::INT AS key
 ,NULL::INT AS ints
 ,NULL::VARCHAR(1) AS chars
 ,NULL::VARCHAR(3) AS unnest
LIMIT 0 ;

insert into tmpT (key, ints, chars, unnest)
values   (1 , 1   , 'o',  CHR( 130 - 10 ) )       
        ,(2 , 2   , 'n',  CHR( 130 - 11 ) )       
        ,(3 , 3   , 'm',            NULL  )       
        --,(4 , 4   , 'l',  CHR( 130 - 13 ) ) -- missing set       
        ,(5 , 5   , null, CHR( 130 - 14 ) )        
        ,(6 , null, 'j',  CHR( 130 - 15 ) )        
        ,(7 , 7   , null, CHR( 130 - 16 ) )         
        ,(8 , null, 'h',  CHR( 130 - 17 ) )        
        ,(9 , 9   , null, CHR( 130 - 18 ) )         
        ,(10, null, 'f' ,           NULL  )        
        ,(11, null, 'a',  CHR( 130 - 20 ) )        
        ,(12, 12  , null, CHR( 130 - 21 ) )         
 ; /** end of build a dataset **/

/** set up full set of pivotal column positions, to backfill any missing  **/
DROP TABLE IF EXISTS tGenSer ; 
CREATE TEMP TABLE tGenSer AS SELECT generate_series( 1, 1000 )::INT AS key ;

/ ** THEN THE PIVOT ** /

/* Pivot 10 columns */
SELECT *
FROM     /* name the columns*/
(    SELECT a a ,a b ,a c ,a d ,a e ,a f ,a g ,a h ,a i ,a j /*,a k ,a l ,a m ,a n ,a o ,a p ,a q ,a r ,a s ,a t*/ /* ,a u ,a v ,a w ,a x ,a y ,a z*/ FROM ( SELECT NULL::VARCHAR(3) AS a /**seed the typed columns **/) a  
    
    UNION /** union is just a helper, to assign names to unnamed columns **/
    
    /** 20 columns **/
    SELECT * FROM
    (
        /* enumerate columns, no name */
        SELECT t1.x[1 ] ,t1.x[2 ] ,t1.x[3 ] ,t1.x[4 ] ,t1.x[5 ] ,t1.x[6 ] ,t1.x[7 ] ,t1.x[8 ] ,t1.x[9 ] ,t1.x[10] 
        FROM ( SELECT  ARRAY( SELECT  a.ints::TEXT  AS v   
                        FROM tGenSer tg /**backfill missing keys**/ 
                        LEFT JOIN tmpT a ON tg.key = a.key ORDER BY tg.key 
                        ) AS x 
             ) t1
        
        UNION ALL
        
        SELECT t1.x[1 ] ,t1.x[2 ] ,t1.x[3 ] ,t1.x[4 ] ,t1.x[5 ] ,t1.x[6 ] ,t1.x[7 ] ,t1.x[8 ] ,t1.x[9 ] ,t1.x[10] 
        FROM ( SELECT  ARRAY( SELECT  a.chars::TEXT AS v 
                        FROM tGenSer tg /**backfill missing keys**/ 
                        LEFT JOIN tmpT a ON tg.key = a.key ORDER BY tg.key 
                        ) AS x 
            ) t1
        
        UNION ALL
        
        SELECT t1.x[1 ] ,t1.x[2 ] ,t1.x[3 ] ,t1.x[4 ] ,t1.x[5 ] ,t1.x[6 ] ,t1.x[7 ] ,t1.x[8 ] ,t1.x[9 ] ,t1.x[10] 
        FROM ( SELECT  ARRAY( SELECT  a.unnest      AS v 
                        FROM tGenSer tg /**backfill missing keys**/ 
                        LEFT JOIN tmpT a ON tg.key = a.key 
                        ORDER BY tg.key 
                        ) AS x 
           ) t1
     ) a
)b
WHERE ( a,b,c,d,e,f,g,h,i,j) IS DISTINCT FROM ( NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL ,NULL )        

;
    

結果:

+---+---+--+--+--+--+--+--+--+--+
| a | b |c |d |e |f |g |h |i |j |
+---+---+--+--+--+--+--+--+--+--+
| x | w |  |  |t |s |r |q |p |  |
| o | n |m |  |  |j |  |h |  |f |
| 1 | 2 |3 |  |5 |  |7 |  |9 |  |
+---+---+--+--+--+--+--+--+--+--+
于 2021-05-20T00:57:15.637 に答える