Redrock Postgres 搜索 英文
版本: 9.3 / 9.4 / 9.5 / 9.6 / 10 / 11 / 12 / 13 / 14 / 15 / 16 / 17

41.10. 触发函数 #

41.10.1. 数据更改上的触发器
41.10.2. 事件上的触发器

PL/pgSQL 可用于针对数据更改或数据库事件定义触发函数。使用 CREATE FUNCTION 命令创建触发函数,将其声明为不含任何参数且返回类型为 trigger(对于数据更改触发器)或 event_trigger(对于数据库事件触发器)的函数。自动定义名为 TG_something 的特殊本地变量,以描述触发调用的条件。

41.10.1. 数据更改上的触发器 #

数据更改触发器 被声明为不含任何参数且返回类型为 trigger 的函数。注意,即使该函数预期接收在 CREATE TRIGGER 中指定的某些参数,该函数也必须被声明为不含任何参数——如以下所述,这些参数通过 TG_ARGV 传入。

当以触发器的形式调用 PL/pgSQL 函数时,将在顶级块中自动创建几个特殊变量。它们是

NEW 记录 #

在行级别触发器中的 INSERT/UPDATE 操作的新数据库行。此变量在语句级别触发器和 DELETE 操作中为 null。

OLD 记录 #

在行级别触发器中的 UPDATE/DELETE 操作的旧数据库行。此变量在语句级别触发器和 INSERT 操作中为 null。

TG_NAME 名称 #

触发的名称。

TG_WHEN 文本 #

BEFOREAFTERINSTEAD OF,具体取决于触发器的定义。

TG_LEVEL 文本 #

ROWSTATEMENT,具体取决于触发器的定义。

TG_OP 文本 #

触发该触发器的操作:INSERTUPDATEDELETETRUNCATE

