Skip to content

语句级触发器 FOR EACH STATEMENT ​

FOR EACH STATEMENT 让触发器函数对每条 DML 语句只执行一次,无论该语句影响了 0 行、1 行还是 100 万行。它拿不到 NEW / OLD(这两个变量在语句级触发器里是 NULL),因此不适合做逐行校验,但正因为「常数次调用」,它才是大批量审计、批量联动、批量统计的正确选择。PostgreSQL 中它也是默认层级——不写 FOR EACH ... 时就是语句级。

前置知识

阅读本节前,建议先了解:行级触发器 FOR EACH ROW

基础概念 ​

调用次数模型 ​

text
一条语句影响 N 行:

行级:   N 次调用(N = 0 时不调用)
语句级: 1 次调用(N = 0 时也调用)★

这个「零行也执行」的特性有实际价值:可以做「批量操作开始/结束」的确定性钩子,不依赖是否真的有行被改。

sql
-- 即使一行都没影响,这条 AFTER STATEMENT 触发器仍会执行
CREATE TRIGGER trg_mark_batch
  AFTER UPDATE ON products
  FOR EACH STATEMENT EXECUTE FUNCTION mark_batch();

语句级触发器的能力边界 ​

能力是否可用说明
访问 NEW / OLD否均为 NULL,访问会报错
读取 TG_OP / TG_TABLE_NAME 等是与行级完全一致
执行任意 SQL是可以 INSERT / UPDATE / 查询其他表
通过过渡表看到受影响的行是(AFTER + REFERENCING)见后续「过渡表」一节
修改将要写入的行否没有「将要写入的行」这个概念
通过 RAISE EXCEPTION 阻止语句是语句与触发器副作用一起回滚
用 WHEN 条件是但语句级下用处有限

语句级的位置与顺序 ​

text
语句级 BEFORE(一次)★ 最先
      ↓
逐行处理(行级 BEFORE → 写行 → 行级 AFTER)
      ↓
语句级 AFTER(一次)★ 最后

固定顺序:语句级 BEFORE → 所有行级触发器 → 语句级 AFTER。所以语句级 AFTER 能确定「本语句的全部行级副作用都已发生」。

语法与用法 ​

定义语句级触发器 ​

text
CREATE [ OR REPLACE ] [ CONSTRAINT ] TRIGGER name
    { BEFORE | AFTER }
    { INSERT | UPDATE [ OF column_name [, ...] ] | DELETE | TRUNCATE } [ OR ... ]
    ON { table_name | view_name }
    [ REFERENCING { { OLD | NEW } TABLE [ AS ] transition_relation_name } [ ... ] ]
    [ FOR [ EACH ] STATEMENT ]        -- 也可完全省略,默认即语句级
    [ WHEN ( condition ) ]
    EXECUTE { FUNCTION | PROCEDURE } function_name ( arguments )

要点:

  • FOR EACH STATEMENT 可省略不写,默认就是语句级;
  • 语句级可用 INSERT / UPDATE / DELETE / TRUNCATE 全部四类事件;
  • 视图上的语句级 BEFORE / AFTER 触发器只有在视图同时存在 INSTEAD OF 触发器时才会被触发。

函数模板 ​

sql
CREATE OR REPLACE FUNCTION trg_<用途>_stmt() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  -- 不要访问 NEW / OLD:语句级下它们是 NULL
  RAISE NOTICE '语句级触发: op=% table=%.%',
    TG_OP, TG_TABLE_SCHEMA, TG_TABLE_NAME;

  -- 可以执行任意 SQL
  -- INSERT INTO ... SELECT ... FROM ... WHERE ...;

  RETURN NULL;     -- 语句级触发器必须返回 NULL
END;
$$;

CREATE TRIGGER trg_<用途>_stmt
  AFTER INSERT OR UPDATE OR DELETE ON <表>
  FOR EACH STATEMENT EXECUTE FUNCTION trg_<用途>_stmt();

语句级触发器应始终返回 NULL

官方原文:被语句级触发器调用的函数应当始终返回 NULL。返回值没有任何意义,写成 RETURN NEW 只会误导读者。

详细说明 ​

