Utility functions for enabling/disabling fkey triggers - Mailing list pgsql-performance

From Craig Ringer
Subject Utility functions for enabling/disabling fkey triggers
Date
Msg-id 47D4FB94.9080601@postnewspapers.com.au
Whole thread Raw
List pgsql-performance
Hi all

I've just spent some time working with PostgreSQL 8.3 trying to get a 90
minute job to run in a reasonable amount of time, and in the process
I've come up with something that I thought others might find useful.

Attached is a pair of PL/PgSQL functions that enable/disable the
triggers associated with a given foreign key constraint. They use the
system catalogs to obtain all the required information about the
involved tables. A fairly fast consistency check is performed before
re-enabling the triggers.

As it turns out I don't need it after all, but I though that others
doing really large data imports might given messages like:

http://archives.postgresql.org/pgsql-performance/2003-03/msg00157.php



I wrote it because I was frustrated with the slow execution of the ALTER
TABLE ... ADD CONSTRAINT ... FOREIGN KEY statements I was running to
rebuild the foreign key constraints on some of my tables after some bulk
imports. Leaving the constraints enabled was resulting in execution time
that increased for every record inserted, and rebuilding them after the
insert wasn't much faster.

Unfortunately it turns out that the issue wasn't with the way ALTER
TABLE ... ADD CONSTRAINT ... FOREIGN KEY was doing the check, as the
integrity check run by those functions is almost as slow as the ALTER
TABLE in the context of the transaction they're run in - and both run in
< 1 second outside of a transaction context or in a separate transaction.

Oh well, maybe the code will be useful to somebody anyway.

--
Craig Ringer
--
-- This file defines functions to (hopefully) reasonably safely enable and
-- disable enforcement of a foreign key constraint, written by Craig Ringer.
-- They're free for any use your care to make of them.
--
-- These functions work on the system that they're used on, but you
-- should still evaluate them for correctness and sanity before
-- adopting them yourself.
--
-- I make no guarantees that they won't destroy your data or steal your
-- lunch.
--


CREATE OR REPLACE FUNCTION disable_triggers_for_fkey_constraint(constraint_name VARCHAR) RETURNS void AS $$
DECLARE
    tgrec RECORD;
    relname VARCHAR;
    constraint_type CHAR;
BEGIN
    SELECT contype
    INTO constraint_type
    FROM pg_catalog.pg_constraint
    WHERE pg_catalog.pg_constraint.conname = constraint_name;

    IF constraint_type <> 'f' THEN
        RAISE EXCEPTION 'Can only disable triggers for foreign key constraints';
    END IF;

    FOR tgrec IN SELECT tgname FROM pg_catalog.pg_trigger WHERE tgconstrname = constraint_name
    LOOP
        -- Obtain the name of the table this trigger affects. Foreign key
        -- constraint triggers may affect the fkey or pkey tables and we have
        -- to find out which in order to disable the constraint.
        SELECT pg_catalog.pg_class.relname
        INTO STRICT relname
        FROM pg_catalog.pg_class INNER JOIN pg_catalog.pg_trigger
          ON pg_catalog.pg_trigger.tgrelid = pg_catalog.pg_class.oid
        WHERE pg_catalog.pg_trigger.tgname=tgrec.tgname;

        EXECUTE 'ALTER TABLE "'||relname||'" DISABLE TRIGGER "'||tgrec.tgname||'";';
    END LOOP;
END;
$$ LANGUAGE 'plpgsql' VOLATILE;

COMMENT ON FUNCTION disable_triggers_for_fkey_constraint(VARCHAR) IS 'Disable enforcement of foreign key constraint
$1';


