Search code examples
postgresqlplpgsqldynamic-sql

How to use variable as table name in plpgsql


I'm new to plpgsql. I'm trying to run a simple query in plpgsql using a variable as table name in plpgsql. But the variable is being interpreted as the table name instead of the value of the variable being interpreted as variable name.

DECLARE
  v_table text;
  z_table text;
  max_id bigint;

BEGIN

FOR v_table IN
    SELECT table_name  
    FROM information_schema.tables 
    WHERE table_catalog = 'my_database' 
    AND table_schema = 'public'
    AND table_name not like 'z_%'
LOOP
    z_table := 'z_' || v_table;
    SELECT max(id) from z_table INTO max_id;
    DELETE FROM v_table where id > max_id;
END LOOP;

Some background information. For every table in my database, I have another table starting with "z_". E.g. for a table called "employee" I have identical table called "z_employee". z_employee contains the same set of data as employee. I use it to restore the employee table at the start of every test.

When I run this function I get the following error:

ERROR:  relation "z_table" does not exist
LINE 1: SELECT max(id) from z_table

My guess is that I'm not allowed to use the variable z_table in the SQL query. At least not the way I'm using it here. But I don't know how it's supposed to be done.


Solution

  • Use dynamic SQL with EXECUTE, simplify, and escape identifiers properly:

    CREATE OR REPLACE FUNCTION f_test()
      RETURNS void AS
    $func$
    DECLARE
       v_table text;
    BEGIN
       FOR v_table IN
          SELECT table_name  
          FROM   information_schema.tables 
          WHERE  table_catalog = 'my_database' 
          AND    table_schema = 'public'
          AND    table_name NOT LIKE 'z_%'
       LOOP
          EXECUTE format('DELETE FROM %I v WHERE v.id > (SELECT max(id) FROM %I)'
                        , v_table, 'z_' || v_table);
       END LOOP;
    END
    $func$  LANGUAGE plpgsql;
    

    Table names may need to be quoted to defend against syntax errors or even SQL injection! I use the convenient format() to concatenate the DELETE statement and escape identifiers properly.

    • A separate SELECT would be more expensive. You can do it all with a single DELETE statement.

    Related:

    Aside:

    You might use the (slightly faster) system catalog pg_tables instead:

          SELECT tablename
          FROM   pg_catalog.pg_tables
          WHERE  schemaname = 'public'
          AND    tablename NOT LIKE 'z_%'
    

    See:

    table_catalog in information_schema.tables has no equivalent here. Only tables of the current database are visible anyway. So the above predicate WHERE table_catalog = 'my_database' produces an empty result set when connected to the wrong database.