语句级 BEFORE:改不了行,但能拦语句 ​

BEFORE 语句级在语句「开始做任何事之前」执行,由于此时没有任何行被处理,它的价值在于:

sql
-- ① 前置条件检查:不满足就整体拒绝
CREATE OR REPLACE FUNCTION refuse_weekend_write() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  IF extract(isodow FROM now()) IN (6, 7) THEN
    RAISE EXCEPTION '生产表在周末禁止批量修改'
      USING ERRCODE = '42501', HINT = '请在工作日执行';
  END IF;
  RETURN NULL;
END;
$$;

CREATE TRIGGER trg_refuse_weekend_write
  BEFORE UPDATE OR DELETE ON ledger_entries
  FOR EACH STATEMENT EXECUTE FUNCTION refuse_weekend_write();
sql
-- ② 准备会话级/临时数据,供后续行级触发器读取
CREATE OR REPLACE FUNCTION snapshot_before_batch() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  CREATE TEMP TABLE IF NOT EXISTS batch_ctx (started_at timestamptz) ON COMMIT DROP;
  TRUNCATE batch_ctx;
  INSERT INTO batch_ctx VALUES (now());
  RETURN NULL;
END;
$$;

在语句级 BEFORE 里看不到本语句的修改

语句级 BEFORE 执行时本语句的修改都还没发生,SELECT 到的是语句执行前的数据。

语句级 AFTER:能看到全部修改 ​

语句级 AFTER 在整条语句(含所有行级触发器)结束后执行,它执行的 SQL 能看到本语句的全部修改:

sql
CREATE OR REPLACE FUNCTION refresh_dashboard() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  v_cnt int;
BEGIN
  -- 本语句所有行的写入都已完成,统计可信
  SELECT count(*) INTO v_cnt
    FROM products WHERE price < 0;      -- 只是示例:此处读到的是最新数据

  RAISE NOTICE '本次 % 后,异常商品数 = %', TG_OP, v_cnt;
  RETURN NULL;
END;
$$;

CREATE TRIGGER trg_refresh_dashboard
  AFTER INSERT OR UPDATE OR DELETE ON products
  FOR EACH STATEMENT EXECUTE FUNCTION refresh_dashboard();

语句级 TRUNCATE ​

TRUNCATE 只能用语句级触发器,这是它唯一合法的层级:

sql
CREATE OR REPLACE FUNCTION guard_truncate() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  IF TG_TABLE_SCHEMA = 'public' AND TG_TABLE_NAME NOT LIKE 'tmp_%' THEN
    RAISE EXCEPTION '禁止 TRUNCATE 表 %.%,请改用带 WHERE 的 DELETE',
      TG_TABLE_SCHEMA, TG_TABLE_NAME
      USING ERRCODE = '42501';
  END IF;
  RETURN NULL;
END;
$$;

CREATE TRIGGER trg_guard_truncate
  BEFORE TRUNCATE ON orders
  FOR EACH STATEMENT EXECUTE FUNCTION guard_truncate();

TRUNCATE 触发器拿不到「清空了多少行」,也没有 OLD TABLE / NEW TABLE 可供引用(过渡表只对 INSERT/UPDATE/DELETE 有意义)。

WHEN 条件在语句级下的意义 ​

语句级触发器可以使用 WHEN,但由于它不针对具体行,WHEN 里不能引用行变量:

sql
-- 可以使用会话/环境相关的条件
CREATE TRIGGER trg_only_app
  AFTER INSERT ON audit_log
  FOR EACH STATEMENT
  WHEN (current_user = 'app_role')
  EXECUTE FUNCTION notify_audit();
sql
-- 错误!语句级下没有 NEW/OLD 可用
CREATE TRIGGER trg_bad
  AFTER INSERT ON products
  FOR EACH STATEMENT
  WHEN (NEW.price > 100)          -- ERROR
  EXECUTE FUNCTION f();

用过渡表把「逐行」变「批量」 ​

语句级触发器最大的短板是看不到行,而这个短板由过渡表补足:AFTER 语句级触发器可以请求 REFERENCING OLD TABLE AS ... / NEW TABLE AS ...,在函数里像查询普通只读临时表一样查询受影响的行集合。

