# 数据库触发器（Trigger）
#### 场景介绍
数据写进数据库之后，常有若干事需要随之发生：记录更新时间随修改自动刷新、价格变动留存审计、库存与汇总随之重算、派生字段同步更新。这类"变更即联动"若交由应用在每次写数据后手动触发，既容易遗漏，也会令同一套逻辑分散到各处，改一处牵动多处。触发器（Trigger）是PostgreSQL内置的事件驱动机制，绑定在表上，表发生插入、更新或删除时即自动执行一段预先定义的SQL逻辑，全程在数据库内完成，无需应用参与。它与Data API、RPC互补：后两者面向应用主动发起的调用，触发器面向数据被动变更的响应。
触发器在库内执行，可读写本表与其它表、调用其它函数，适合承接需要就近数据完成的联动逻辑。逻辑与数据同库执行，可借事务保证多表一致，无需在应用与数据库间搬运中间结果。典型应用如下：
- 自动维护字段，如更新时刷新updated_at。
- 变更审计，记录价格等字段变更前后的值。
- 派生与冗余字段刷新，如定价变动后重算产品最低价。
- 写入前校验与规范化，不合规则即中止，避免脏数据落库。
将联动置于数据库而非应用层，是因为数据变更本身就是联动的源头，在源头响应最为及时、不易遗漏。若诉求是把变更送达外部系统，应使用[数据库Webhook](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_26.html)；若诉求是前端界面随数据实时刷新，应使用[实时订阅（Realtime）](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_29.html)，后者经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）](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_29.html)中g的Realtime都由数据变化驱动，区别在"谁接收、怎么接收"。
表5数据变更三类机制对比 
| 机制      | 接收方     | 传递方式        | 是否入库      | 典型场景                                                                                                             |
|:---|:---|:---|:---|:---|
| 触发器     | 数据库自身   | 库内自动执行SQL  | 是（可写其它表） | 自动维护字段、审计、级联校验、派生计算。                                                                                               |
| Webhook | 外部系统    | HTTP请求外发   | 否（仅外呼）    | 通知第三方、同步下游、触发外部工作流，详见[数据库Webhook（Webhook）](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_26.html)。 |
| Realtime  | 已连接的客户端 | WebSocket推送 | 否（仅推送）    | 前端实时刷新、协同编辑、在线状态，详见[实时订阅（Realtime）](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_29.html)。       |
   
库内联动用触发器、外呼外部用Webhook、推前端用Realtime。何时该用触发器、何时仍交给应用，可按[表6]判断。
 表6场景与选型 
| 场景                 | 选型         | 说明                                                                                                               |
|:---|:---|:---|
| 自动维护updated_at等字段 | 触发器      | BEFORE UPDATE改写NEW，免应用每次传值。                                                                                      |
| 数据变更写审计/日志         | 触发器       | AFTER触发，记录变更前后值。                                                                                                 |
| 派生/冗余字段自动更新        | 触发器      | 如订单变动更新库存汇总、产品最低价缓存。                                                                                             |
| 写入前业务校验、数据规范化    | 触发器     | BEFORE校验不合规则返回NULL取消。                                                                                             |
| 变更即通知外部系统            | Webhook   | 调价通知营销平台、新用户同步CRM，详见[数据库Webhook（Webhook）](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_26.html)。 |
| 前端实时刷新            | Realtime | 详见[实时订阅（Realtime）](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_29.html)。                        |
| 定时周期性任务          | pg_cron  | 按cron定时执行，属定时任务范畴（非事件驱动）。                                                                                        |
   
核心价值：**数据变更即库内自动化、联动免应用、就近执行保事务一致**。
 #### 定义与使用触发器
触发器通过SQL定义，在控制台SQL Editor中执行。本节示例基于[数据库API（Database Data API）](https://support.huaweicloud.com/usermanual-rds-pg/rds_supabase_23.html)中已建好的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标签），以界面方式创建与管理。
  创建步骤：
  1. 登录RDS Supabase Dashboard控制台界面。
  
  2. 选择"Database \> Triggers"，进入触发器管理页面。
     图1Supabase Dashboard触发器管理页面   
     ![](https://support.huaweicloud.com/usermanual-rds-pg/zh-cn_image_0000002770885442.png "点击放大") 
  
  3. 单击"New trigger"。
     图2Supabase Dashboard触发器创建页面   
     ![](https://support.huaweicloud.com/usermanual-rds-pg/zh-cn_image_0000002800085265.png "点击放大")
     请按[表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开源相关文档，仅供参考。
- [Supabase Postgres Triggers](https://supabase.com/docs/guides/database/postgres/triggers)
- [Supabase数据库函数](https://supabase.com/docs/guides/database/functions)
- [PostgreSQL --- CREATE TRIGGER](https://www.postgresql.org/docs/current/sql-createtrigger.html)
- [PostgreSQL --- 触发器](https://www.postgresql.org/docs/current/triggers.html)
 
