postgresqlpivot-tableplpgsqldynamic-sqlpostgresql-9.1

Dynamically generate columns for crosstab in PostgreSQL


I am trying to create crosstab queries in PostgreSQL such that it automatically generates the crosstab columns instead of hardcoding it. I have written a function that dynamically generates the column list that I need for my crosstab query. The idea is to substitute the result of this function in the crosstab query using dynamic sql.

How do I do it in PostgreSQL?

-- 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 ?

Solution

  • You can use the provided C function crosstab_hash for this.

    The manual is not very clear in this respect. It's mentioned at the end of the chapter on crosstab() with two parameters:

    You can create predefined functions to avoid having to write out the result column names and types in each query. See the examples in the previous section. The underlying C function for this form of crosstab is named crosstab_hash.

    For your example:

    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;
    

    Call:

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

    Note that you need to create a distinct crosstab_hash function for every crosstab function with a different return type.

    Related:


    Your function to generate the column list is rather convoluted, the result is incorrect (int missing after kernel_id), it can be replaced with this SQL query:

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

    And it cannot be used dynamically anyway.