TG_RELID oid(引用 pg_class.oid #

触发触发器调用的表的对象 ID。

TG_RELNAME 名称 #

触发触发器调用的表。此项现已弃用,并可能会在未来版本中消失。请改用 TG_TABLE_NAME

TG_TABLE_NAME 名称 #

触发触发器调用的表。

TG_TABLE_SCHEMA 名称 #

触发触发器调用的表的架构。

TG_NARGS 整数 #

CREATE TRIGGER 语句中提供给触发器函数的参数数量。

TG_ARGV 文本[] #

CREATE TRIGGER 语句中的参数。索引从 0 开始计数。无效索引(小于 0 或大于或等于 tg_nargs)将导致一个空值。

触发器函数必须返回 NULL 或一个记录/行值,其结构与触发该触发器的表的结构完全相同。

可以在 BEFORE 前触发的行级触发器返回 null,以向触发器管理器发出信号,以针对此行跳过操作的其余部分(即,不触发后续触发器,并且对该行不执行 INSERT/UPDATE/DELETE)。如果返回非空值,则操作将继续使用该行值。返回不同于 NEW 的原始值的行值会更改将要插入或更新的行。因此,如果触发器函数希望触发操作正常进行而不会更改行值,则必须返回 NEW(或等于它的值)。若要更改要存储的行,可以直接用 NEW 中的单个值进行替换并返回修改后的 NEW,或构建一个完整的新记录/行进行返回。在 DELETE 上的 before 触发器的情况下,返回的值没有直接影响,但它必须为非空,才能允许触发操作继续进行。请注意,NEWDELETE 触发器中为 null,因此返回通常不会明智。DELETE 触发器中的习惯用法是返回 OLD

INSTEAD OF 触发器(它总是行级触发器,并且只能在视图上使用)可以返回 null 以表明它们没有执行任何更新,并且对于此行的操作的其余部分应跳过(即,不触发后续触发器,并且在周围的 INSERT/UPDATE/DELETE 的受影响行状态中未统计该行)。否则应返回非空值,以表明触发器执行了请求的操作。对于 INSERTUPDATE 操作,返回值应为 NEW,触发器函数可能会修改它来支持 INSERT RETURNINGUPDATE RETURNING(这也将影响传递给任何后续触发器的行值,或传递给特殊 EXCLUDED 别名引用(在带有 ON CONFLICT DO UPDATE 子句的 INSERT 语句中)。对于 DELETE 操作,返回值应为 OLD

AFTER 触发的行级触发器的返回值或由 BEFOREAFTER 触发的语句级触发器的返回值始终将被忽略;它也可能是 null。然而,任何此类类型的触发器仍会通过引发错误来中止整个操作。

示例 41.3显示了 PL/pgSQL 中触发器函数的示例。

示例 41.3 PL/pgSQL 触发器函数

此示例触发器可确保每当在表中插入或更新行时,当前用户名和时间都已加盖到行中。并且,它会检查员工姓名是否已提供,并且工资是否为正值。

CREATE TABLE emp (
    empname           text,
    salary            integer,
    last_date         timestamp,
    last_user         text
);

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Check that empname and salary are given
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname cannot be null';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% cannot have null salary', NEW.empname;
        END IF;

        -- Who works for us when they must pay for it?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
        END IF;

        -- Remember who changed the payroll when
        NEW.last_date := current_timestamp;
        NEW.last_user := current_user;
        RETURN NEW;
    END;
$emp_stamp$ LANGUAGE plpgsql;

CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp
    FOR EACH ROW EXECUTE FUNCTION emp_stamp();

记录表变更的另一种方法涉及创建一个新表,其中保存对发生插入、更新或删除的每一行。此方法可被认为是审计表变更。示例 41.4显示了 PL/pgSQL 中的审计触发器函数示例。

示例 41.4 PL/pgSQL 用于审计的触发器函数

此示例触发器可确保 emp 表中的任何插入、更新或删除都将被记录(即审计)在 emp_audit 表中。将当前时间和用户名加盖到行中,以及执行的操作类型。

CREATE TABLE emp (
    empname           text NOT NULL,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    stamp             timestamp NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer
);

CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- Create a row in emp_audit to reflect the operation performed on emp,
        -- making use of the special variable TG_OP to work out the operation.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit SELECT 'D', now(), current_user, OLD.*;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit SELECT 'U', now(), current_user, NEW.*;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit SELECT 'I', now(), current_user, NEW.*;
        END IF;
        RETURN NULL; -- result is ignored since this is an AFTER trigger
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
    FOR EACH ROW EXECUTE FUNCTION process_emp_audit();

前一示例的变体使用将主表与审计表结合的视图,以显示上次修改每次输入的时间。此方法仍会记录表的变更全审计跟踪,但还会显示审计跟踪的简化视图,仅显示每个输入的审计跟踪派生的上次修改时间戳。示例 41.5显示了 PL/pgSQL 中视图的审计触发器示例。

示例 41.5 PL/pgSQL 用于审计的视图触发器函数

此示例使用视图触发器使视图可更新,并确保 emp_audit 表中会记录(即,审计)视图中任何行的插入、更新或删除情况。将记录当前时间和用户名,以及执行的操作类型,并且视图将显示每行的上次修改时间。

CREATE TABLE emp (
    empname           text PRIMARY KEY,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer,
    stamp             timestamp NOT NULL
);

CREATE VIEW emp_view AS
    SELECT e.empname,
           e.salary,
           max(ea.stamp) AS last_updated
      FROM emp e
      LEFT JOIN emp_audit ea ON ea.empname = e.empname
     GROUP BY 1, 2;

CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
    BEGIN
        --
        -- Perform the required operation on emp, and create a row in emp_audit
        -- to reflect the change made to emp.
        --
        IF (TG_OP = 'DELETE') THEN
            DELETE FROM emp WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            OLD.last_updated = now();
            INSERT INTO emp_audit VALUES('D', current_user, OLD.*);
            RETURN OLD;
        ELSIF (TG_OP = 'UPDATE') THEN
            UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('U', current_user, NEW.*);
            RETURN NEW;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp VALUES(NEW.empname, NEW.salary);

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('I', current_user, NEW.*);
            RETURN NEW;
        END IF;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
    FOR EACH ROW EXECUTE FUNCTION update_emp_view();

触发器的一种用法是维护另一个表的汇总表。由此产生的汇总可以用于某些查询(通常极大地减少了运行时间),以代替原始表。此技术通常用于数据仓储中,其中测量或观测数据的表(称为事实表)可能非常大。示例 41.6展示了一个 PL/pgSQL 中的触发器示例函数,用于维护数据仓库中事实表的汇总表。

示例 41.6。用于维护汇总表的 PL/pgSQL 触发器函数

这里详细介绍的模式部分基于 Ralph Kimball 编著的 数据仓库工具包 中的 杂货店 示例。

--
-- Main tables - time dimension and sales fact.
--
CREATE TABLE time_dimension (
    time_key                    integer NOT NULL,
    day_of_week                 integer NOT NULL,
    day_of_month                integer NOT NULL,
    month                       integer NOT NULL,
    quarter                     integer NOT NULL,
    year                        integer NOT NULL
);
CREATE UNIQUE INDEX time_dimension_key ON time_dimension(time_key);

CREATE TABLE sales_fact (
    time_key                    integer NOT NULL,
    product_key                 integer NOT NULL,
    store_key                   integer NOT NULL,
    amount_sold                 numeric(12,2) NOT NULL,
    units_sold                  integer NOT NULL,
    amount_cost                 numeric(12,2) NOT NULL
);
CREATE INDEX sales_fact_time ON sales_fact(time_key);

--
-- Summary table - sales by time.
--
CREATE TABLE sales_summary_bytime (
    time_key                    integer NOT NULL,
    amount_sold                 numeric(15,2) NOT NULL,
    units_sold                  numeric(12) NOT NULL,
    amount_cost                 numeric(15,2) NOT NULL
);
CREATE UNIQUE INDEX sales_summary_bytime_key ON sales_summary_bytime(time_key);

--
-- Function and trigger to amend summarized column(s) on UPDATE, INSERT, DELETE.
--
CREATE OR REPLACE FUNCTION maint_sales_summary_bytime() RETURNS TRIGGER
AS $maint_sales_summary_bytime$
    DECLARE
        delta_time_key          integer;
        delta_amount_sold       numeric(15,2);
        delta_units_sold        numeric(12);
        delta_amount_cost       numeric(15,2);
    BEGIN

        -- Work out the increment/decrement amount(s).
        IF (TG_OP = 'DELETE') THEN

            delta_time_key = OLD.time_key;
            delta_amount_sold = -1 * OLD.amount_sold;
            delta_units_sold = -1 * OLD.units_sold;
            delta_amount_cost = -1 * OLD.amount_cost;

        ELSIF (TG_OP = 'UPDATE') THEN

            -- forbid updates that change the time_key -
            -- (probably not too onerous, as DELETE + INSERT is how most
            -- changes will be made).
            IF ( OLD.time_key != NEW.time_key) THEN
                RAISE EXCEPTION 'Update of time_key : % -> % not allowed',
                                                      OLD.time_key, NEW.time_key;
            END IF;

            delta_time_key = OLD.time_key;
            delta_amount_sold = NEW.amount_sold - OLD.amount_sold;
            delta_units_sold = NEW.units_sold - OLD.units_sold;
            delta_amount_cost = NEW.amount_cost - OLD.amount_cost;

        ELSIF (TG_OP = 'INSERT') THEN

            delta_time_key = NEW.time_key;
            delta_amount_sold = NEW.amount_sold;
            delta_units_sold = NEW.units_sold;
            delta_amount_cost = NEW.amount_cost;

        END IF;


        -- Insert or update the summary row with the new values.
        <<insert_update>>
        LOOP
            UPDATE sales_summary_bytime
                SET amount_sold = amount_sold + delta_amount_sold,
                    units_sold = units_sold + delta_units_sold,
                    amount_cost = amount_cost + delta_amount_cost
                WHERE time_key = delta_time_key;

            EXIT insert_update WHEN found;

            BEGIN
                INSERT INTO sales_summary_bytime (
                            time_key,
                            amount_sold,
                            units_sold,
                            amount_cost)
                    VALUES (
                            delta_time_key,
                            delta_amount_sold,
                            delta_units_sold,
                            delta_amount_cost
                           );

                EXIT insert_update;

            EXCEPTION
                WHEN UNIQUE_VIOLATION THEN
                    -- do nothing
            END;
        END LOOP insert_update;

        RETURN NULL;

    END;
$maint_sales_summary_bytime$ LANGUAGE plpgsql;

CREATE TRIGGER maint_sales_summary_bytime
AFTER INSERT OR UPDATE OR DELETE ON sales_fact
    FOR EACH ROW EXECUTE FUNCTION maint_sales_summary_bytime();

INSERT INTO sales_fact VALUES(1,1,1,10,3,15);
INSERT INTO sales_fact VALUES(1,2,1,20,5,35);
INSERT INTO sales_fact VALUES(2,2,1,40,15,135);
INSERT INTO sales_fact VALUES(2,3,1,10,1,13);
SELECT * FROM sales_summary_bytime;
DELETE FROM sales_fact WHERE product_key = 1;
SELECT * FROM sales_summary_bytime;
UPDATE sales_fact SET units_sold = units_sold * 2;
SELECT * FROM sales_summary_bytime;

AFTER 触发器还可以使用转换表来检查触发语句更改的整个行集。CREATE TRIGGER 命令为一个或两个转换表分配名称,然后函数可以将这些名称称为只读临时表。示例 41.7 展示了一个示例。

示例 41.7。使用转换表进行审核

此示例生成的结果与 示例 41.4 相同,但没有使用对每一行触发一次的触发器,而是使用在转换表中收集相关信息后对每个语句触发一次的触发器。当调用语句修改许多行时,这明显比按行触发的方法更快。请注意,我们必须针对每种事件制定单独的触发器声明,因为对于每种情况 REFERENCING 子句必须不同。但这不妨碍我们在选择时使用单个触发器函数。(实际上,最好使用三个单独的函数,避免对 TG_OP 的运行时测试。)

CREATE TABLE emp (
    empname           text NOT NULL,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    stamp             timestamp NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer
);

CREATE OR REPLACE FUNCTION process_emp_audit() RETURNS TRIGGER AS $emp_audit$
    BEGIN
        --
        -- Create rows in emp_audit to reflect the operations performed on emp,
        -- making use of the special variable TG_OP to work out the operation.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit
                SELECT 'D', now(), current_user, o.* FROM old_table o;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit
                SELECT 'U', now(), current_user, n.* FROM new_table n;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit
                SELECT 'I', now(), current_user, n.* FROM new_table n;
        END IF;
        RETURN NULL; -- result is ignored since this is an AFTER trigger
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit_ins
    AFTER INSERT ON emp
    REFERENCING NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_upd
    AFTER UPDATE ON emp
    REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_del
    AFTER DELETE ON emp
    REFERENCING OLD TABLE AS old_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();

41.10.2。事件触发器 #

可以使用 PL/pgSQL 定义 事件触发器PostgreSQL 要求将作为事件触发器调用的函数声明为不带参数且返回类型为 event_trigger 的函数。

PL/pgSQL 函数作为事件触发器调用时,在顶层块中自动创建了几个特殊变量。它们是

TG_EVENT text #

触发器触发的事件。

TG_TAG text #

触发器触发的命令标签。

示例 41.8 显示了一个事件触发器函数的示例,采用 PL/pgSQL

示例 41.8. PL/pgSQL 事件触发器函数

这个示例触发器只是在每次执行受支持命令时触发一个 NOTICE 消息。

CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
    RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE FUNCTION snitch();