--
-- This stored procedure does a rapid check of the referential integrity protected by `constraint_name'
-- (MUCH faster than the incredibly slow one postgresql does during ALTER TABLE ... ADD CONSTRAINT ... FOREIGN KEY ...)
-- then re-enables the triggers that enforce the constraint.
--
-- It only works on foreign keys with only one column involved.
--
CREATE OR REPLACE FUNCTION enable_triggers_for_fkey_constraint(constraint_name VARCHAR) RETURNS void AS $$
DECLARE
    tgrec RECORD;
    relname VARCHAR;
    foreign_key_misses RECORD;
    constraint_info RECORD;
    fkey_table_name VARCHAR;
    pkey_table_name VARCHAR;
    fkey_col_list VARCHAR;
    fkey_col_not_null_clause VARCHAR;
    pkey_col_list VARCHAR;
    colname VARCHAR; -- temporary variable
    -- Used to control comma insertion in loops
    first BOOLEAN;
    -- Loop variables
    i INTEGER;
    -- Query text
    q VARCHAR;
BEGIN

    -- Look up the tables and columns that the foreign key involves
    SELECT
        contype,
        conrelid, -- oid of referencing relation
        confrelid, -- oid of referenced relation
        (SELECT pg_catalog.pg_type.typname FROM pg_catalog.pg_type WHERE pg_catalog.pg_type.typrelid = conrelid) AS
conrelid_name,-- name of referencing relation 
        (SELECT pg_catalog.pg_type.typname FROM pg_catalog.pg_type WHERE pg_catalog.pg_type.typrelid = confrelid) AS
confrelid_name,-- name of referenced relation 
        pg_catalog.pg_constraint.conkey, -- Position of referencing column, eg {14}
        pg_catalog.pg_constraint.confkey -- Position of referenced column, eg {1}
    INTO STRICT constraint_info
    FROM pg_catalog.pg_constraint
    WHERE pg_catalog.pg_constraint.conname = constraint_name;

    IF constraint_info.contype <> 'f' THEN
        RAISE EXCEPTION 'Can only enable triggers for foreign key constraints';
    END IF;

    fkey_table_name := constraint_info.conrelid_name;
    pkey_table_name := constraint_info.confrelid_name;

    -- Now we need to build SQL snippets for:
    --   the fkey table column list
    --   a WHERE clause snippet to exclude foreign key values where all fields are NULL from the check
    --   the pkey table column list
    first := 't';
    fkey_col_list := '';
    fkey_col_not_null_clause := '';
    FOR i IN array_lower(constraint_info.conkey,1)..array_upper(constraint_info.conkey,1) LOOP
        IF first THEN
            first := 'f';
            fkey_col_not_null_clause := '(';
        ELSE
            fkey_col_list := fkey_col_list||', ';
            fkey_col_not_null_clause := fkey_col_not_null_clause||' AND ';
        END IF;

        SELECT pg_catalog.pg_attribute.attname
        INTO colname
        FROM pg_catalog.pg_attribute
        WHERE pg_catalog.pg_attribute.attnum = constraint_info.conkey[i] AND pg_catalog.pg_attribute.attrelid =
constraint_info.conrelid;

        fkey_col_list := fkey_col_list||'"'||colname||'"';
        fkey_col_not_null_clause := fkey_col_not_null_clause||colname||' IS NOT NULL';
    END LOOP;
    fkey_col_not_null_clause := fkey_col_not_null_clause||')';

    first := 't';
    pkey_col_list := '';
    FOR i IN array_lower(constraint_info.conkey,1)..array_upper(constraint_info.confkey,1) LOOP
        IF first THEN
            first := 'f';
        ELSE
            pkey_col_list := pkey_col_list||', ';
        END IF;

        SELECT pg_catalog.pg_attribute.attname
        INTO colname
        FROM pg_catalog.pg_attribute
        WHERE pg_catalog.pg_attribute.attnum = constraint_info.confkey[i] AND pg_catalog.pg_attribute.attrelid =
constraint_info.confrelid;

        pkey_col_list := pkey_col_list||'"'||colname||'"';
    END LOOP;

    -- An optimised foreign key check, found at
    -- http://archives.postgresql.org/pgsql-performance/2003-03/msg00157.php
    -- and adapted for correct handling of NULLs, improved efficiency with many
    -- similar foreign key entries, handling of multiple key columns.
    --
    -- It's highly efficient if the foreign key table is a similar or larger
    -- size than the primary key table, but doesn't run as fast as an indexed
    -- SELECT when the primary key table is much larger then the list of
    -- foreign keys being checked. In that case, though, you won't be using
    -- these functions...
    --
    -- If any results are returned by this query, they're foreign key entries without
    -- matching primary key entries, so if the loop body is executed there's a referential
    -- integrity error.
    --
    q := '
        SELECT '||fkey_col_list||'
        FROM (
            SELECT DISTINCT '||fkey_col_list||', 0 AS pri FROM "'||fkey_table_name||'" WHERE
'||fkey_col_not_null_clause||'
            UNION ALL
            SELECT '||pkey_col_list||', 1 AS pri FROM "'||pkey_table_name||'"
        ) AS key_info
        GROUP BY '||fkey_col_list||'
        HAVING sum(pri) = 0';

    --RAISE NOTICE 'About to execute: %',q;

    FOR foreign_key_misses IN EXECUTE q
    LOOP
        RAISE EXCEPTION 'Foreign key constraint check failed on %(%)',fkey_table_name,fkey_col_list;
    END LOOP;
    RAISE NOTICE 'Referential integrity check on %(%) REFERENCES %(%) passed - activating
triggers',fkey_table_name,fkey_col_list,pkey_table_name,pkey_col_list;

    FOR tgrec IN SELECT tgname FROM pg_catalog.pg_trigger WHERE tgconstrname = constraint_name
    LOOP
        -- Obtain the name of the table this trigger affects. Foreign key
        -- constraint triggers may affect the fkey or pkey tables and we have
        -- to find out which in order to enable the constraint.
        SELECT pg_catalog.pg_class.relname
        INTO STRICT relname
        FROM pg_catalog.pg_class INNER JOIN pg_catalog.pg_trigger
          ON pg_catalog.pg_trigger.tgrelid = pg_catalog.pg_class.oid
        WHERE pg_catalog.pg_trigger.tgname=tgrec.tgname;

        EXECUTE 'ALTER TABLE "'||relname||'" ENABLE TRIGGER "'||tgrec.tgname||'";';
    END LOOP;
END;
$$ LANGUAGE 'plpgsql' VOLATILE;

COMMENT ON FUNCTION enable_triggers_for_fkey_constraint(VARCHAR) IS 'Quickly re-check and enable enforcement of foreign
keyconstraint $1'; 

pgsql-performance by date:

Previous
From: Greg Smith
Date:
Subject: Re: UPDATE 66k rows too slow
Next
From: Craig Ringer
Date:
Subject: Very slow (2 tuples/second) sequential scan after bulk insert; speed returns to ~500 tuples/second after commit