基于本文回答
0
评论

PostgreSQL 中触发器(Trigger)的工作原理

知识点图片

在 PostgreSQL 中,触发器(Trigger)是一种特殊的存储过程,它被绑定在特定的数据库表或视图上。当对这些表或视图执行特定的数据操作(如 INSERTUPDATEDELETETRUNCATE)时,数据库会自动“触发”并执行预先定义好的代码逻辑。

理解 PostgreSQL 触发器的工作原理,可以从以下几个核心维度来剖析:


1. 触发器的核心设计:分离架构

与其他某些数据库(如 MySQL 或 SQL Server)不同,PostgreSQL 的触发器设计分为两步,这种设计极大地提高了代码的复用性:

  1. 触发器函数(Trigger Function): 先使用 PL/pgSQL(或其他支持的语言如 Python、C)编写一个函数,该函数不带参数,且返回值类型必须为 trigger
  2. 触发器定义(Trigger Definition): 将这个函数绑定到具体的表和具体的事件上。一个触发器函数可以被多个不同的触发器(甚至不同的表)复用。

2. 触发器的三大核心要素

A. 触发时机(Timing)

触发器可以在数据修改发生的不同阶段执行:

  • BEFORE: 在实际的数据修改(插入、更新、删除)发生之前执行。
    • 用途: 常用于数据校验、修改即将插入/更新的值(例如自动生成时间戳,或强制数据大写)。
    • 原理: 在 BEFORE 行级触发器中,你可以修改内置变量 NEW,甚至返回 NULL拦截(取消)这次操作。
  • AFTER: 在实际的数据修改发生之后执行。
    • 用途: 常用于记录审计日志(Audit logs)、同步更新其他表的数据。此时数据已经落盘,可以安全地读取。
  • INSTEAD OF: 代替原来的操作执行。
    • 用途: 仅适用于视图(Views)。因为复杂的视图通常无法直接进行 INSERT/UPDATE/DELETE,可以通过它来手动编写更新底层基础表的逻辑。

B. 触发事件(Events)

可以引起触发器执行的 SQL 操作:

  • INSERT
  • UPDATE(可以精确到某几列被更新时才触发,例如 UPDATE OF salary
  • DELETE
  • TRUNCATE(仅支持语句级触发器)

C. 触发级别(Level)

  • 行级触发器(Row-Level, FOR EACH ROW):
    受到影响的每一行数据都会执行一次触发器。例如一条 UPDATE 语句更新了 100 行记录,行级触发器就会被执行 100 次。
  • 语句级触发器(Statement-Level, FOR EACH STATEMENT):
    无论一条 SQL 语句影响了多少行(哪怕是 0 行),触发器都只执行一次。默认级别是语句级。

3. 工作原理中的“上下文变量”

在触发器函数内部,PostgreSQL 提供了一组特殊的内置变量,用于获取当前操作的上下文状态:

  • NEW:包含 INSERTUPDATE 操作后的新数据行(DELETE 时为 NULL)。
  • OLD:包含 UPDATEDELETE 操作前的旧数据行(INSERT 时为 NULL)。
  • TG_OP:触发操作的名称字符串(如 'INSERT', 'UPDATE', 'DELETE')。
  • TG_TABLE_NAME:触发器所在表的名称。
  • TG_WHEN:触发时机('BEFORE', 'AFTER', 'INSTEAD OF')。
  • TG_LEVEL:触发级别('ROW', 'STATEMENT')。

4. 触发器的完整执行流程(以 UPDATE 为例)

假设你对一张表执行了一条影响 3 行数据的 UPDATE 语句,并且表上定义了所有的触发器,PostgreSQL 内部的执行顺序如下:

  1. 执行 BEFORE 语句级触发器(如果有)。
  2. 针对第 1 行数据:
    • 执行 BEFORE 行级触发器。
    • 执行实际的 UPDATE 写入。
  3. 针对第 2 行数据:
    • 执行 BEFORE 行级触发器。
    • 执行实际的 UPDATE 写入。
  4. 针对第 3 行数据:
    • 执行 BEFORE 行级触发器。
    • 执行实际的 UPDATE 写入。
  5. 执行所有受影响行的 AFTER 行级触发器(针对第1, 2, 3行分别执行)。
  6. 执行 AFTER 语句级触发器(如果有)。

5. 简单示例:员工薪资审计日志

这个例子展示了触发器如何工作。需求:每当员工的薪资发生变化时,自动将旧薪资和新薪资记录到审计表中。

步骤 1:创建表

sql
CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name VARCHAR(50),
    salary NUMERIC
);

CREATE TABLE salary_audit (
    emp_id INT,
    old_salary NUMERIC,
    new_salary NUMERIC,
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

步骤 2:编写触发器函数

sql
CREATE OR REPLACE FUNCTION log_salary_change()
RETURNS TRIGGER AS $$
BEGIN
    -- 只有当薪资真正发生变化时才记录
    IF NEW.salary <> OLD.salary THEN
        INSERT INTO salary_audit (emp_id, old_salary, new_salary)
        VALUES (OLD.id, OLD.salary, NEW.salary);
    END IF;
    
    -- 对于 AFTER 触发器,返回值通常被忽略,但按规范返回 NEW
    RETURN NEW; 
END;
$$ LANGUAGE plpgsql;

步骤 3:绑定触发器到表

sql
CREATE TRIGGER trigger_salary_change
AFTER UPDATE OF salary ON employees -- 仅在 salary 列更新后触发
FOR EACH ROW -- 行级触发器
EXECUTE FUNCTION log_salary_change();

6. 高级特性与最佳实践

  1. WHEN 条件(性能优化):
    在创建触发器时,可以使用 WHEN 子句过滤不必要的触发。这比在触发器函数内部写 IF 性能更好,因为避免了调用函数的开销。
    sql
    CREATE TRIGGER ... WHEN (OLD.salary IS DISTINCT FROM NEW.salary) ...
  2. 事件触发器(Event Triggers):
    除了上述针对 DML(增删改)的触发器,PostgreSQL 还支持针对 DDL(如 CREATE TABLE, DROP TABLE)的事件触发器,常用于全局架构保护。
  3. 慎重使用:
    • 不可见性(Hidden Logic): 触发器在后台默默运行,如果滥用,会让系统的业务逻辑变得难以追踪(被称为“幽灵代码”)。
    • 性能损耗: 行级触发器在处理大批量数据(Bulk Operations)时会导致性能急剧下降,因为每行都要上下文切换执行一次函数。大批量数据处理时,优先考虑语句级触发器或在应用层解决。
右滑查看面试常问