web-dev-qa-db-ja.com

PostgreSQLでクロス集計の列を動的に生成する

PostgreSQLでcrosstabクエリを作成して、ハードコーディングする代わりにcrosstab列を自動的に生成するようにしています。 crosstabクエリに必要な列リストを動的に生成する関数を作成しました。アイデアは、動的SQLを使用してcrosstabクエリでこの関数の結果を置き換えることです。

SQL Serverでこれを簡単に行う方法は知っていますが、PostgreSQLに関する私の限られた知識が、ここでの私の進歩を妨げています。列の動的リストを生成する関数の結果を変数に格納し、それを使用してSQLクエリを動的に構築することを考えていました。誰かが同じことについて私を案内してくれるといいですね。


-- Table which has be pivoted
CREATE TABLE test_db
(
    kernel_id int,
    key int,
    value int
);

INSERT INTO test_db VALUES
(1,1,99),
(1,2,78),
(2,1,66),
(3,1,44),
(3,2,55),
(3,3,89);


-- This function dynamically returns the list of columns for crosstab
CREATE FUNCTION test() RETURNS TEXT AS '
DECLARE
    key_id int;
    text_op TEXT = '' kernel_id int, '';
BEGIN
    FOR key_id IN SELECT DISTINCT key FROM test_db ORDER BY key LOOP
    text_op := text_op || key_id || '' int , '' ;
    END LOOP;
    text_op := text_op || '' DUMMY text'';
    RETURN text_op;
END;
' LANGUAGE 'plpgsql';

-- This query works. I just need to convert the static list
-- of crosstab columns to be generated dynamically.
SELECT * FROM
crosstab
(
    'SELECT kernel_id, key, value FROM test_db ORDER BY 1,2',
    'SELECT DISTINCT key FROM test_db ORDER BY 1'
)
AS x (kernel_id int, key1 int, key2 int, key3 int); -- How can I replace ..
-- .. this static list with a dynamically generated list of columns ?
20
invinc4u

これには、提供されているC関数_crosstab_hash_を使用できます。

マニュアルはこの点であまり明確ではありません。それは言及されています crosstab()の章の最後に2つのパラメータがあります:

事前定義された関数を作成して、各クエリで結果の列名と型を書き出す必要を回避できます。前のセクションの例を参照してください。この形式のcrosstabの基礎となるC関数の名前は_crosstab_hash_です。

あなたの例のために:

_CREATE OR REPLACE FUNCTION f_cross_test_db(text, text)
  RETURNS TABLE (kernel_id int, key1 int, key2 int, key3 int)
  AS '$libdir/tablefunc','crosstab_hash' LANGUAGE C STABLE STRICT;
_

コール:

_SELECT * FROM f_cross_test_db(
      'SELECT kernel_id, key, value FROM test_db ORDER BY 1,2'
     ,'SELECT DISTINCT key FROM test_db ORDER BY 1');
_

戻り値の型が異なるcrosstab関数ごとに異なる_crosstab_hash_関数を作成する必要があることに注意してください。

これは密接に関連する別の回答です。


列リストを生成する関数はかなり複雑ですが、結果は正しくありません(intが_kernel_id_の後に欠落しています)。このSQLで置き換えることができますクエリ:

_SELECT 'kernel_id int, '
       || string_agg(DISTINCT key::text, ' int, '  ORDER BY key::text)
       || ' int, DUMMY text'
FROM   test_db;
_

とにかく動的に使用することはできません。

11

@ erwin-brandstetter:変換された結果で常にJSON型を返す場合、関数の戻り型は問題ではありません。

これが私が思いついた機能です:

CREATE OR REPLACE FUNCTION report.test(
    i_start_date TIMESTAMPTZ,
    i_end_date TIMESTAMPTZ,
    i_interval INT
    ) RETURNS TABLE (
    tab JSON
    ) AS $ab$
DECLARE
    _key_id TEXT;
    _text_op TEXT = '';
    _ret JSON;
