PostgreSQL: PostgreSQL触发器与事件触发器
最后更新:2026-08-26
1. 你将学到
- CREATE TRIGGER(BEFORE / AFTER / INSTEAD OF)
- 行级触发器 vs 语句级触发器
- NEW / OLD 记录变量
- 条件触发器(WHEN 子句)
- 触发器执行顺序(按名称字母序)
- INSTEAD OF 触发器(视图上)
- 事件触发器(DDL 事件:CREATE/ALTER/DROP TABLE)
- 启用/禁用触发器
- 触发器 vs 应用逻辑
2. 故事
Alice 是电商平台的数据库架构师。她需要实现两个核心自动化逻辑:
- 库存自动扣减:当新订单插入
order_items时,BEFORE INSERT 触发器自动扣减products.stock_qty,库存不足则拒绝插入 - 价格审计日志:当产品价格更新时,AFTER UPDATE 触发器将旧价格和新价格记录到
price_audit_log表
Alice 选择用触发器实现,确保无论哪个应用写入数据,业务规则都能一致执行。
3. Concept:触发器基础
(1) 触发器类型全景
| 触发时机 | 行级(FOR EACH ROW) | 语句级(FOR EACH STATEMENT) |
|---|---|---|
| BEFORE | 可修改 NEW、拒绝操作 | 无 NEW/OLD,可校验/准备 |
| AFTER | 可读 NEW/OLD,记录日志 | 适合汇总统计 |
| INSTEAD OF | 仅用于视图,替代原操作 | 不适用于语句级 |
▶ 示例:创建基础 BEFORE INSERT 行级触发器
SQL
CREATE OR REPLACE FUNCTION fn_before_order_item()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.created_at := NOW();
NEW.line_total := NEW.quantity * NEW.unit_price;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_before_order_item
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_before_order_item();
输出:
TEXT
📖 仅展示
INSERT 0 1
▶ 示例:AFTER UPDATE 审计日志触发器
SQL
CREATE OR REPLACE FUNCTION fn_audit_price_change()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
INSERT INTO price_audit_log
(product_id, old_price, new_price, changed_by, changed_at)
VALUES
(NEW.product_id, OLD.unit_price, NEW.unit_price,
CURRENT_USER, NOW());
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_audit_price
AFTER UPDATE OF unit_price ON products
FOR EACH ROW
EXECUTE FUNCTION fn_audit_price_change();
输出:
TEXT
📖 仅展示
INSERT 0 1
(2) NEW 与 OLD 变量
| 触发时机 | NEW | OLD | 能否修改 |
|---|---|---|---|
| BEFORE INSERT | 有(待插入行) | 无 | 可修改 NEW |
| BEFORE UPDATE | 有(新值) | 有(旧值) | 可修改 NEW |
| BEFORE DELETE | 无 | 有(待删除行) | 不可修改 |
| AFTER INSERT | 有(只读) | 无 | 不可修改 |
| AFTER UPDATE | 有(只读) | 有(只读) | 不可修改 |
▶ 示例:BEFORE UPDATE 修改 NEW
SQL
CREATE OR REPLACE FUNCTION fn_auto_update_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_products_updated_at
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION fn_auto_update_timestamp();
输出:
TEXT
📖 仅展示
CREATE TABLE
4. Concept:行级 vs 语句级触发器
(1) 执行频率差异
| 维度 | FOR EACH ROW | FOR EACH STATEMENT |
|---|---|---|
| 执行次数 | 每影响一行执行一次 | 每条 SQL 执行一次 |
| NEW/OLD | 可用 | 不可用 |
| 性能影响 | 受影响行数影响 | 固定开销 |
| 典型用途 | 数据校验、列计算、级联操作 | 统计汇总、缓存刷新 |
▶ 示例:语句级触发器刷新汇总
SQL
CREATE OR REPLACE FUNCTION fn_refresh_order_stats()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_order_stats;
RETURN NULL;
END;
$$;
CREATE TRIGGER trg_refresh_order_stats
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH STATEMENT
EXECUTE FUNCTION fn_refresh_order_stats();
输出:
TEXT
📖 仅展示
INSERT 0 1
▶ 示例:行级触发器验证库存
SQL
CREATE OR REPLACE FUNCTION fn_check_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_stock INT;
BEGIN
SELECT stock_qty INTO v_stock
FROM products
WHERE product_id = NEW.product_id;
IF v_stock < NEW.quantity THEN
RAISE EXCEPTION 'Insufficient stock: product % has % units, requested %',
NEW.product_id, v_stock, NEW.quantity;
END IF;
UPDATE products
SET stock_qty = stock_qty - NEW.quantity
WHERE product_id = NEW.product_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_check_stock();
输出:
TEXT
📖 仅展示
INSERT 0 1
5. Concept:条件触发器与执行顺序
(1) WHEN 子句
WHEN 子句让触发器仅在满足条件时执行,减少不必要的调用开销。
▶ 示例:WHEN 条件触发器
SQL
CREATE OR REPLACE FUNCTION fn_log_big_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO big_order_log (order_id, total_amount, created_at)
VALUES (NEW.order_id, NEW.total_amount, NOW());
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_log_big_order
AFTER INSERT ON orders
FOR EACH ROW
WHEN (NEW.total_amount >= 5000)
EXECUTE FUNCTION fn_log_big_order();
输出:
TEXT
📖 仅展示
INSERT 0 1
(2) 触发器执行顺序规则
| 规则 | 说明 |
|---|---|
| BEFORE 先于 AFTER | 所有 BEFORE 执行完才执行 AFTER |
| 同时机按名称字母序 | trg_a 在 trg_b 前执行 |
| INSTEAD OF 替代原操作 | 仅视图上,不执行原 INSERT/UPDATE/DELETE |
| BEFORE 返回 NULL 阻止操作 | 对 UPDATE/INSERT,返回 NULL 则跳过该行 |
▶ 示例:多个触发器的执行顺序
SQL
-- These triggers execute in alphabetical order: a -> b -> c
CREATE TRIGGER trg_a_validate
BEFORE INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_validate_order();
CREATE TRIGGER trg_b_calc_tax
BEFORE INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_calc_order_tax();
CREATE TRIGGER trg_c_notify
AFTER INSERT ON orders FOR EACH ROW
EXECUTE FUNCTION fn_notify_new_order();
输出:
TEXT
📖 仅展示
INSERT 0 1
6. Concept:INSTEAD OF 触发器
(1) 视图上的 INSTEAD OF
视图本身不支持直接 INSERT/UPDATE/DELETE,INSTEAD OF 触发器拦截操作并自定义执行逻辑。
| 特性 | 说明 |
|---|---|
| 仅用于视图 | 表上不能用 INSTEAD OF |
| 必须是 FOR EACH ROW | 不支持语句级 |
| 替代原操作 | 不执行默认行为 |
| 适合可更新视图 | 将视图写入映射到基础表 |
▶ 示例:INSTEAD OF INSERT 到视图
SQL
CREATE VIEW vw_customer_orders AS
SELECT
c.customer_id,
c.first_name,
c.last_name,
o.order_id,
o.total_amount
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id;
CREATE OR REPLACE FUNCTION fn_insert_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO customers (first_name, last_name)
VALUES (NEW.first_name, NEW.last_name)
ON CONFLICT DO NOTHING;
INSERT INTO orders (customer_id, total_amount, order_date)
SELECT customer_id, NEW.total_amount, CURRENT_DATE
FROM customers
WHERE first_name = NEW.first_name AND last_name = NEW.last_name;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_insert_customer_order
INSTEAD OF INSERT ON vw_customer_orders
FOR EACH ROW
EXECUTE FUNCTION fn_insert_customer_order();
输出:
TEXT
📖 仅展示
INSERT 0 1
▶ 示例:INSTEAD OF UPDATE 到视图
SQL
CREATE OR REPLACE FUNCTION fn_update_customer_order()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE customers
SET first_name = NEW.first_name,
last_name = NEW.last_name
WHERE customer_id = NEW.customer_id;
UPDATE orders
SET total_amount = NEW.total_amount
WHERE order_id = NEW.order_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_update_customer_order
INSTEAD OF UPDATE ON vw_customer_orders
FOR EACH ROW
EXECUTE FUNCTION fn_update_customer_order();
输出:
TEXT
📖 仅展示
CREATE TABLE
7. Concept:事件触发器
(1) DDL 事件触发器(PG 特色)
事件触发器在 DDL 命令(CREATE/ALTER/DROP)时触发,独立于具体表。
| 事件 | 触发时机 |
|---|---|
ddl_command_start |
DDL 执行前 |
ddl_command_end |
DDL 执行后 |
sql_drop |
DROP 命令执行前 |
table_rewrite |
表重写前(如 ALTER TYPE) |
▶ 示例:禁止 DROP TABLE 的事件触发器
SQL
CREATE OR REPLACE FUNCTION fn_block_drop_table()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'DROP TABLE is not allowed in production!';
END;
$$;
CREATE EVENT TRIGGER etg_block_drop
ON sql_drop
WHEN tag IN ('DROP TABLE')
EXECUTE FUNCTION fn_block_drop_table();
输出:
TEXT
📖 仅展示
CREATE TABLE
▶ 示例:DDL 审计日志
SQL
CREATE TABLE ddl_audit_log (
id SERIAL PRIMARY KEY,
event_type TEXT,
tag TEXT,
object_type TEXT,
object_name TEXT,
command_text TEXT,
current_user TEXT,
event_time TIMESTAMP DEFAULT NOW()
);
CREATE OR REPLACE FUNCTION fn_log_ddl()
RETURNS EVENT_TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_obj RECORD;
BEGIN
v_obj := NULL;
INSERT INTO ddl_audit_log (event_type, tag, object_type, object_name, current_user)
VALUES (TG_EVENT, TG_TAG,
v_obj.object_type, v_obj.object_identity,
CURRENT_USER);
RAISE NOTICE 'DDL logged: % %', TG_EVENT, TG_TAG;
END;
$$;
CREATE EVENT TRIGGER etg_log_ddl
ON ddl_command_end
EXECUTE FUNCTION fn_log_ddl();
输出:
TEXT
📖 仅展示
INSERT 0 1
(2) 事件触发器 vs 表触发器
| 维度 | 表触发器 | 事件触发器 |
|---|---|---|
| 绑定对象 | 具体表/视图 | 全局 DDL 事件 |
| DML/DDL | DML(INSERT/UPDATE/DELETE) | DDL(CREATE/ALTER/DROP) |
| NEW/OLD | 有 | 无 |
| TG_TAG | 无 | 有(标签如 DROP TABLE) |
| 典型用途 | 数据校验、审计、级联 | DDL 审计、安全管控 |
8. Concept:启用/禁用与触发器 vs 应用逻辑
(1) 启用/禁用触发器
| 命令 | 效果 |
|---|---|
ALTER TABLE t DISABLE TRIGGER trg_name; |
禁用指定触发器 |
ALTER TABLE t ENABLE TRIGGER trg_name; |
启用指定触发器 |
ALTER TABLE t DISABLE TRIGGER ALL; |
禁用所有触发器 |
ALTER TABLE t ENABLE TRIGGER ALL; |
启用所有触发器 |
▶ 示例:批量导入时禁用触发器
SQL
-- Disable triggers for bulk import performance
ALTER TABLE products DISABLE TRIGGER ALL;
COPY products(product_name, unit_price, category, stock_qty)
FROM '/data/products_bulk.csv' WITH (FORMAT csv, HEADER true);
-- Re-enable after import
ALTER TABLE products ENABLE TRIGGER ALL;
输出:
TEXT
📖 仅展示
-- SQL 语句执行成功
(2) 触发器 vs 应用逻辑
| 维度 | 触发器 | 应用逻辑 |
|---|---|---|
| 一致性 | 任何写入方式都触发 | 依赖应用遵守规则 |
| 调试难度 | 隐式执行,难追踪 | 显式调用,易调试 |
| 性能 | 每行额外开销 | 可批量优化 |
| 可移植性 | PG 专有语法 | 通用语言 |
| 适用场景 | 强制不变量、审计、级联 | 复杂业务流程、跨系统 |
▶ 示例:触发器保证数据一致性
SQL
-- Enforce: order total must always equal sum of line items
CREATE OR REPLACE FUNCTION fn_enforce_order_total()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_calculated NUMERIC;
BEGIN
SELECT COALESCE(SUM(line_total), 0) INTO v_calculated
FROM order_items
WHERE order_id = NEW.order_id;
IF v_calculated IS DISTINCT FROM (
SELECT total_amount FROM orders WHERE order_id = NEW.order_id
) THEN
UPDATE orders SET total_amount = v_calculated
WHERE order_id = NEW.order_id;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_enforce_order_total
AFTER INSERT OR UPDATE ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_enforce_order_total();
输出:
TEXT
📖 仅展示
result
----------
42.50
(1 row)
9. 流程图:触发器选择决策
flowchart TD
A[需要自动化逻辑?] --> B{DML 还是 DDL?}
B -->|DDL| C[事件触发器<br/>EVENT TRIGGER]
B -->|DML| D{操作表还是视图?}
D -->|视图| E[INSTEAD OF 触发器<br/>FOR EACH ROW]
D -->|表| F{需要在操作前修改数据?}
F -->|是| G[BEFORE 触发器<br/>可修改 NEW]
F -->|否| H{需要记录日志/级联?}
H -->|是| I[AFTER 触发器<br/>可读 NEW/OLD]
H -->|否| J[不需要触发器]
G --> K{每行还是每条SQL?}
I --> K
K -->|每行| L[FOR EACH ROW]
K -->|每SQL| M[FOR EACH STATEMENT]
L --> N{需限制条件?}
N -->|是| O[WHEN 子句]
N -->|否| P[无条件]
style C fill:#fff9c4
style E fill:#e1bee7
style G fill:#c8e6c9
style I fill:#bbdefb
10. 综合示例
Alice 的电商触发器方案——库存扣减 + 价格审计 + 时间戳自动更新:
SQL
-- 1. Auto-update timestamp on product changes
CREATE OR REPLACE FUNCTION fn_products_timestamp()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_products_timestamp
BEFORE UPDATE ON products
FOR EACH ROW
EXECUTE FUNCTION fn_products_timestamp();
-- 2. Decrease stock on order item insert, reject if insufficient
CREATE OR REPLACE FUNCTION fn_decrease_stock()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_stock INT;
BEGIN
SELECT stock_qty INTO v_stock FROM products
WHERE product_id = NEW.product_id FOR UPDATE;
IF v_stock IS NULL THEN
RAISE EXCEPTION 'Product % not found', NEW.product_id;
ELSIF v_stock < NEW.quantity THEN
RAISE EXCEPTION 'Insufficient stock: product % (stock=%, requested=%)',
NEW.product_id, v_stock, NEW.quantity;
END IF;
UPDATE products SET stock_qty = stock_qty - NEW.quantity
WHERE product_id = NEW.product_id;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_decrease_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION fn_decrease_stock();
-- 3. Audit log when price changes
CREATE OR REPLACE FUNCTION fn_price_audit()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.unit_price IS DISTINCT FROM OLD.unit_price THEN
INSERT INTO price_audit_log
(product_id, old_price, new_price, changed_by, changed_at)
VALUES
(NEW.product_id, OLD.unit_price, NEW.unit_price,
CURRENT_USER, NOW());
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_price_audit
AFTER UPDATE OF unit_price ON products
FOR EACH ROW
WHEN (OLD.unit_price IS DISTINCT FROM NEW.unit_price)
EXECUTE FUNCTION fn_price_audit();
❓ 常见问题
Q BEFORE 触发器返回 NULL 会怎样?
A 对 INSERT/UPDATE,返回 NULL 表示跳过该行操作(不执行实际写入和后续触发器)。对 DELETE 无效。注意 AFTER 触发器的返回值被忽略。
Q 同一个表同一时机有多个触发器,执行顺序怎么定?
A PostgreSQL 按触发器名称的字母序排列执行。如
trg_a 先于 trg_b。BEFORE 全部执行完后才执行 AFTER。Q 触发器里能执行 COMMIT 吗?
A 普通表触发器不能执行 COMMIT/ROLLBACK(在事务内部)。如需自主事务,可在触发器函数中用 dblink 或 PG 14+ 的过程化方案。
Q WHEN 子句里的条件能引用其他表吗?
A 不能。WHEN 条件只能引用 NEW/OLD 的列,不能包含子查询或引用其他表。需要复杂条件判断时,在触发器函数体内部检查。
Q INSTEAD OF 触发器能用 FOR EACH STATEMENT 吗?
A 不能。INSTEAD OF 触发器必须是 FOR EACH ROW,因为需要逐行决定如何映射到基础表操作。
Q 事件触发器的 TG_TAG 是什么?
A TG_TAG 是触发该事件的 DDL 命令标签,如
CREATE TABLE、ALTER TABLE、DROP TABLE 等。可在 WHEN tag IN (...) 中过滤。Q 禁用触发器后 COPY 导入更快吗?
A 是的,大量数据导入时禁用行级触发器能显著提升性能,但需注意导入后重新启用并手动处理触发器本应执行的逻辑(如计算列、审计记录)。
Q 触发器递归调用怎么避免?
A 触发器 A 更新表 T 可能再次触发 A。避免方法:用 WHEN 条件限制、设置状态变量(如包级变量)、或改用 AFTER 触发器+条件判断。
📖 小节
- 触发时机:BEFORE(可修改 NEW)、AFTER(记录日志)、INSTEAD OF(视图映射)
- 行级触发器每行执行,语句级每条 SQL 执行一次
- NEW 存新值,OLD 存旧值,BEFORE 阶段可修改 NEW
- WHEN 子句条件过滤,减少不必要的触发器调用
- 同时机触发器按名称字母序执行
- INSTEAD OF 仅用于视图,必须 FOR EACH ROW
- 事件触发器(PG 特色)监听 DDL 命令,适合审计和安全管控
- DISABLE/ENABLE TRIGGER 控制触发器状态,批量导入时禁用可提速
- 触发器保证数据一致性,但增加调试复杂度,需权衡使用
📝 作业
-
⭐ 编写一个 BEFORE UPDATE 触发器函数和触发器,当
customers表的email被修改时,自动将updated_at设为NOW()。 -
⭐⭐ 编写一个 AFTER INSERT 触发器,当
orders表新增订单且total_amount >= 1000USD 时,自动插入high_value_order_log表(记录 order_id、total_amount、customer_id、created_at)。 -
⭐⭐⭐ 创建视图
vw_product_sales(关联 products + order_items 计算销量),编写 INSTEAD OF UPDATE 触发器,将视图中修改的total_sold映射回更新products.stock_qty。再编写一个事件触发器,记录所有CREATE TABLE和DROP TABLE的 DDL 操作到ddl_audit_log表。