sql
CREATE TRIGGER trg_audit_bulk
  AFTER INSERT OR UPDATE OR DELETE ON products
  REFERENCING OLD TABLE AS old_rows NEW TABLE AS new_rows
  FOR EACH STATEMENT EXECUTE FUNCTION audit_bulk();

对应的表可用性受事件限制:

事件OLD TABLENEW TABLE
INSERT不可请求可用
UPDATE可用可用
DELETE可用不可请求

注意:一个 REFERENCING 子句只能服务一个事件,因此 INSERT OR UPDATE OR DELETE 的合并写法在语法上会因「同一触发器请求不同过渡表」而需要用三个触发器分别声明。详见后续「过渡表」一节。

语句级与分区的交互 ​

分区表上有两条明确的规则:

  • 修改父表时,只有被显式命名的表的语句级触发器会触发,受影响的分区上的语句级触发器不触发;
  • 行级触发器则会在受影响的分区上触发。
sql
-- products_p 是分区表,products_p_2026_q1 是它的分区
UPDATE products_p SET price = price + 1 WHERE created_at >= '2026-01-01';
-- → products_p 上的语句级触发器触发一次
-- → products_p_2026_q1 上的语句级触发器不触发
-- → products_p_2026_q1 上的行级触发器逐行触发

语句级触发器与 MERGE、ON CONFLICT 的交互 ​

这两处都有「语句级触发器比直觉更容易被触发」的特点:

sql
-- ON CONFLICT DO UPDATE:语句级 UPDATE 触发器一定会执行,
-- 无论是否真的走了 UPDATE 分支、也无论是否影响了任何行
INSERT INTO products (sku, name, price) VALUES ('SKU-001', '苹果', 19.90)
ON CONFLICT (sku) DO UPDATE SET price = EXCLUDED.price;

带 ON CONFLICT DO UPDATE 的 INSERT,语句级触发器的执行顺序是:

text
语句级 BEFORE INSERT
  → 语句级 BEFORE UPDATE
    → 语句级 AFTER UPDATE
      → 语句级 AFTER INSERT
sql
-- MERGE:动作里声明了哪类动作,就触发该类语句级触发器(哪怕该动作最终未执行)
MERGE INTO products p
USING staging_products s ON p.sku = s.sku
WHEN MATCHED THEN UPDATE SET price = s.price
WHEN NOT MATCHED THEN INSERT (sku, name, price) VALUES (s.sku, s.name, s.price);

用 GET DIAGNOSTICS 拿本语句的行数 ​

语句级触发器自身不知道影响了多少行,但在函数里执行 DML 时可以用 GET DIAGNOSTICS ... ROW_COUNT 拿该条 DML 的行数:

sql
CREATE OR REPLACE FUNCTION archive_and_count() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  v_moved bigint;
BEGIN
  INSERT INTO orders_archive
  SELECT * FROM orders WHERE order_date < now() - interval '1 year';

  GET DIAGNOSTICS v_moved = ROW_COUNT;      -- 上面 INSERT 实际写入的行数
  RAISE NOTICE '本次归档 % 行', v_moved;

  RETURN NULL;
END;
$$;

CREATE TRIGGER trg_archive_and_count
  AFTER INSERT ON archive_trigger
  FOR EACH STATEMENT EXECUTE FUNCTION archive_and_count();

ROW_COUNT 反映的是最近一条 SQL 命令的影响行数,不是触发它的那条外层语句的行数。

语句级与事务边界 ​

触发器(无论层级)都在与触发语句同一事务内执行,且以「排队该事件的角色」身份运行(除非函数标了 SECURITY DEFINER):

text
BEGIN;
  UPDATE big_table SET flag = true;    -- 语句级 BEFORE → 行级 → 语句级 AFTER
  -- 若触发器抛错,整条 UPDATE 及其在触发器内的副作用一起回滚
COMMIT;

因此语句级 AFTER 里做的批量写入与主语句同生共死:主语句失败,批量写入也回滚;批量写入失败,主语句也被回滚。

什么时候必须选语句级 ​

