Handle result when dynamic SQL is in a loop
Asked Answered
A

1

1

I have a bunch of table that have a "stat" column (stat for status ;-)

I would like the count of each stats, and see it!

My tables look like this

create table a (
   a_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
   a_stat status_t
);
create table b (
   b_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
   b_stat status_t
);

status_t is an enum.

So I did this:

DO $$
DECLARE
  tableName RECORD;
  result RECORD;
BEGIN
  SET SEARCH_PATH = projet, public;

  FOR tableName IN SELECT
             c.relname,
             a.attname
           FROM pg_class AS c
             INNER JOIN pg_attribute AS a ON a.attrelid = c.oid
           WHERE a.attname LIKE '%stat' AND c.relkind = 'r' LOOP

      EXECUTE format('SELECT %I, count(%I) FROM %I GROUP BY %I',
                     tableName.attname, tableName.attname, tableName.relname, tableName.attname) INTO result;
      SELECT * FROM result;
  END LOOP;
END;
$$;

There are things that I think I'm not doing well here.

  • There is maybe a better form of format
  • I can't select a RECORD, I think the datatype is not good (but can't figure out what type I should use)
  • A select inside a for loop is not a good idea (I think ?) But I didn't found How to put result into a array of result, and display it after the for loop.

How to do this properly?

Ajax answered 18/9, 2017 at 15:18 Comment(0)
D
1

You cannot return from a DO command. You could raise notices or write to a temporary table to work around this. But rather use a proper function instead. Like this:

CREATE OR REPLACE FUNCTION foo()
  RETURNS TABLE (sch_name text, tbl_name text, col_name text, row_count_notnull int8) AS
$func$
DECLARE
   _sch text;
   _tbl text;
   _col text;
BEGIN
  FOR _sch, _tbl, _col IN
      SELECT c.relnamespace::regnamespace, c.relname, a.attname
      FROM   pg_class     c
      JOIN   pg_attribute a ON a.attrelid = c.oid
      WHERE  c.relnamespace = ANY ('{projet, public}'::regnamespace[])  -- project?
      AND    c.relkind = 'r'           -- only regular tables
      AND    a.attname LIKE '%\_stat'  -- a_stat, b_stat
      AND    a.attnum > 0              -- only user columns (redundant here)
      AND    NOT a.attisdropped        -- exclude dropped columns
   LOOP
      RETURN QUERY EXECUTE format(
         'SELECT $1, $2, $3, count(%I) FROM %I.%I GROUP BY 1'
       , _col, _sch, _tbl)
      USING _sch, _tbl, _col;
   END LOOP;
END
$func$  LANGUAGE plpgsql;

Call:

SELECT * FROM foo();

Major points:

  • You cannot not SELECT without target (SELECT * FROM result;), neither in a DO command nor in a plpgsql function. (You could in a plain SQL function, but you have no loop there.) I return results with RETURN QUERY EXECUTE.

  • Pass values to EXECUTE with the USING clause.

  • Schema-qualify tables in the dynamic query. Else, you might query the wrong table by accident.

  • Does not include the temporary schema like your attempt (even if you were not aware of it). You could add it using pg_my_temp_schema() if you want, but you probably did not want it anyway:

Related:

Search for more examples with dynamic SQL in plpgsql here on SO.

Dallasdalli answered 19/9, 2017 at 2:12 Comment(1)
Thanks, I was wondering if I could do a select at the end of for loop, but your solution with a proper function seems nice, thanksKinder

© 2022 - 2024 — McMap. All rights reserved.