PL/pgSQL 可用于为数据变更或数据库事件
定义触发器函数。触发器函数用 CREATE FUNCTION
命令创建,声明为一个不带参数、返回类型为
trigger(用于数据变更触发器)或
event_trigger(用于数据库事件触发器)的函数。
名为 TG_
的特殊局部变量会被自动定义,用以描述引发这次调用的条件。
something
数据变更触发器被声明为一个
不带参数、返回类型为 trigger 的函数。
注意,即使函数预期接收在 CREATE TRIGGER
中指定的一些参数,它也必须声明为不带参数 ——
这些参数通过 TG_ARGV 传递,如下所述。
当 PL/pgSQL 函数作为触发器被调用时, 顶层块中会自动创建若干特殊变量。它们是:
NEW record #
行级触发器中 INSERT/UPDATE
操作的新数据库行。在语句级触发器中以及对于
DELETE 操作,此变量为空。
OLD record #
行级触发器中 UPDATE/DELETE
操作的旧数据库行。在语句级触发器中以及对于
INSERT 操作,此变量为空。
TG_NAME name #被触发的触发器的名字。
TG_WHEN text #
BEFORE、AFTER 或
INSTEAD OF,取决于触发器的定义。
TG_LEVEL text #
ROW 或 STATEMENT,
取决于触发器的定义。
TG_OP text #
触发器为之被引发的操作:
INSERT、UPDATE、
DELETE 或 TRUNCATE。
TG_RELID oid (references pg_class.oid) #引发触发器调用的表的对象 ID。
TG_RELNAME name #
引发触发器调用的表。它现在已被弃用,
并可能在未来的版本中消失。请改用
TG_TABLE_NAME。
TG_TABLE_NAME name #引发触发器调用的表。
TG_TABLE_SCHEMA name #引发触发器调用的表所在的模式。
TG_NARGS integer #
在 CREATE TRIGGER 语句中给触发器函数
的参数个数。
TG_ARGV text[] #
来自 CREATE TRIGGER 语句的参数。
索引从 0 开始计数。无效的索引(小于 0 或大于等于
tg_nargs)将产生空值。
触发器函数必须返回 NULL,
或者返回一个与触发它的表具有完全相同结构的记录/行值。
以 BEFORE 触发的行级触发器可以返回空值,
以通知触发器管理器跳过对该行操作的剩余部分
(即,后续的触发器不会被引发,且该行的
INSERT/UPDATE/DELETE
不会发生)。如果返回了非空值,则操作将以该行值继续进行。
返回一个与 NEW 原始值不同的行值
会改变将要被插入或更新的行。因此,如果触发器函数希望触发动作
在不更改行值的情况下正常成功,就必须返回
NEW(或与之相等的值)。
要更改将被存储的行,可以直接替换 NEW 中的
单个值并返回修改后的 NEW,
或者构建一个全新的记录/行来返回。对于
DELETE 上的 before 触发器,
返回的值没有直接作用,但它必须非空才能让触发器动作继续进行。
注意,在 DELETE 触发器中
NEW 为空,所以返回它通常没有意义。
DELETE 触发器中的常见惯用法是返回
OLD。
INSTEAD OF 触发器(它们总是行级触发器,
且只能用在视图上)可以返回空值,以表明它们没有执行任何更新,
并且对该行操作的剩余部分应当被跳过(即,后续的触发器不会被引发,
该行也不会被计入外层
INSERT/UPDATE/DELETE
的受影响行数状态)。否则应返回一个非空值,
以表明触发器执行了所请求的操作。对于
INSERT 和 UPDATE 操作,
返回值应当是 NEW,触发器函数可以修改它,
以支持 INSERT RETURNING 和
UPDATE RETURNING(这也会影响传递给任何后续触发器的
行值,或传递给带 ON CONFLICT DO UPDATE 子句的
INSERT 语句中特殊 EXCLUDED
别名引用的行值)。对于 DELETE 操作,
返回值应当是 OLD。
以 AFTER 触发的行级触发器,
以及以 BEFORE 或 AFTER
触发的语句级触发器,其返回值总是被忽略;
返回空值也无妨。不过,这些类型的任何触发器
仍然可以通过引发错误来中止整个操作。
Example 41.3 给出了一个 PL/pgSQL 触发器函数的示例。
Example 41.3. A PL/pgSQL Trigger Function
这个示例触发器确保每当表中有行被插入或更新时, 当前用户名和时间都会被写入该行。它还检查是否给出了 雇员姓名,以及薪水是否为正值。
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();
记录对表的更改的另一种方式是创建一张新表, 为发生的每次插入、更新或删除保存一行。 这种做法可以视为对表变更的审计。 Example 41.4 给出了一个 PL/pgSQL 审计触发器函数的示例。
Example 41.4. A PL/pgSQL Trigger Function for Auditing
这个示例触发器确保对 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();
前一个示例的一个变体使用一个把主表与审计表连接起来的视图, 以显示每个条目最后一次被修改的时间。这种方法仍然记录 表变更的完整审计轨迹,同时呈现审计轨迹的一个简化视图, 只显示从审计轨迹为每个条目推导出的最后修改时间戳。 Example 41.5 给出了一个 PL/pgSQL 中视图上的审计触发器示例。
Example 41.5. A PL/pgSQL View Trigger Function for Auditing
这个示例在视图上使用触发器使其可更新,
并确保对视图中任何行的插入、更新或删除都会被记录
(即审计)到 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();
触发器的一种用途是维护另一张表的汇总表。 对于某些查询,得到的汇总可以用来替代原始表 —— 通常能大幅缩短运行时间。 这项技术常用于数据仓库,其中测量或观测数据的表 (称为事实表)可能极其庞大。 Example 41.6 给出了一个 PL/pgSQL 触发器函数示例, 它为数据仓库中的事实表维护一张汇总表。
Example 41.6. A PL/pgSQL Trigger Function for Maintaining a Summary Table
这里详述的模式部分基于 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 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
命令为一个或两个过渡表指派名字,然后函数就可以引用这些名字,
就像它们是只读的临时表一样。
Example 41.7
给出了一个示例。
Example 41.7. Auditing with Transition Tables
这个示例产生的结果与
Example 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();
PL/pgSQL 可以用来定义
事件触发器。
PostgreSQL 要求,将被作为事件触发器
调用的函数必须声明为一个不带参数、返回类型为
event_trigger 的函数。
当 PL/pgSQL 函数作为事件触发器被调用时, 顶层块中会自动创建若干特殊变量。它们是:
Example 41.8 给出了一个 PL/pgSQL 事件触发器函数的示例。
Example 41.8. A PL/pgSQL Event Trigger Function
这个示例触发器在每次有受支持的命令被执行时
简单地引发一条 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();