场景为什么必须语句级
TRUNCATE 相关逻辑TRUNCATE 只有语句级
大批量审计避免 N 次 INSERT 的开销
「一次批量操作记一条日志」行级会产生 N 条
批量操作的前置检查要在任何行被处理前拦截
零行也需触发的钩子行级在零行时不执行
批量刷新汇总表结果只依赖最终状态,一次即可

实战示例 ​

场景:批量导入的「开始/结束」双钩子 + 批量审计 ​

需求:每天凌晨从上游系统批量导入商品数据,要求:

  1. 导入前记录批次开始(即使本次一行都没进来也要留痕);
  2. 导入结束后把「本次新增/变更」的商品一次性写入审计表(不要每行一条 INSERT);
  3. 本次导入若有价格下降的商品,汇总成一条告警。
sql
CREATE TABLE products (
  sku        text PRIMARY KEY,
  name       text NOT NULL,
  price      numeric(12,2) NOT NULL,
  updated_at timestamptz
);

CREATE TABLE import_batch_log (
  id         bigserial PRIMARY KEY,
  op         text NOT NULL,
  started_at timestamptz NOT NULL DEFAULT now(),
  finished_at timestamptz,
  note       text
);

CREATE TABLE product_change_audit (
  id         bigserial PRIMARY KEY,
  batch_id   bigint,
  op         text NOT NULL,
  sku        text NOT NULL,
  old_price  numeric(12,2),
  new_price  numeric(12,2),
  at         timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE product_alerts (
  id       bigserial PRIMARY KEY,
  kind     text NOT NULL,
  detail   text NOT NULL,
  at       timestamptz NOT NULL DEFAULT now()
);

-- 用一个临时表在「语句级 BEFORE」与「语句级 AFTER」之间传递批次号
CREATE OR REPLACE FUNCTION batch_begin() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  v_id bigint;
BEGIN
  INSERT INTO import_batch_log (op, note)
  VALUES (TG_OP, TG_TABLE_SCHEMA || '.' || TG_TABLE_NAME)
  RETURNING id INTO v_id;

  CREATE TEMP TABLE IF NOT EXISTS _batch_ctx (batch_id bigint, old_prices jsonb)
    ON COMMIT DROP;
  TRUNCATE _batch_ctx;
  INSERT INTO _batch_ctx (batch_id) VALUES (v_id);

  RETURN NULL;
END;
$$;

-- 只对 INSERT 请求 NEW TABLE
CREATE OR REPLACE FUNCTION batch_end_insert() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  v_batch bigint;
  v_rows  bigint;
BEGIN
  SELECT batch_id INTO v_batch FROM _batch_ctx;

  INSERT INTO product_change_audit (batch_id, op, sku, new_price)
  SELECT v_batch, TG_OP, sku, price FROM new_rows;

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  UPDATE import_batch_log
     SET finished_at = now(), note = note || '(新增 ' || v_rows || ' 行)'
   WHERE id = v_batch;

  RETURN NULL;
END;
$$;

-- 只对 UPDATE 请求 OLD TABLE + NEW TABLE(两张都可用)
CREATE OR REPLACE FUNCTION batch_end_update() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  v_batch bigint;
  v_drop  numeric;
BEGIN
  SELECT batch_id INTO v_batch FROM _batch_ctx;

  INSERT INTO product_change_audit (batch_id, op, sku, old_price, new_price)
  SELECT v_batch, TG_OP, n.sku, o.price, n.price
    FROM new_rows n
    JOIN old_rows o USING (sku);

  -- 汇总本次导入中价格下降的总额
  SELECT sum(o.price - n.price) INTO v_drop
    FROM new_rows n
    JOIN old_rows o USING (sku)
   WHERE n.price < o.price;

  IF v_drop IS NOT NULL AND v_drop > 0 THEN
    INSERT INTO product_alerts (kind, detail)
    VALUES ('price_drop', format('本次批量更新价格合计下降 %s', v_drop));
  END IF;

  UPDATE import_batch_log SET finished_at = now() WHERE id = v_batch;
  RETURN NULL;
END;
$$;

-- 【注意】一个触发器不能同时为 INSERT 与 UPDATE 声明不同的过渡表,
-- 因此按事件拆成三个触发器
CREATE TRIGGER trg_batch_begin
  BEFORE INSERT OR UPDATE ON products
  FOR EACH STATEMENT EXECUTE FUNCTION batch_begin();

CREATE TRIGGER trg_batch_end_insert
  AFTER INSERT ON products
  REFERENCING NEW TABLE AS new_rows
  FOR EACH STATEMENT EXECUTE FUNCTION batch_end_insert();

CREATE TRIGGER trg_batch_end_update
  AFTER UPDATE ON products
  REFERENCING OLD TABLE AS old_rows NEW TABLE AS new_rows
  FOR EACH STATEMENT EXECUTE FUNCTION batch_end_update();
text
INSERT INTO products (sku, name, price) VALUES ('SKU-001', '苹果', 19.90);
INSERT 0 1
-- 语句级 BEFORE 写入 batch_id=1
-- 语句级 AFTER 从 new_rows 读到 1 行,写入 product_change_audit

INSERT INTO products (sku, name, price)
VALUES ('SKU-002', '香蕉', 5.00), ('SKU-003', '橙子', 7.50);
INSERT 0 2

UPDATE products SET price = 18.90 WHERE sku = 'SKU-001';
UPDATE 1

-- 零行更新:行级不触发,语句级照常触发
UPDATE products SET price = 1 WHERE sku = 'NOT-EXIST';
UPDATE 0
-- batch_log 仍会新增一条记录(证明零行也会执行语句级)

SELECT id, op, finished_at IS NOT NULL AS done, note
  FROM import_batch_log ORDER BY id;
 id |   op   | done |            note
----+--------+------+---------------------------
  1 | INSERT | t    | public.products(新增 1 行)
  2 | INSERT | t    | public.products(新增 2 行)
  3 | UPDATE | t    | public.products
  4 | UPDATE | t    | public.products
(4 rows)

SELECT batch_id, op, sku, old_price, new_price FROM product_change_audit ORDER BY id;
 batch_id |   op   |   sku   | old_price | new_price
----------+--------+---------+-----------+-----------
        1 | INSERT | SKU-001 |           |     19.90
        2 | INSERT | SKU-002 |           |      5.00
        2 | INSERT | SKU-003 |           |      7.50
        3 | UPDATE | SKU-001 |     19.90 |     18.90
(4 rows)

SELECT kind, detail FROM product_alerts;
     kind     |              detail
--------------+----------------------------------
 price_drop   | 本次批量更新价格合计下降 1.00
(1 row)

对比一下:若用行级触发器做同样的审计,INSERT 2 行就会产生 2 次函数调用与 2 条独立 INSERT;语句级 + 过渡表只产生 1 次调用与 1 条批量 INSERT,行数越多差距越大。

场景:语句级 BEFORE 做批量前置校验 + 语句级 AFTER 刷新汇总 ​

需求:stock_move 表用于记录仓库流水,要求:

  1. 一次批量写入前,先确认涉及的 SKU 都存在于 sku_master(避免部分写入后才发现问题);
  2. 批量写入后重算受影响 SKU 的库存汇总(一次算完,不要逐行算)。
sql
CREATE TABLE sku_master (
  sku      text PRIMARY KEY,
  sku_name text NOT NULL
);

CREATE TABLE stock_move (
  id        bigserial PRIMARY KEY,
  sku       text NOT NULL,
  qty       int NOT NULL,          -- 正入负出
  moved_at  timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE sku_stock (
  sku     text PRIMARY KEY REFERENCES sku_master(sku),
  on_hand bigint NOT NULL DEFAULT 0,
  updated_at timestamptz
);

-- 语句级 BEFORE:批量前置校验(用临时表暂存待处理 SKU)
CREATE OR REPLACE FUNCTION stock_move_precheck() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  -- 注意:此时本语句的行还没写入,不能靠查 stock_move 来获取待处理行,
  -- 因此这里只做「全局前提」校验;逐行校验交给行级触发器或约束。
  IF NOT EXISTS (SELECT 1 FROM sku_master) THEN
    RAISE EXCEPTION '商品主数据为空,禁止录入库存流水'
      USING ERRCODE = 'P0001';
  END IF;
  RETURN NULL;
END;
$$;

-- 行级 BEFORE:逐行校验(这里必须用行级,才能拿到 NEW)
CREATE OR REPLACE FUNCTION stock_move_row_check() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  IF NOT EXISTS (SELECT 1 FROM sku_master WHERE sku = NEW.sku) THEN
    RAISE EXCEPTION '未知 SKU: %', NEW.sku
      USING ERRCODE = '23503', COLUMN = 'sku', TABLE = 'stock_move';
  END IF;
  IF NEW.qty = 0 THEN
    RAISE EXCEPTION '库存变动数不能为 0'
      USING ERRCODE = '23514', COLUMN = 'qty';
  END IF;
  RETURN NEW;
END;
$$;

-- 语句级 AFTER:一次重算所有受影响 SKU
CREATE OR REPLACE FUNCTION stock_move_resync() RETURNS trigger
LANGUAGE plpgsql AS $$
DECLARE
  v_rows bigint;
BEGIN
  INSERT INTO sku_stock (sku, on_hand, updated_at)
  SELECT m.sku, sum(m.qty), now()
    FROM stock_move m
   WHERE m.sku IN (SELECT sku FROM new_rows)   -- 只处理本次写入涉及的 SKU
   GROUP BY m.sku
  ON CONFLICT (sku) DO UPDATE
    SET on_hand = EXCLUDED.on_hand,
        updated_at = EXCLUDED.updated_at;

  GET DIAGNOSTICS v_rows = ROW_COUNT;
  RAISE NOTICE '库存汇总已刷新,涉及 % 个 SKU', v_rows;
  RETURN NULL;
END;
$$;

-- 语句级 BEFORE 前置校验
CREATE TRIGGER trg_stock_precheck
  BEFORE INSERT OR UPDATE OR DELETE ON stock_move
  FOR EACH STATEMENT EXECUTE FUNCTION stock_move_precheck();

-- 行级 BEFORE 逐行校验
CREATE TRIGGER trg_stock_row_check
  BEFORE INSERT OR UPDATE OF qty ON stock_move
  FOR EACH ROW EXECUTE FUNCTION stock_move_row_check();

-- 语句级 AFTER + 过渡表(INSERT 只请求 NEW TABLE)
CREATE TRIGGER trg_stock_resync_insert
  AFTER INSERT ON stock_move
  REFERENCING NEW TABLE AS new_rows
  FOR EACH STATEMENT EXECUTE FUNCTION stock_move_resync();
text
INSERT INTO sku_master VALUES ('SKU-001', '苹果'), ('SKU-002', '香蕉');

INSERT INTO stock_move (sku, qty) VALUES ('SKU-001', 100), ('SKU-002', 50);
NOTICE:  库存汇总已刷新,涉及 2 个 SKU
INSERT 0 2

INSERT INTO stock_move (sku, qty) VALUES ('SKU-001', 20), ('SKU-999', 5);
ERROR:  未知 SKU: SKU-999
-- 整条语句回滚:两条明细都没写入,也不会有任何汇总刷新

INSERT INTO stock_move (sku, qty) VALUES ('SKU-001', -30);
NOTICE:  库存汇总已刷新,涉及 1 个 SKU
INSERT 0 1

SELECT sku, on_hand, updated_at IS NOT NULL AS synced FROM sku_stock ORDER BY sku;
   sku   | on_hand | synced
---------+---------+--------
 SKU-001 |      90 | t
 SKU-002 |      50 | t
(2 rows)

这个例子刻意展示了职责分工:语句级 BEFORE 只能做「全局前提」判断(因为此时看不到本语句的行),逐行校验必须交给行级 BEFORE,而批量汇总交给语句级 AFTER + 过渡表。

注意事项 ​

  • 在语句级触发器里访问 NEW / OLD:它们是 NULL,运行时报 record "new" is not assigned yet。-- 错误!
    sql
    CREATE TRIGGER t AFTER INSERT ON x FOR EACH STATEMENT EXECUTE FUNCTION f();  -- f 里 NEW.col 报错
  • 语句级触发器返回 NEW:返回值被忽略,且写法误导。-- 不推荐
    sql
    RETURN NEW;    -- 应写 RETURN NULL;
  • 指望语句级 BEFORE 做逐行校验:此时本语句的行还没写入,拿不到待处理行。-- 错误!
    sql
    -- 语句级 BEFORE 里这样查不到本语句的行
    SELECT * FROM stock_move WHERE ...;    -- 只有历史数据
  • TRUNCATE 触发器写成行级:不支持。-- 错误!
    sql
    CREATE TRIGGER t BEFORE TRUNCATE ON x FOR EACH ROW ...;   -- ERROR
  • 指望语句级触发器「零行时不执行」:恰恰相反,零行时它仍然执行。依赖「语句级触发器意味着有数据被改」会出错。
  • 在一个触发器里为多个事件请求不同的过渡表:语法上不可行,必须按事件拆分触发器。-- 错误!
    sql
    CREATE TRIGGER t AFTER INSERT OR DELETE ON x
      REFERENCING NEW TABLE AS n OLD TABLE AS o    -- ERROR
      FOR EACH STATEMENT EXECUTE FUNCTION f();
  • 在视图上建语句级触发器却没建 INSTEAD OF:该语句级触发器不会被触发。-- 不推荐
  • 误判 GET DIAGNOSTICS ROW_COUNT 的含义:它返回的是函数内最近一条 SQL 的行数,不是外层触发语句的行数。-- 错误!
    sql
    GET DIAGNOSTICS v = ROW_COUNT;   -- v 是上一条 DML 的行数
  • 在分区父表上依赖子分区的语句级触发器:父表语句只触发父表自己的语句级触发器。-- 错误!
  • 在语句级 AFTER 里做长时间操作:它处在语句的收尾路径上并持有锁,会拉长事务。

最佳实践 ​

  • 大批量审计默认选语句级 + 过渡表:把 N 次 INSERT 合并为 1 次批量 INSERT。
    sql
    CREATE TRIGGER t AFTER UPDATE ON x
      REFERENCING NEW TABLE AS new_rows OLD TABLE AS old_rows
      FOR EACH STATEMENT EXECUTE FUNCTION bulk_audit();
  • 按事件拆分语句级触发器:不同事件的过渡表请求不同,拆开写更清晰也更强类型。
  • 语句级函数固定 RETURN NULL,把它当模板强制项。
  • 需要逐行数据就必须用行级:语句级只能看过渡表(AFTER 才行),BEFORE 语句级完全看不到本语句的行。
  • 把「批量开始」放语句级 BEFORE、「批量收尾」放语句级 AFTER,形成对称的钩子对。
    sql
    CREATE TRIGGER t0 BEFORE INSERT ON x FOR EACH STATEMENT EXECUTE FUNCTION batch_begin();
    CREATE TRIGGER t1 AFTER  INSERT ON x FOR EACH STATEMENT EXECUTE FUNCTION batch_end();
  • 用临时表在 BEFORE 与 AFTER 之间传递批次上下文,并加 ON COMMIT DROP 避免残留。
    sql
    CREATE TEMP TABLE IF NOT EXISTS _batch_ctx (batch_id bigint) ON COMMIT DROP;
  • 批量汇总用 INSERT ... SELECT ... GROUP BY ... ON CONFLICT DO UPDATE 一次算完,避免逐行 UPSERT。
  • TRUNCATE 触发器优先做拦截:清表是破坏性操作,用 RAISE EXCEPTION 拦住误操作收益最高。
  • 在分区表上把语句级逻辑建在父表,因为子分区的语句级触发器不会被触发。
  • 为语句级触发器配 WHEN 时只引用会话/环境条件,不要引用行变量。
  • 用 GET DIAGNOSTICS ... ROW_COUNT 记录函数内 DML 的实际影响行数,让日志可核对。
  • 文档化层级选择理由:用 COMMENT ON TRIGGER 写清「为什么是语句级而不是行级」。
    sql
    COMMENT ON TRIGGER trg_stock_resync_insert ON stock_move
      IS 'AFTER STATEMENT + 过渡表:一次重算所有受影响 SKU 的库存汇总';

下一节

继续学习:过渡表 REFERENCING

参考链接 ​