PostgreSQL: PostgreSQL触发器与事件触发器

最后更新:2026-08-26

1. 你将学到


2. 故事

Alice 是电商平台的数据库架构师。她需要实现两个核心自动化逻辑:

  1. 库存自动扣减:当新订单插入 order_items 时,BEFORE INSERT 触发器自动扣减 products.stock_qty,库存不足则拒绝插入
  2. 价格审计日志:当产品价格更新时,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_atrg_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. 流程图:触发器选择决策

100%
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 TABLEALTER TABLEDROP TABLE 等。可在 WHEN tag IN (...) 中过滤。
Q 禁用触发器后 COPY 导入更快吗?
A 是的,大量数据导入时禁用行级触发器能显著提升性能,但需注意导入后重新启用并手动处理触发器本应执行的逻辑(如计算列、审计记录)。
Q 触发器递归调用怎么避免?
A 触发器 A 更新表 T 可能再次触发 A。避免方法:用 WHEN 条件限制、设置状态变量(如包级变量)、或改用 AFTER 触发器+条件判断。

📖 小节


📝 作业

  1. ⭐ 编写一个 BEFORE UPDATE 触发器函数和触发器,当 customers 表的 email 被修改时,自动将 updated_at 设为 NOW()

  2. ⭐⭐ 编写一个 AFTER INSERT 触发器,当 orders 表新增订单且 total_amount >= 1000 USD 时,自动插入 high_value_order_log 表(记录 order_id、total_amount、customer_id、created_at)。

  3. ⭐⭐⭐ 创建视图 vw_product_sales(关联 products + order_items 计算销量),编写 INSTEAD OF UPDATE 触发器,将视图中修改的 total_sold 映射回更新 products.stock_qty。再编写一个事件触发器,记录所有 CREATE TABLEDROP TABLE 的 DDL 操作到 ddl_audit_log 表。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