triggers功能介绍
triggers功能介绍
首先得提一下triggers函数,triggers函数与用户自定义函数类似,也是用户自定义的“函数”,不同的是triggers函数不需要在 sql 中显示调用执行,而是由数据库系统在特定事件发生时自动去执行的(例如表/视图上的增删改等)。因此,用户的在创建triggers函数这一特殊“函数”时,除了定义函数体之外,还需要指定该“函数”的执行时机,即指定”哪些事件“(增/删/该)发生在”哪个对象“(表/视图)之前/后时执行triggers函数,这样以来,一个完整的triggers就创建出来了。
所以triggers共包含两部分内容:1、triggers函数;2、triggers的定义(即定义triggers函数的执行时机)。
triggers的特性使其成为数据审计、保证数据一致性的强大工具,例如可以使用triggers自动记录表上的增删改操作,使用triggers实现跨越多个表的约束检查或者实现多个表的级联更新等等。另外一个有意思的地方,PG的外键便是基于triggers实现的。
目前,大部分成熟的数据库产品都提供了triggers的功能,KlustronDB 自然也不例外(版本 1.2 开始支持)。本文将详细介绍triggers的实现原理,帮助用户更好地使用triggers。
原理
KlustronDB 支持行级和语句级两种级别的triggers。
- 行(row)级的triggers。针对每行修改的元组调用一次函数;
- 语句(statement)级triggers。针对每条增删改语句调用一次函数;
一、行级triggers
行triggers又分为前triggers和后triggers,分别在修改某一行元组的前后执行,其大致的处理逻辑如下面的流程图所示。可以看到前triggers在修改具体的行前立即执行,而后triggers并没有立即执行,而是将其缓存起来(包括执行所需要的信息,例如行修改前后的状态等),当所有元组都更新完毕之后,这些缓存的后triggers才会执行。

二、语句级triggers
行级triggers类似,语句级triggers也分为前triggers和后triggers,其中前triggers在 DML 执行之前会执行,而后triggers在 DML 执行完之后才会执行。和行后triggers类似,语句级后triggers也是先被加入到triggers缓冲队列中的,然后再由数据库系统统一遍历并执行triggers队列中缓存的所有aftertriggers(包括行级和语句级的triggers)。其大致的处理逻辑如下图所示。

