Postgresql 中文操作指南
43.10. Trigger Functions #
PL/pgSQL 可用于在数据更改或数据库事件上定义触发器函数。可以使用 CREATE FUNCTION 命令创建触发器函数,声明它为没有参数且返回类型为 trigger (用于数据更改触发器)或 event_trigger (用于数据库事件触发器)的函数。会自动定义名为 TG _something 的特殊局部变量来描述触发调用的条件。
43.10.1. Triggers on Data Changes #
一个 data change trigger被声明为一个没有参数,返回类型为_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 #
-
BEFORE 、 AFTER 或 INSTEAD OF ,具体取决于触发器的定义。
-
-
TG_LEVEL text #
-
ROW 或 STATEMENT ,具体取决于触发器的定义。
-
-
TG_OP text #
-
触发器被触发的操作:INSERT、UPDATE、DELETE 或 TRUNCATE。
-
-
TG_RELID oid (引用 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 进行前触发器时,返回的值不会产生直接影响,但它必须是非空值,以允许触发器操作进行。请注意,NEW 在 DELETE 触发器中是空值,因此返回该空值通常是不明智的。DELETE 触发器中的常用用法是返回 OLD。
INSTEAD OF 触发器(始终是行级触发器,只能用于视图中)可以返回空值,以发出信号表明它们没有执行任何更新,并且应该跳过对这一行的其余操作(即,不会触发后续触发器,并且不会计算此行来统计周围 INSERT/UPDATE/DELETE 的受影响行)。否则,应该返回非空值,以发出信号表明触发器执行了请求的操作。对于 INSERT 和 UPDATE 操作,返回值应该是 NEW,触发器功能可以修改该 NEW 来支持 INSERT RETURNING 和 UPDATE RETURNING(这也会影响传递给任何后续触发器的行值,或传递给在具有 ON CONFLICT DO UPDATE 子句的 INSERT 语句中带有 EXCLUDED 别名引用的特殊 EXCLUDED 别名引用)。对于 DELETE 操作,返回值应该是 OLD。
在 AFTER 中所触发的行级触发器或在 BEFORE 或 AFTER 中所触发的语句级触发器的返回值总是会被忽略;它也可以是空值。但是,这些类型的任何触发器都可能通过引发错误来中止整个操作。
Example 43.3展示了一个在PL/pgSQL中的触发器函数的例子。
Example 43.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 43.4展示了一个在PL/pgSQL中的审计触发器函数的例子。
Example 43.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 43.5显示了 PL/pgSQL 中视图上的审计触发器的示例。
Example 43.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 43.6显示了 PL/pgSQL 中一个触发器函数的示例,该函数维护数据仓库中事实表的汇总表。
Example 43.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_触发器还可以使用_transition tables_来检查触发语句更改的所有行。_CREATE TRIGGER_命令为一个或两个转换表分配名称,然后该函数可以引用这些名称,就好像它们是只读临时表一样。 Example 43.7展示了一个示例。
Example 43.7. Auditing with Transition Tables
此示例产生了与 Example 43.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();
43.10.2. Triggers on Events #
PL/pgSQL 可用于定义 event triggers。PostgreSQL 要求作为事件触发器调用的函数必须声明为不带参数的函数,返回值类型为_event_trigger_。
当调用 PL/pgSQL 函数作为事件触发器时,将自动在顶级块中创建几个特殊变量。它们是:
Example 43.8显示了 PL/pgSQL 中事件触发器函数的示例。
Example 43.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();