更新时间:2026-10-21 GMT+08:00
分享

数据库触发器(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都由数据变化驱动,区别在“谁接收、怎么接收”。

表5 数据变更三类机制对比

机制

接收方

传递方式

是否入库

典型场景

触发器

数据库自身

库内自动执行SQL

是(可写其它表)

自动维护字段、审计、级联校验、派生计算。

Webhook

外部系统

HTTP请求外发

否(仅外呼)

通知第三方、同步下游、触发外部工作流,详见数据库Webhook(Webhook)。

Realtime

已连接的客户端

WebSocket推送

否(仅推送)

前端实时刷新、协同编辑、在线状态,详见实时订阅(Realtime)。

库内联动用触发器、外呼外部用Webhook、推前端用Realtime。何时该用触发器、何时仍交给应用,可按表6判断。

表6 场景与选型

场景

选型

说明

自动维护updated_at等字段

触发器

BEFORE UPDATE改写NEW,免应用每次传值。

数据变更写审计/日志

触发器

AFTER触发,记录变更前后值。

派生/冗余字段自动更新

触发器

如订单变动更新库存汇总、产品最低价缓存。

写入前业务校验、数据规范化

触发器

BEFORE校验不合规则返回NULL取消。

变更即通知外部系统

Webhook

调价通知营销平台、新用户同步CRM,详见数据库Webhook(Webhook)。

前端实时刷新

Realtime

详见实时订阅(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触发器:写入前校验与改写

    示例场景:上架或调价时,价格不得为负。用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标签),以界面方式创建与管理。

    创建步骤:

    1. 登录RDS Supabase Dashboard控制台界面。
    2. 选择“Database > Triggers”,进入触发器管理页面。
      图1 Supabase Dashboard触发器管理页面
    3. 单击“New trigger”。
      图2 Supabase Dashboard触发器创建页面
      请按表7中配置项进行参数填写。
      表7 触发器Trigger创建配置项

      配置项

      说明

      示例

      名称

      触发器名。

      trg_check_price

      表

      监听的表。

      product_pricings

      事件

      INSERT/UPDATE/DELETE,可多选。

      Insert、Update

      时机

      BEFORE或AFTER。

      BEFORE

      频率

      行级或语句级。

      FOR EACH ROW

      触发器函数

      选已定义的返回trigger的函数。

      check_price_positive

      条件(可选)

      WHEN表达式。

      NEW.price IS DISTINCT FROM OLD.price

    4. 确认无误后保存,平台据此生成对应的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触发器串到同一张表上,全程库内联动,应用无感知。

  1. BEFORE触发器:校验价格非负

    复用BEFORE触发器:写入前校验与改写中的check_price_positive,保证负价不落库。

  2. AFTER触发器:刷新最低价缓存

    复用AFTER触发器:维护冗余计算列中的refresh_product_min_price,调价后重算products.min_price。

  3. WHEN触发器:写价格审计日志

    复用WHEN条件触发中已建好的trg_price_audit_when(含审计表product_price_audit与函数log_price_change):价格变化时把product_id、旧价、新价写入审计表,供后续追溯。该触发器自带WHEN限定价格真正变化才触发,无需重复定义。

  4. 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开源相关文档,仅供参考。

相关文档