三、triggers参数
和普通的用户自定义函数不同,triggers函数是由数据库系统自动调用执行的。在调用普通的函数时,我们可以指定传入的参数,那么对于由数据库系统自动调用的triggers函数,它的参数是什么?又是如何传入的呢? 其实triggers函数被要求声明为无参数的,但是数据库系统在调用它时向它传递了一个“执行上下文”,这个上下文中包含了triggers函数调用时数据的状态变化信息以及创建triggers时事先存放传递的信息,triggers函数就是通过这个上下文执行差异化的操作的。
如下所示的TriggerData结构体便是triggers的上下文信息。
typedef struct TriggerData
{
NodeTag type;
TriggerEvent tg_event; // 事件(更新、插入或者删除事件)
Relation tg_relation; // triggers所针对的表
HeapTuple tg_trigtuple; // 更新前的元组值
HeapTuple tg_newtuple; // 更新后的元组值
...
Tuplestorestate *tg_oldtable; // 所有被更新的元组在更新前的值
Tuplestorestate *tg_newtable; // 所有被更新的元组在更新后的值
} TriggerData;
当然,triggers函数并不是直接访问这个上下文结构体的成员,而是通过事先约定好的变量名来访问(triggers函数一般使用 plpgsql 语句来书写,简单起见,后续的讲解默认使用的是plpgsql)。例如通过OLD和NEW变量名分别表示元组更新前后的值,通过引用这两个变量就能访问上下文结构体中tg_rigtuple和tg_newtuple成员了。更多预先定义变量名及其含义可以参考文档.
另外,结构体中的tg_oldtable和tg_newtable两个成员,它们分别存储 DML 语句执行期间更新的所有元组的状态(即更新前后的值),它主要用于语句级的后triggers。根据前面介绍,行级后triggers和语句级后triggers的一样,也是在 DML 语句执行完毕之后才执行的,因此行级后triggers也能够访问这两个成员。这两个成员是以“表”的形式被triggers函数引用的,并且表名可以在创建triggers的时候自行指定。下面以例子来说明:
-- 创建UPDATE语句级triggers函数.
-- 其中oldtable/newtable用来引用上下文中的tg_oldtable/tg_newtable;
-- 功能:将UPDATE语句执行前后的元组的变化打印出来.
create function dump_update() returns trigger as $$
begin
raise notice 'trigger = %, old table = %, new table = %',
TG_NAME, -- triggers名称
(select string_agg((a,b)::text, ', ' order by a) from oldtable),
(select string_agg((a,b)::text, ', ' order by a) from newtable);
return null;
end;
$$ language plpgsql;
-- 创建语句级triggers
create trigger update_dump_trigger AFTER UPDATE ON t -- triggers名称以及触发的时机(更新表t之后)
REFERENCING OLD TABLE AS oldtable -- triggers函数使用oldtable引用tg_oldtable
NEW TABLE AS newtable -- triggers函数使用newtable引用tg_newtable
FOR EACH STATEMENT -- triggers级别(语句级triggers)
EXECUTE PROCEDURE dump_update(); -- triggers函数
-- 执行UPDATE
postgres=# update t set b=10+b where b=2;
NOTICE: trigger = update_dump_trigger, old table = (AAA,2), (BBB,2), new table = (AAA,12), (BBB,12)
UPDATE 2
然而,这些变量/引用表并不是总是有效。例如,在语句级前triggers中是无法读取上下文中tg_oldtable或tg_newtable,因为此时 DML 语句尚未执行;而INSERT事件触发的行级triggers也无法通过OLD变量去获取元组变更前的状态,因为它只有插入的新值。所幸是,这并不会成为困扰我们使用triggers的难题,结合前面介绍的行、语句级triggers的处理流程,我们可以非常准确地“猜测”出哪些变量在这些triggers中是可用的。
四、triggers返回值
在前面的例子,可以看到triggers函数的返回值类型是TRIGGER,但这只是个伪类型,用来表示标记这个函数只能作为triggers函数来使用。triggers函数真实的返回类型其实是triggers所在的表的元组类型,并且只有行级前triggers的返回值才有意义,其他triggers的返回都会被系统忽略。那么行级前triggers的返回值有什么作用呢?
前面介绍行级triggers的处理流程时为了简单故意跳过,实质上当行级前triggers函数返回nul时,是会直接跳过后续步骤的。如果返回值不为NULL,且事件是INSERT或UPDATE,那么返回值将作为最终新值被存入到表中,这也意味着我们可以干预用户对数据修改,使其符合业务约束(如果没有这种需求,返回NEW即可);对于DELETE事件,只关心返回值是否为null,如果是null则不会执行后续操作(包括删除数据,以及行级后triggers的处理),如果非null则继续往下执行。
五、triggers的创建
更具前面的介绍,我们可以已经triggers函数的与普通函数的区别:借助预定义的变量名来引用上下文中数据变化信息;特殊的返回值类型。除此之外,和其他的普通函数没什么太大区别。这里主要介绍的是triggers的创建,下面是创建triggers的语法规则:
CREATE [ CONSTRAINT ] TRIGGER name -- triggers名称
{ BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] } -- 触发的时机(BEFORE/AFTER)和触发事件(例如增删改等)
ON table_name -- triggers针对的表
[ NOT DEFERRABLE | [ DEFERRABLE ] [ INITIALLY IMMEDIATE | INITIALLY DEFERRED ] ] -- 是否延迟执行
[ REFERENCING { { OLD | NEW } TABLE [ AS ] name } [ ... ] ] -- 见上文的'triggers参数'
[ FOR [ EACH ] { ROW | STATEMENT } ] -- triggers的级别
[ WHEN ( condition ) ] -- triggers时需要额外满足的条件
EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments ) -- triggers函数
where event can be one of:
INSERT
UPDATE [ OF column_name [, ... ] ]
DELETE
语法规则中的添加备注信息已经非常详细了,读者可以参考讲解triggers参数时给的例子来体会。这里再补充以下要点:
目前 KlustronDB 不支持延迟执行triggers(即提交事务时再执行triggers函数)
虽然创建triggers时可以为triggers函数指定的''参数',但triggers函数仍然要求是一个无参数的函数;而这些这些传入的“参数”其实韩式存放在上面提到的上下文中,并且通过系统约定好的名称
TG_ARGV来引用。在更新操作并发量很高或者修改的数据量很大时,行triggers对性能的影响是非常明显的。它不仅阻止了 DML 语句的下推,也使得triggers函数被大量执行。如果我们只关心部分数据的修改,可以在创建triggers时指定
WHEN子句,这样一来只当满足子句的条件时才会执行triggers函数,降低对性能的影响。目前 KlustronDB 要求创建triggers的表必须存在主键(后续版本可能去除这个限制)。
如果在分区表上创建语句级triggers,只有当用户显示更新分区表时会触发,单独更新分区表的子表不会触发;如果在分区表上创建行级triggers,当用户更新分区表或者单独更新分区表的子表时都会触发。
postgres=# create table t(a int primary key ,b int) partition by range(a); CREATE TABLE postgres=# create table tp1 partition of t for values from (0) to (100); CREATE TABLE postgres=# INSERT INTO T VALUES(1,1); INSERT 0 1 postgres=# postgres=# -- 创建简单的打印信息的triggers函数 postgres=# create or replace function dump() returns trigger as $$ postgres$# begin postgres$# raise notice 'trigger = %', TG_NAME; postgres$# return null; postgres$# end; postgres$# $$ language plpgsql; CREATE FUNCTION postgres=# postgres=# -- 在分区表t上创建DELETE语句级triggers postgres=# create trigger trigger1 AFTER UPDATE ON t postgres-# FOR EACH STATEMENT EXECUTE PROCEDURE dump(); CREATE TRIGGER postgres=# postgres=# -- 更新分区表,triggers会执行 postgres=# UPDATE t SET b=b+1; NOTICE: trigger = trigger1 UPDATE 1 postgres=# postgres=# -- 更新分区表子表,triggers不执行 postgres=# UPDATE tp1 SET b=b+1; UPDATE 1 postgres=# postgres=# -- 将triggers改成行级triggers postgres=# drop trigger trigger1 on t; DROP TRIGGER postgres=# create trigger trigger1 AFTER UPDATE ON t postgres-# FOR EACH ROW EXECUTE PROCEDURE dump(); CREATE TRIGGER postgres=# postgres=# -- 更新分区表,triggers会执行 postgres=# UPDATE t SET b=b+1; NOTICE: trigger = trigger1 UPDATE 1 postgres=# postgres=# -- 更新分区表子表,triggers也会执行 postgres=# UPDATE tp1 SET b=b+1; NOTICE: trigger = trigger1 UPDATE 1 postgres=#
六、视图triggers
在 KlustronDB 中,不仅可以为表创建triggers,还可以为视图创建triggers,是不是很神奇?在什么样的场合需要为视图创建triggers呢?
视图是数据库系统中一种“虚拟的表”,它不仅可以隐藏数据的复杂性(可能是多表连接的结果),简化用户的访问,而且也是限制用户访问数据的有效手段,例如可以将用户有权访问的那部分数据表示为视图的形式,并将视图的访问权限授权给用户,但保留用户对基表的访问权限。很显然,用户也希望能够对这部分数据具有修改权限,那么如何在用户无法直接访问基表的情况下更新这部分数据呢?一个有意思解决思路是,让用户更新视图这个“虚拟的表”来更新基表中对应的数据。很多数据库系统确实是允许用户来更新简单的视图的,例如:
abc=# create table t(a int, b int);
CREATE TABLE
abc=# create view v1 as select a from t where a<2;
CREATE VIEW
abc=# explain update v1 set a=a+1;
QUERY PLAN
------------------------------------------------------------------------------------------
Update on t (cost=21.02..21.02 rows=1 width=40)
Shard: 0 Remote SQL: update `abc_$$_public`.`t` set `a`=(t.a + 1) WHERE (t.a < 2)
(2 rows)
可以看到数据库会自动将对视图的更新转变为对基表的更新。但是对于复杂的视图,就没那么好运了,例如:
abc=# create view v2 as select t.a, count(1) as cnt from t left join t1 on t.a=t1.a where t.a<2 group by t.a;
CREATE VIEW
abc=# update v2 set a=a+1;
ERROR: cannot update view "v2"
DETAIL: Views containing GROUP BY are not automatically updatable.
HINT: To enable updating the view, provide an INSTEAD OF UPDATE trigger or an unconditional ON UPDATE DO INSTEAD rule.
此时系统会报错,拒绝更新包含了GROUP BY子句的视图 ,并建议我们在视图上创建INSTEAD OF UPDATE 类型的triggers。INSTEAD OF在前面的叙述中尚未提及,因为它是专门用来实现复杂视图的增删改的行级视图。从它的名称也能看出,这类triggers是用来“定义”视图的更新的。
例如上面的例子,可以在视图上定义如下triggers。可以看到,我们在triggers函数中除了将视图上的更新转换为对基表的更新之外,还对新数据进行约束检查,非常的简单灵活。
CREATE FUNCTION v2_update() RETURNS TRIGGER AS $$
BEGIN
-- 检查约束条件
IF NEW.a>=2 THEN
RAISE Exception 'a should be less than 2';
END IF;
-- 检查是否更新了cnt
IF NEW.cnt != OLD.cnt THEN
RAISE Exception 'cnt is not updatable';
END IF;
-- 转换为对基表t的更新
UPDATE t SET a=NEW.a WHERE a=OLD.a;
RETURN NEW;
END;
$$ LANGUAGE PLPGSQL;
-- 创建视图的INSTEAD OF函数
CREATE TRIGGER v2_update_trig
INSTEAD OF UPDATE ON v2
FOR EACH ROW EXECUTE PROCEDURE v2_update();
此外,虽然数据库允许对简单的视图直接进行更新,但是数据库系统只是进行了简单的替换,将其转换为对基表的更新,所以,我们仍然可以为其构建INSTEAD OFtriggers,执行更加灵活的约束检查。例如:
create table usr(uid int primary key, level int);
create view v1 as select * from usr where level < 2;
insert into usr values(1,1), (2,2);
-- 修改用户级别
update v1 set level=2 where uid=1;
-- 创建triggers函数
create function update_v1() returns trigger as $$
begin
-- 拒绝更新为过高的级别
IF NEW.level >= 2 THEN
RAISE Exception 'level should be less than 2';
END IF;
-- 对基表进行更新
update usr set uid=NEW.uid and level = NEW.level where uid = OLD.uid;
return NEW;
end;
$$ language plpgsql;
-- 创建triggers
create trigger update_v1_trig
instead of update on v1
for each row execute procedure update_v1();
-- update会被拒绝
abc=# update v1 set level=2 where uid=1;
ERROR: level should be less than 2
CONTEXT: PL/pgSQL function update_v1() line 5 at RAISE
总结
下面列出的是目前 KlustronDB 对triggers的支持情况。
| When | Event | Row-level | Statement-level |
|---|---|---|---|
BEFORE | INSERT/UPDATE/DELETE | Tables | Tables, views |
AFTER | INSERT/UPDATE/DELETE | Tables | Tables, views |
INSTEAD OF | INSERT/UPDATE/DELETE | Views | — |
有了triggers的加持,数据库可以帮助用户实现很多复杂需求,例如对记录用户的增删改操作,实现跨越多表的约束检查或者级联更新,通过视图更新基表数据等等。但是triggers也会带来维护的复杂性,不可滥用。例如triggers函数中执行的 DML 语句可能会触发其他表/视图上的triggers,从而产生复杂的调用关系,不便于问题定位;再比如,triggers会是使得 DML 语句无法下推到存储节执行,对性能敏感的业务需要做好压测工作,权衡好triggers带来的利弊。