BEGIN
    -- SELECT DISTINCT for query results
    FOR _key_id IN
    SELECT DISTINCT at_name
      FROM report.company_data_date cd 
      JOIN report.company_data_amount cda ON cd.id = cda.company_data_date_id 
      JOIN report.amount_types at ON cda.amount_type_id  = at.id 
     WHERE date_start BETWEEN i_start_date AND i_end_date
       AND interval_type_id = i_interval
    LOOP
    -- build function_call with datatype of column
        IF char_length(_text_op) > 1 THEN
            _text_op := _text_op || ', ' || _key_id || ' NUMERIC(20,2)';
        ELSE
            _text_op := _text_op || _key_id || ' NUMERIC(20,2)';
        END IF;
    END LOOP;
    -- build query with parameter filters
    RETURN QUERY
    EXECUTE '
        SELECT array_to_json(array_agg(row_to_json(t)))
          FROM (
        SELECT * FROM crosstab(''SELECT date_start, at.at_name,  cda.amount ct 
          FROM report.company_data_date cd 
          JOIN report.company_data_amount cda ON cd.id = cda.company_data_date_id 
          JOIN report.amount_types at ON cda.amount_type_id  = at.id 
         WHERE date_start between $$' || i_start_date::TEXT || '$$ AND $$' || i_end_date::TEXT || '$$ 
           AND interval_type_id = ' || i_interval::TEXT || ' ORDER BY date_start'') 
            AS ct (date_start timestamptz, ' || _text_op || ')
             ) t;';
END;
$ab$ LANGUAGE 'plpgsql';

したがって、実行すると動的な結果がJSONで取得され、ピボットされた値の数を知る必要はありません。

select * from report.test(now()- '1 week'::interval, now(), 1);
                                                                                                                     tab                                                                                                                      
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 [{"date_start":"2015-07-27T08:40:01.277556-04:00","burn_rate":0.00,"monthly_revenue":5800.00,"cash_balance":0.00},{"date_start":"2015-07-27T08:50:02.458868-04:00","burn_rate":34000.00,"monthly_revenue":15800.00,"cash_balance":24000.00}]
(1 row)

編集:クロスタブにデータ型が混在している場合は、次のようなものを使用して、列ごとにデータ型を検索するロジックを追加できます。

  SELECT a.attname as column_name, format_type(a.atttypid, a.atttypmod) AS data_type 
    FROM pg_attribute a 
    JOIN pg_class b ON (a.attrelid = b.relfilenode) 
    JOIN pg_catalog.pg_namespace n ON n.oid = b.relnamespace 
   WHERE n.nspname = $$schema_name$$ AND b.relname = $$table_name$$ and a.attstattarget = -1;"
3
Caullyn

ここで説明するアプローチhttp://www.cureffi.org/2013/03/19/automatically-creating-pivot-table-column-names-in-postgresql/は私にとってはうまくいきました。ピボットテーブルを直接取得する代わりに。より簡単な方法は、関数にSQLクエリ文字列を生成させることです。結果のSQLクエリ文字列をオンデマンドで動的に実行します。

1
Ben

これは古い投稿ですが、同じ問題についてしばらくの間苦労していました。

私の問題の説明:フィールドに複数の値を持つテーブルがあり、1行に40以上の列見出しがあるクロス集計クエリを作成したいと考えていました。

私の解決策は、クロス集計クエリ内の列見出しとして使用したい値を取得するために、テーブル列をループする関数を作成することでした。

この関数内で、クロス集計クエリを作成できます。私の使用例では、このクロス集計結果を別のテーブルに追加しました。

例えば。

CREATE OR REPLACE FUNCTION field_values_ct ()
 RETURNS VOID AS $$
DECLARE rec RECORD;
DECLARE str text;
BEGIN
str := '"Issue ID" text,';
   -- looping to get column heading string
   FOR rec IN SELECT DISTINCT field_name
        FROM issue_fields
        ORDER BY field_name
    LOOP
    str :=  str || '"' || rec.field_name || '" text' ||',';
    END LOOP;
    str:= substring(str, 0, length(str));

    EXECUTE 'CREATE EXTENSION IF NOT EXISTS tablefunc;
    DROP TABLE IF EXISTS temp_issue_fields;
    CREATE TABLE temp_issue_fields AS
    SELECT *
    FROM crosstab(''select issue_id, field_name, field_value from issue_fields order by 1'',
                 ''SELECT DISTINCT field_name FROM issue_fields ORDER BY 1'')
         AS final_result ('|| str ||')';
END;
$$ LANGUAGE plpgsql;
1
Travisty