选择 打开 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0
历史版本PostgreSQL 9.0 已于 2015 年 10 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本

39.9. 触发器函数 #

PL/pgSQL 可以被用来定义触发器函数。 触发器函数用 CREATE FUNCTION 命令创建,它被声明为 一个没有参数并且返回类型为 trigger 的函数。注意,即便该函数准备接收一些在 CREATE TRIGGER 中指定的参数 — 这类参数通过 TG_ARGV 传递(如下所述),也必须把它 声明为没有参数。

当一个 PL/pgSQL 函数作为触发器被调用 时,会在顶层块中自动创建一些特殊变量。它们是:

NEW

数据类型为 RECORD;该变量保存行级触发器中用于 INSERT/UPDATE 操作的新数据行。在 语句级触发器和 DELETE 操作中,该变量为 NULL

OLD

数据类型为 RECORD;该变量保存行级触发器中用于 UPDATE/DELETE 操作的旧数据行。在 语句级触发器和 INSERT 操作中,该变量为 NULL

TG_NAME

数据类型为 name;该变量包含实际触发的触发器 的名称。

TG_WHEN

数据类型为 text;根据触发器定义,其值为字符串 BEFOREAFTER

TG_LEVEL

数据类型为 text;根据触发器定义,其值为字符串 ROWSTATEMENT

TG_OP

数据类型为 text;表示触发器对应操作的字符串: INSERTUPDATEDELETETRUNCATE

TG_RELID

数据类型为 oid;导致触发器调用的表的对象 ID。

TG_RELNAME

数据类型为 name;导致触发器调用的表名。该变量 已弃用,未来版本可能移除;请改用 TG_TABLE_NAME

TG_TABLE_NAME

数据类型为 name;导致触发器调用的表名。

TG_TABLE_SCHEMA

数据类型为 name;导致触发器调用的表所在模式名。

TG_NARGS

数据类型为 integerCREATE TRIGGER 语句中给触发器函数的参数 个数。

TG_ARGV[]

数据类型为 text 的数组; CREATE TRIGGER 语句中的参数。索引从 0 开始计。无效索引(小于 0 或大于等于 tg_nargs)会产生空值。

一个触发器函数必须返回 NULL 或者是一个与触发 器为之引发的表结构完全相同的记录/行值。

BEFORE 行级触发器可以返回 null,以通知触发器 管理器跳过该行后续的操作(也就是说,不再触发后续触发器,并且 不会对该行执行 INSERT/UPDATE/DELETE)。如果 返回非 null 值,则操作会继续进行,并使用该行值。返回一个不同 于原始 NEW 的行值会改变即将插入或更新的行。 因此,如果触发器函数希望触发动作正常成功而不修改行值,就必须 返回 NEW(或与之相等的值)。若要修改将被存储 的行,可以直接替换 NEW 中的单个值并返回修改 后的 NEW,或者构造一个完整的新记录/行来返回。 对于作用于 DELETE 的 before 触发器,返回值本身没有直接 效果,但必须为非 null 才能让触发器动作继续。注意,在 DELETE 触发器中 NEW 为 null,因此通常没有理由返回它。在 DELETE 触发器中,常见写法是返回 OLD

一个 AFTER 行级触发器或一个 BEFOREAFTER 语句级触发器的返回值总是会被忽略, 因此也可以返回 null。不过,任何这些类型的触发器可能仍会通过 抛出一个错误来中止整个操作。

例 39.3 展示了 PL/pgSQL 中一个触发器函数的示例。

例 39.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 she 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 PROCEDURE emp_stamp();

另一种记录对表的改变的方法涉及到创建一个新表来为每一个发生的 插入、更新或删除保持一行。这种方法可以被认为是对一个表的改变 的审计。例 39.4 展示了 PL/pgSQL 中一个审计触发器函数的 示例。

例 39.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,
        -- make use of the special variable TG_OP to work out the operation.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*;
            RETURN OLD;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*;
            RETURN NEW;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*;
            RETURN 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 PROCEDURE process_emp_audit();

触发器的一种用法是维护另一个表的汇总表。所得的汇总可以替代 原表用于某些查询 — 运行时间往往大幅缩短。这种技术常用于 数据仓库,其中被测量或观察数据(称为事实表)的表可能极其庞大。 例 39.5 展示了 PL/pgSQL 中一个触发器函数的示例, 它为数据仓库中的一个事实表维护汇总表。

例 39.5. 一个维护汇总表的 PL/pgSQL 触发器函数

这里详述的模式部分基于 Ralph Kimball 的 The Data Warehouse Toolkit 中的 Grocery Store 示例。

--
-- 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 PROCEDURE 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;

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。