语句级触发器 FOR EACH STATEMENT
FOR EACH STATEMENT 让触发器函数对每条 DML 语句只执行一次,无论该语句影响了 0 行、1 行还是 100 万行。它拿不到 NEW / OLD(这两个变量在语句级触发器里是 NULL),因此不适合做逐行校验,但正因为「常数次调用」,它才是大批量审计、批量联动、批量统计的正确选择。PostgreSQL 中它也是默认层级——不写 FOR EACH ... 时就是语句级。
前置知识
阅读本节前,建议先了解:行级触发器 FOR EACH ROW
基础概念
调用次数模型
一条语句影响 N 行:
行级: N 次调用(N = 0 时不调用)
语句级: 1 次调用(N = 0 时也调用)★这个「零行也执行」的特性有实际价值:可以做「批量操作开始/结束」的确定性钩子,不依赖是否真的有行被改。
-- 即使一行都没影响,这条 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 条件 | 是 | 但语句级下用处有限 |
语句级的位置与顺序
语句级 BEFORE(一次)★ 最先
↓
逐行处理(行级 BEFORE → 写行 → 行级 AFTER)
↓
语句级 AFTER(一次)★ 最后固定顺序:语句级 BEFORE → 所有行级触发器 → 语句级 AFTER。所以语句级 AFTER 能确定「本语句的全部行级副作用都已发生」。
语法与用法
定义语句级触发器
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触发器时才会被触发。
函数模板
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 语句级在语句「开始做任何事之前」执行,由于此时没有任何行被处理,它的价值在于:
-- ① 前置条件检查:不满足就整体拒绝
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();-- ② 准备会话级/临时数据,供后续行级触发器读取
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 能看到本语句的全部修改:
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 只能用语句级触发器,这是它唯一合法的层级:
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 里不能引用行变量:
-- 可以使用会话/环境相关的条件
CREATE TRIGGER trg_only_app
AFTER INSERT ON audit_log
FOR EACH STATEMENT
WHEN (current_user = 'app_role')
EXECUTE FUNCTION notify_audit();-- 错误!语句级下没有 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 ...,在函数里像查询普通只读临时表一样查询受影响的行集合。
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 TABLE | NEW TABLE |
|---|---|---|
INSERT | 不可请求 | 可用 |
UPDATE | 可用 | 可用 |
DELETE | 可用 | 不可请求 |
注意:一个 REFERENCING 子句只能服务一个事件,因此 INSERT OR UPDATE OR DELETE 的合并写法在语法上会因「同一触发器请求不同过渡表」而需要用三个触发器分别声明。详见后续「过渡表」一节。
语句级与分区的交互
分区表上有两条明确的规则:
- 修改父表时,只有被显式命名的表的语句级触发器会触发,受影响的分区上的语句级触发器不触发;
- 行级触发器则会在受影响的分区上触发。
-- 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 的交互
这两处都有「语句级触发器比直觉更容易被触发」的特点:
-- 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,语句级触发器的执行顺序是:
语句级 BEFORE INSERT
→ 语句级 BEFORE UPDATE
→ 语句级 AFTER UPDATE
→ 语句级 AFTER INSERT-- 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 的行数:
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):
BEGIN;
UPDATE big_table SET flag = true; -- 语句级 BEFORE → 行级 → 语句级 AFTER
-- 若触发器抛错,整条 UPDATE 及其在触发器内的副作用一起回滚
COMMIT;因此语句级 AFTER 里做的批量写入与主语句同生共死:主语句失败,批量写入也回滚;批量写入失败,主语句也被回滚。
什么时候必须选语句级
| 场景 | 为什么必须语句级 |
|---|---|
TRUNCATE 相关逻辑 | TRUNCATE 只有语句级 |
| 大批量审计 | 避免 N 次 INSERT 的开销 |
| 「一次批量操作记一条日志」 | 行级会产生 N 条 |
| 批量操作的前置检查 | 要在任何行被处理前拦截 |
| 零行也需触发的钩子 | 行级在零行时不执行 |
| 批量刷新汇总表 | 结果只依赖最终状态,一次即可 |
实战示例
场景:批量导入的「开始/结束」双钩子 + 批量审计
需求:每天凌晨从上游系统批量导入商品数据,要求:
- 导入前记录批次开始(即使本次一行都没进来也要留痕);
- 导入结束后把「本次新增/变更」的商品一次性写入审计表(不要每行一条 INSERT);
- 本次导入若有价格下降的商品,汇总成一条告警。
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();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 表用于记录仓库流水,要求:
- 一次批量写入前,先确认涉及的 SKU 都存在于
sku_master(避免部分写入后才发现问题); - 批量写入后重算受影响 SKU 的库存汇总(一次算完,不要逐行算)。
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();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。-- 错误!sqlCREATE TRIGGER t AFTER INSERT ON x FOR EACH STATEMENT EXECUTE FUNCTION f(); -- f 里 NEW.col 报错 - 语句级触发器返回
NEW:返回值被忽略,且写法误导。-- 不推荐sqlRETURN NEW; -- 应写 RETURN NULL; - 指望语句级
BEFORE做逐行校验:此时本语句的行还没写入,拿不到待处理行。-- 错误!sql-- 语句级 BEFORE 里这样查不到本语句的行 SELECT * FROM stock_move WHERE ...; -- 只有历史数据 TRUNCATE触发器写成行级:不支持。-- 错误!sqlCREATE TRIGGER t BEFORE TRUNCATE ON x FOR EACH ROW ...; -- ERROR- 指望语句级触发器「零行时不执行」:恰恰相反,零行时它仍然执行。依赖「语句级触发器意味着有数据被改」会出错。
- 在一个触发器里为多个事件请求不同的过渡表:语法上不可行,必须按事件拆分触发器。
-- 错误!sqlCREATE 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 的行数,不是外层触发语句的行数。-- 错误!sqlGET DIAGNOSTICS v = ROW_COUNT; -- v 是上一条 DML 的行数 - 在分区父表上依赖子分区的语句级触发器:父表语句只触发父表自己的语句级触发器。
-- 错误! - 在语句级
AFTER里做长时间操作:它处在语句的收尾路径上并持有锁,会拉长事务。
最佳实践
- 大批量审计默认选语句级 + 过渡表:把 N 次
INSERT合并为 1 次批量INSERT。sqlCREATE 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,形成对称的钩子对。sqlCREATE 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避免残留。sqlCREATE 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写清「为什么是语句级而不是行级」。sqlCOMMENT ON TRIGGER trg_stock_resync_insert ON stock_move IS 'AFTER STATEMENT + 过渡表:一次重算所有受影响 SKU 的库存汇总';
下一节
继续学习:过渡表 REFERENCING