数据库触发器(Trigger)
场景介绍
数据写进数据库之后,常有若干事需要随之发生:记录更新时间随修改自动刷新、价格变动留存审计、库存与汇总随之重算、派生字段同步更新。这类"变更即联动"若交由应用在每次写数据后手动触发,既容易遗漏,也会令同一套逻辑分散到各处,改一处牵动多处。触发器(Trigger)是PostgreSQL内置的事件驱动机制,绑定在表上,表发生插入、更新或删除时即自动执行一段预先定义的SQL逻辑,全程在数据库内完成,无需应用参与。它与Data API、RPC互补:后两者面向应用主动发起的调用,触发器面向数据被动变更的响应。
触发器在库内执行,可读写本表与其它表、调用其它函数,适合承接需要就近数据完成的联动逻辑。逻辑与数据同库执行,可借事务保证多表一致,无需在应用与数据库间搬运中间结果。典型应用如下:
- 自动维护字段,如更新时刷新updated_at。
- 变更审计,记录价格等字段变更前后的值。
- 派生与冗余字段刷新,如定价变动后重算产品最低价。
- 写入前校验与规范化,不合规则即中止,避免脏数据落库。
将联动置于数据库而非应用层,是因为数据变更本身就是联动的源头,在源头响应最为及时、不易遗漏。若诉求是把变更送达外部系统,应使用数据库Webhook;若诉求是前端界面随数据实时刷新,应使用实时订阅(Realtime),后者经WebSocket将变更推送给已连接的客户端,与触发器一库内一跨端、一自动执行一推送的定位不同。
本章完整说明触发器函数的功能、基础写法与定义。示例沿用products(产品表)与product_pricings(产品定价表)两张表。
相关概念
- 触发器介绍
如引言所述,触发器绑定在表上,表发生指定事件时即自动执行一段预先绑定的函数(称为触发器函数),无需应用主动调用。它和Data API、RPC互补:后两者面向应用主动发起的调用,触发器面向数据被动变更的响应。正因全程在库内执行,它能承接"变更即联动"的逻辑,如自动维护字段、写审计日志、级联校验、派生计算列等。
- 触发器组成
创建一个触发器涉及两个独立对象,详见表1。
表1 触发器的组成 对象
作用
定义方式
触发器函数
被执行的具体逻辑,返回Trigger类型。
CREATE FUNCTION ... RETURNS trigger
触发器对象
声明“何时、在哪张表、对什么事件、以什么频率”调用上述函数。
CREATE TRIGGER ...
一个触发器函数可被多个触发器复用,如set_updated_at()同时绑到products与product_pricings两张表。
- 触发时机:BEFORE与AFTER
根据“在变更写入前还是后执行”,触发器分两类,决定能否改写或取消本次变更。
表2 触发时机 时机
执行点
能否改写/取消变更
典型场景
BEFORE
变更写入前
能:改写NEW字段值、返回NULL取消该行操作。
自动填充/改写字段、写入前校验、规范化数据。
AFTER
变更写入后
不能(数据已落库)。
审计日志、派生计算、通知联动。
BEFORE触发器返回NEW表示放行(可用改写后的值继续写入),返回NULL则取消该行操作;AFTER触发器返回值无实际作用,通常返回NULL或NEW均可。
- 触发频率:行级与语句级
根据“每行触发一次还是整条语句触发一次”,分两种频率。
表3 触发频率 频率
含义
适用
FOR EACH ROW
每个受影响行各触发一次。
需逐行读写NEW/OLD的场景(绝大多数业务触发器)。
FOR EACH STATEMENT
整条SQL语句只触发一次(不论影响几行)。
整体统计、批量场景的聚合计算。
业务触发器多用FOR EACH ROW,审计、自动维护字段都要拿到具体哪一行变了;FOR EACH STATEMENT适合"本次操作影响了几行"这类整体性统计,对批量操作更高效。
- 触发事件与按列触发
触发事件即“什么操作会触发”:INSERT、UPDATE、DELETE、TRUNCATE,可多选(如INSERT OR UPDATE)。其中TRUNCATE较为特殊,只能用于语句级触发器,且不能与INSERT/UPDATE/DELETE混在同一个触发器里。对UPDATE还可限定“仅当指定列变化时触发”。
-- 仅当price列被UPDATE时才触发(改其它列不触发) CREATE TRIGGER trg_price_audit AFTER UPDATE OF price ON product_pricings FOR EACH ROW EXECUTE FUNCTION log_price_change();
UPDATE OF price比任何UPDATE都触发更精准,修改tier、unit等无关列时不会误触,减少无谓的执行。
- 触发器函数的特殊变量
触发器函数内可访问一组特殊变量,描述本次触发的上下文与数据。
表4 触发器函数特殊变量 变量
含义
NEW
新行数据(INSERT/UPDATE可用),BEFORE中可改写。
OLD
旧行数据(UPDATE/DELETE可用)。
TG_OP
触发操作:INSERT/UPDATE/DELETE/TRUNCATE。
TG_WHEN
时机:BEFORE/AFTER。
TG_LEVEL
频率:ROW/STATEMENT。
TG_TABLE_NAME
触发所在表名。
TG_TABLE_SCHEMA
触发所在表的模式。
TG_NAME
触发器名。
TG_ARGV
创建触发器时传入的字符串参数数组。
TG_NARGS
TG_ARGV的参数个数。
多事件触发器(如INSERT OR UPDATE OR DELETE)可在函数内用TG_OP分支处理不同操作,复用同一函数。
功能场景
触发器响应数据变更,落点在库内联动。它和数据库Webhook、实时订阅(Realtime)中g的Realtime都由数据变化驱动,区别在“谁接收、怎么接收”。
| 机制 | 接收方 | 传递方式 | 是否入库 | 典型场景 |
|---|---|---|---|---|
| 触发器 | 数据库自身 | 库内自动执行SQL | 是(可写其它表) | 自动维护字段、审计、级联校验、派生计算。 |
| Webhook | 外部系统 | HTTP请求外发 | 否(仅外呼) | 通知第三方、同步下游、触发外部工作流,详见数据库Webhook(Webhook)。 |
| Realtime | 已连接的客户端 | WebSocket推送 | 否(仅推送) | 前端实时刷新、协同编辑、在线状态,详见实时订阅(Realtime)。 |
库内联动用触发器、外呼外部用Webhook、推前端用Realtime。何时该用触发器、何时仍交给应用,可按表6判断。
| 场景 | 选型 | 说明 |
|---|---|---|
| 自动维护updated_at等字段 | 触发器 | BEFORE UPDATE改写NEW,免应用每次传值。 |
| 数据变更写审计/日志 | 触发器 | AFTER触发,记录变更前后值。 |
| 派生/冗余字段自动更新 | 触发器 | 如订单变动更新库存汇总、产品最低价缓存。 |
| 写入前业务校验、数据规范化 | 触发器 | BEFORE校验不合规则返回NULL取消。 |
| 变更即通知外部系统 | Webhook | 调价通知营销平台、新用户同步CRM,详见数据库Webhook(Webhook)。 |
| 前端实时刷新 | Realtime | |
| 定时周期性任务 | pg_cron | 按cron定时执行,属定时任务范畴(非事件驱动)。 |
核心价值:数据变更即库内自动化、联动免应用、就近执行保事务一致。
定义与使用触发器
触发器通过SQL定义,在控制台SQL Editor中执行。本节示例基于数据库API(Database Data API)中已建好的products与product_pricings两张表。
- 基本语法
定义触发器分两步,先创建返回trigger的函数,再创建触发器对象绑定到表。
-- 步骤一:定义触发器函数 CREATE OR REPLACE FUNCTION 函数名() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN -- 函数体:读写NEW/OLD、调用其它SQL RETURN NEW; -- BEFORE返回NEW放行;返回NULL取消该行 END; $$; -- 步骤二:创建触发器 CREATE TRIGGER 触发器名 {BEFORE | AFTER} {INSERT | UPDATE | DELETE [OR ...]} ON 表名 [FOR EACH ROW | FOR EACH STATEMENT] [WHEN (条件)] EXECUTE FUNCTION 函数名(可选参数...);以最常见的自动维护updated_at为例:该字段通常仅插入时取默认值,更新时不会自动刷新。用触发器在每次UPDATE前把updated_at改写为当前时间即可。
-- 触发器函数:BEFORE UPDATE时把新行的updated_at改写为当前时间 CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = now(); -- 改写新行的updated_at RETURN NEW; -- 返回新行,使其继续写入 END; $$; -- 绑定到products的UPDATE:更新前自动刷新updated_at CREATE TRIGGER trg_products_updated_at BEFORE UPDATE ON products FOR EACH ROW EXECUTE FUNCTION set_updated_at();
此后对products执行任何UPDATE,updated_at都会自动刷新,无需应用层传值。同一函数可复用到product_pricings等其它表,只需各自CREATE TRIGGER绑定。
- BEFORE/AFTER:详见BEFORE触发器:写入前校验与改写和AFTER触发器:维护冗余计算列。
- 事件可多选:INSERT OR UPDATE OR DELETE。
- WHEN:仅满足条件时才触发,详见WHEN条件触发。
- BEFORE触发器:写入前校验与改写
示例场景:上架或调价时,价格不得为负。用BEFORE INSERT OR UPDATE触发器在写入前校验,不合规则中止操作,避免脏数据落库后再回滚。
CREATE OR REPLACE FUNCTION check_price_positive() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.price < 0 THEN RAISE EXCEPTION '价格不能为负: %', NEW.price; -- 抛错中止本次操作 END IF; RETURN NEW; -- 校验通过,放行 END; $$; CREATE TRIGGER trg_check_price BEFORE INSERT OR UPDATE ON product_pricings FOR EACH ROW EXECUTE FUNCTION check_price_positive();注意:BEFORE中RAISE EXCEPTION会中止当前SQL语句,所在事务进入中止状态(须ROLLBACK后才能继续后续操作);若只想“仅跳过该行而非整条回滚”,改为RETURN NULL,该行被取消,其余行继续。NEW在此可读可改,校验通过返回NEW放行。
- AFTER触发器:维护冗余计算列
示例场景:product_pricings每档价格变动时,希望products上能直接取到该产品当前最低价,省去每次跨表聚合。用AFTER INSERT OR UPDATE OR DELETE触发器维护一个冗余列,较BEFORE改写本行更进一层,可跨表更新父表。
先为products增加最低价列:
ALTER TABLE products ADD COLUMN min_price NUMERIC(10,2);
再定义触发器,在定价变更后重算所属产品的最低价:
CREATE OR REPLACE FUNCTION refresh_product_min_price() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE pid bigint; BEGIN -- DELETE时NEW不可用,按操作类型取对应行的product_id IF TG_OP = 'DELETE' THEN pid := OLD.product_id; ELSE pid := NEW.product_id; END IF; UPDATE products p SET min_price = COALESCE((SELECT min(price) FROM product_pricings WHERE product_id = pid), 0) WHERE p.id = pid; RETURN NULL; -- AFTER返回值无意义 END; $$; CREATE TRIGGER trg_refresh_min_price AFTER INSERT OR UPDATE OR DELETE ON product_pricings FOR EACH ROW EXECUTE FUNCTION refresh_product_min_price();注意:AFTER确保新值已落库再重算;多事件触发器中DELETE时NEW不可用、INSERT时OLD不可用,直接访问会报错,须用TG_OP判断取哪一行;冗余列适合“读多写少”的汇总,以空间换查询效率。
- 按列触发:UPDATE OF
UPDATE默认任意列变化都触发,可用OF限定列,避免无关列更新误触。1.5已示例UPDATE OF price,此处补充:若只想在价格或生效日变化时重算最低价。
CREATE TRIGGER trg_refresh_min_price_on_price AFTER UPDATE OF price, effective_from ON product_pricings FOR EACH ROW EXECUTE FUNCTION refresh_product_min_price();
仅price或effective_from被UPDATE时才触发,修改tier、unit不触发。
- WHEN条件触发
WHEN在触发器层加一层条件,不满足则整条触发器跳过、连函数都不调用。以价格变更审计为例,先建审计表与审计函数(函数内用IF保证仅在价格变化时记录)。
-- 审计表:记录价格变更前后值 CREATE TABLE product_price_audit ( id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, product_id BIGINT NOT NULL, old_price NUMERIC(10,2), new_price NUMERIC(10,2), changed_at TIMESTAMPTZ DEFAULT now() ); CREATE OR REPLACE FUNCTION log_price_change() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.price IS DISTINCT FROM OLD.price THEN INSERT INTO product_price_audit (product_id, old_price, new_price) VALUES (NEW.product_id, OLD.price, NEW.price); END IF; RETURN NEW; END; $$;再用WHEN在触发器层再过滤一道,价格未变则连log_price_change都不调用,省去函数执行开销。
CREATE TRIGGER trg_price_audit_when AFTER UPDATE ON product_pricings FOR EACH ROW WHEN (NEW.price IS DISTINCT FROM OLD.price) EXECUTE FUNCTION log_price_change();
IS DISTINCT FROM可安全比较含NULL的值,=对NULL比较结果为NULL(视为假)会漏判。
- 语句级触发
FOR EACH STATEMENT整条语句只触发一次,不针对具体行,适合整体性处理。例如把每次操作类型记录到日志。
CREATE OR REPLACE FUNCTION log_pricing_stmt() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE LOG 'product_pricings发生%操作', TG_OP; RETURN NULL; END; $$; CREATE TRIGGER trg_pricing_stmt AFTER INSERT OR UPDATE OR DELETE ON product_pricings FOR EACH STATEMENT -- 整条语句一次,不论影响几行 EXECUTE FUNCTION log_pricing_stmt();
语句级触发器内NEW/OLD不可用(不针对具体行),只能用TG_OP等上下文变量。
- 删除触发器
DROP TRIGGER trg_check_price ON product_pricings;
删除触发器对象,其绑定的函数仍保留(可被其它触发器复用)。若函数位于受限模式无法直接删触发器,可连带删函数。
DROP FUNCTION IF EXISTS check_price_positive() CASCADE; -- CASCADE一并移除调用它的触发器
CASCADE会移除所有依赖该函数的触发器,删前请确认无其它触发器引用。
- 触发器与权限、安全
触发器函数的执行权限需特别注意:
- 触发器函数以函数属主身份执行(即便未标记SECURITY DEFINER,PostgreSQL的触发器函数也以属主权限运行),因此能跨表写父表、不受调用方RLS约束。这是触发器联动的前提,也意味着它天然类似SECURITY DEFINER:权限取决于函数属主而非发起SQL的用户。
- 因此触发器函数体内的写操作要谨慎,避免被间接利用做越权写入;若函数写在public模式且属主为postgres等带BYPASSRLS的高权限角色,等同一个高权限入口。
- 建议把触发器函数集中放在受控模式、属主为运维角色,并在函数体内做必要校验;变更后用supabase db advisors(或控制台安全检查)扫描隐患。
触发器函数不能经RPC调用(返回trigger类型的函数仅供触发器使用);普通函数也不能直接绑定为触发器(须返回Trigger类型)。
创建与管理触发器
触发器的创建有两条路径,二者等价、底层都生成同样的触发器对象:在SQL Editor中执行SQL语句,或在Dashboard→Triggers的可视化界面配置。SQL Editor适合复杂逻辑或需纳入迁移脚本的触发器;可视化适合按已有函数快速创建常用触发器与日常管理。
- 方式一:SQL Editor创建
在控制台SQL Editor中执行定义与使用触发器中的CREATE FUNCTION与CREATE TRIGGER即完成创建。查看已有触发器可查询系统视图:
SELECT trigger_name AS 触发器名, event_object_table AS 表名, event_manipulation AS 事件, action_timing AS 时机, action_orientation AS 频率 FROM information_schema.triggers WHERE trigger_schema = 'public' ORDER BY event_object_table, trigger_name;删除触发器用DROP TRIGGER,详见删除触发器。
- 方式二:Dashboard→Triggers可视化创建
在Supabase Dashboard控制台「数据库Database」→「Triggers」入口(或在表详情页的Triggers标签),以界面方式创建与管理。
创建步骤:
- 登录RDS Supabase Dashboard控制台界面。
- 选择“Database > Triggers”,进入触发器管理页面。 图1 Supabase Dashboard触发器管理页面
- 单击“New trigger”。 图2 Supabase Dashboard触发器创建页面
请按表7中配置项进行参数填写。 - 确认无误后保存,平台据此生成对应的CREATE TRIGGER语句并执行。
可视化暴露的是常用配置项,与定义与使用触发器中的CREATE TRIGGER参数一一对应。复杂写法(如UPDATE OF指定多列、含子查询的WHEN)若界面不便表达,使用方式一:SQL Editor创建直接写SQL。
特别注意,触发器函数须事先定义(如基本语法中的set_updated_at、BEFORE触发器:写入前校验与改写中的check_price_positive),可视化创建时从下拉选择已定义的函数。
- 管理与调试
无论哪种方式创建,日常管理都在“数据库 > Triggers”或表详情页:列表查看各表触发器、启用或禁用单个触发器(调试时常用)、删除触发器、跳转至触发器函数。
临时禁用触发器,调试完用ENABLE TRIGGER恢复。生产环境慎用,禁用期间校验与联动失效。
ALTER TABLE product_pricings DISABLE TRIGGER trg_check_price;
综合案例:产品价格变动自动库内联动
示例场景:运营小张在product_pricings调整价格后,库内要自动完成三件事:调价前校验价格非负、调价后刷新products.min_price缓存、调价后把新旧价格写进审计表。这把一个BEFORE触发器与两个AFTER触发器串到同一张表上,全程库内联动,应用无感知。
- BEFORE触发器:校验价格非负
复用BEFORE触发器:写入前校验与改写中的check_price_positive,保证负价不落库。
- AFTER触发器:刷新最低价缓存
复用AFTER触发器:维护冗余计算列中的refresh_product_min_price,调价后重算products.min_price。
- WHEN触发器:写价格审计日志
复用WHEN条件触发中已建好的trg_price_audit_when(含审计表product_price_audit与函数log_price_change):价格变化时把product_id、旧价、新价写入审计表,供后续追溯。该触发器自带WHEN限定价格真正变化才触发,无需重复定义。
- SDK触发联动
以上三步都是数据库侧配置,应用侧无需改动。运营在前端用SDK正常调价,触发器在背后自动联动。
// 运营小张在前端调价,一次普通的SDK update const { error } = await supabase .from('product_pricings') .update({ price: 128.00 }) .eq('id', 1) // 定价档位id=1 if (error) { console.error(error); return } // 这次update背后依次发生: // 1) BEFORE触发器check_price_positive校验128>0,放行 // 2) 数据落库,price由旧值变为128.00 // 3) 两个AFTER触发器按名称字母序触发,二者独立、互不影响结果: // - trg_price_audit_when把新旧价格写进product_price_audit // - trg_refresh_min_price重算所属产品min_price应用侧只是一次普通的.update(),校验、缓存刷新、审计全自动完成。这正是触发器的价值:把数据变更后的库内联动下沉到数据库,应用代码不必感知,也不必为每个调用点重复编排这些副作用。
调价后查这两张表即可确认联动是否生效:
- 看最低价已刷新:SELECT id, min_price FROM products WHERE id = <产品id>
- 看新增一条调价记录:SELECT product_id, old_price, new_price FROM product_price_audit ORDER BY changed_at DESC LIMIT 5
多个触发器在同一表上的执行顺序按名称字母序,跨时机则为BEFORE先于AFTER。若联动顺序敏感,可在命名上体现次序,或合并到单个触发器函数内按序编排。
参考文档
以下为Supabase开源相关文档,仅供参考。