Dynamic CREATE TABLE AS with indexes

  • I'm in the process of migrating our main db from SQL Server to PostgreSQL (while learning it in the process).

    One of the things I need to move are a bunch of stored procedures, that generate tables with dynamic column names. That I got working with no problems. Thing is that some of those tables are later used to grab data to generate yet another table inside another stored procedure. Those subsequent procedures can take a long time to generate, 3-5 minutes sometime. While those are quite complex queries, I suspect this is due to the fact that the dynamically created tables contain no indexes, not even a primary key.

    Below is a sample code for the least complex procedure. Could someone tell me how can I, within that procedure, add a primary key to the first column (material) and then add extra index to the 6th column (total_open)

    CREATE OR REPLACE PROCEDURE sap_data.sp_ssa_po_final()
     LANGUAGE plpgsql
    AS $procedure$
        month_next_5 varchar(3) := to_char(NOW() + interval '5 month', 'mon');
        month_next_4 varchar(3) := to_char(NOW() + interval '4 month', 'mon');
        month_next_3 varchar(3) := to_char(NOW() + interval '3 month', 'mon');
        month_next_2 varchar(3) := to_char(NOW() + interval '2 month', 'mon');
        month_next_1 varchar(3) := to_char(NOW() + interval '1 month', 'mon');
        month_now varchar(3) := to_char(NOW(), 'mon');
        month_prev_1 varchar(3) := to_char(NOW() - interval '1 month', 'mon');
        month_prev_2 varchar(3) := to_char(NOW() - interval '2 month', 'mon');
        month_prev_3 varchar(3) := to_char(NOW() - interval '3 month', 'mon');
        month_prev_4 varchar(3) := to_char(NOW() - interval '4 month', 'mon');
        month_prev_5 varchar(3) := to_char(NOW() - interval '5 month', 'mon');
        month_prev_6 varchar(3) := to_char(NOW() - interval '6 month', 'mon');
        sql_string varchar(3000) := '';


    sql_string := 'CREATE TABLE sap_ssa_po_final AS SELECT
    mat_no AS "material",
    material_descr AS "material_description",
    ('||month_now||'+'||month_prev_1||'+'||month_prev_2||'+'||month_prev_3||'+'||month_prev_4||'+'||month_prev_5||'+'||month_prev_6||') as "open_till_'||month_now||'",
    '||month_next_1||' AS "sum_'||month_next_1||'qty",
    '||month_next_2||' AS "sum
    ('||month_now||'+'||month_prev_1||'+'||month_prev_2||'+'||month_prev_3||'+'||month_prev_4||'+'||month_prev_5||'+'||month_prev_6||'+'||month_next_1||'+'||month_next_2||') as "total_open",
    '||month_next_3||' AS "sum
    '||month_next_4||' AS "sum
    '||month_next_5||' AS "sum
    NOW() as created_date
    FROM v_ssa_po_summarised';

    execute sql_string;

    I did try to google for answers but came up short. 😞

  • I got triggered by the noise in your code. Consider the rewrite.

    While being at it, I also answered your question.

    CREATE OR REPLACE PROCEDURE sap_data.sp_ssa_po_final()
      LANGUAGE plpgsql AS
       mon text[];
       sql_string text;
       -- prepare array with subscripts -6 to +5, and current month at index 0
       SELECT INTO mon
             ('[-6:5]={' || string_agg(to_char(m, 'mon'), ',') || '}')::text[]
       FROM   generate_series(LOCALTIMESTAMP - interval '6 mon'
                            , LOCALTIMESTAMP + interval '5 mon'
                            , interval '1 mon') m;

    sql_string :=
    'CREATE TABLE public.sap_ssa_po_final AS SELECT'
    || concat_ws(E'\n , '
    , E'\n mat_no AS material'
    , 'material_descr AS material_description'
    , '(' || concat_ws('+', mon[0], mon[-1], mon[-2], mon[-3], mon[-4], mon[-5], mon[-6]) || ') AS open_till_' || mon[0]
    , mon[1] || ' AS sum_' || mon[1] || 'qty'
    , mon[2] || ' AS sum
    ' || mon[2] || 'qty'
    , '(' || concat_ws('+', mon[0], mon[-1], mon[-2], mon[-3], mon[-4], mon[-5], mon[-6], mon[1], mon[2]) || ') AS total_open'
    , mon[3] || ' AS sum
    ' || mon[3] || 'qty'
    , mon[4] || ' AS sum
    ' || mon[4] || 'qty'
    , mon[5] || ' AS sum
    ' || mon[5] || '_qty'
    , 'now() AS created_date'
    || E'\nFROM v_ssa_po_summarised;'

    -- RAISE NOTICE '%', mon;
    -- RAISE NOTICE '%', sql_string;
    EXECUTE sql_string;

    -- These can be static. Spell out the schema to be sure!
    ALTER TABLE public.sap_ssa_po_final ADD PRIMARY KEY(material);
    CREATE INDEX ON public.sap_ssa_po_final (total_open);

    Primary key and index can just be created with static code, after the new table has been created. PL/pgSQL plans and executes one statement after the other. (The same would not be possible in a plain SQL function, where the whole function body is parsed at once. But dynamic SQL requires a procedural language anyway.) See:

    • https://stackoverflow.com/a/24771561/939860

    But I would advise to spell out the schema name to avoid mishaps (or even malicious attempts) with the search_path. I used public. (Your dynamic CREATE TABLE statement had no schema, yet.) Adapt to your case. Maybe the temporary schema pg_temp is an option for you? See:

    • https://stackoverflow.com/a/9067777/939860

    I generate the array of months mon with the current month at subscript 0. This allows to simplify quite a bit. About non-standard array subscripts:

    • https://stackoverflow.com/q/12011569/939860

    Note the strategic use of https://www.postgresql.org/docs/current/functions-string.html . See:

    • https://stackoverflow.com/a/12320369/939860

    I work with LOCALTIMESTAMP instead of now() to make clear that the local time zone will be used either way - which makes a difference for corner cases. It's also slightly more efficient with generate_series(). See:

    • https://stackoverflow.com/a/46499873/939860

Suggested Topics

  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2
  • 2