PostgreSQL 中触发器(Trigger)的工作原理
在 PostgreSQL 中,触发器(Trigger)是一种特殊的存储过程,它被绑定在特定的数据库表或视图上。当对这些表或视图执行特定的数据操作(如 INSERT、UPDATE、DELETE 或 TRUNCATE)时,数据库会自动“触发”并执行预先定义好的代码逻辑。
理解 PostgreSQL 触发器的工作原理,可以从以下几个核心维度来剖析:
1. 触发器的核心设计:分离架构
与其他某些数据库(如 MySQL 或 SQL Server)不同,PostgreSQL 的触发器设计分为两步,这种设计极大地提高了代码的复用性:
- 触发器函数(Trigger Function): 先使用 PL/pgSQL(或其他支持的语言如 Python、C)编写一个函数,该函数不带参数,且返回值类型必须为
trigger。 - 触发器定义(Trigger Definition): 将这个函数绑定到具体的表和具体的事件上。一个触发器函数可以被多个不同的触发器(甚至不同的表)复用。
2. 触发器的三大核心要素
A. 触发时机(Timing)
触发器可以在数据修改发生的不同阶段执行:
- BEFORE: 在实际的数据修改(插入、更新、删除)发生之前执行。
- 用途: 常用于数据校验、修改即将插入/更新的值(例如自动生成时间戳,或强制数据大写)。
- 原理: 在 BEFORE 行级触发器中,你可以修改内置变量
NEW,甚至返回NULL来拦截(取消)这次操作。
- AFTER: 在实际的数据修改发生之后执行。
- 用途: 常用于记录审计日志(Audit logs)、同步更新其他表的数据。此时数据已经落盘,可以安全地读取。
- INSTEAD OF: 代替原来的操作执行。
- 用途: 仅适用于视图(Views)。因为复杂的视图通常无法直接进行
INSERT/UPDATE/DELETE,可以通过它来手动编写更新底层基础表的逻辑。
- 用途: 仅适用于视图(Views)。因为复杂的视图通常无法直接进行
B. 触发事件(Events)
可以引起触发器执行的 SQL 操作:
INSERTUPDATE(可以精确到某几列被更新时才触发,例如UPDATE OF salary)DELETETRUNCATE(仅支持语句级触发器)
C. 触发级别(Level)
- 行级触发器(Row-Level,
FOR EACH ROW):
受到影响的每一行数据都会执行一次触发器。例如一条UPDATE语句更新了 100 行记录,行级触发器就会被执行 100 次。 - 语句级触发器(Statement-Level,
FOR EACH STATEMENT):
无论一条 SQL 语句影响了多少行(哪怕是 0 行),触发器都只执行一次。默认级别是语句级。
3. 工作原理中的“上下文变量”
在触发器函数内部,PostgreSQL 提供了一组特殊的内置变量,用于获取当前操作的上下文状态:
NEW:包含INSERT或UPDATE操作后的新数据行(DELETE时为 NULL)。OLD:包含UPDATE或DELETE操作前的旧数据行(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 内部的执行顺序如下:
- 执行 BEFORE 语句级触发器(如果有)。
- 针对第 1 行数据:
- 执行 BEFORE 行级触发器。
- 执行实际的 UPDATE 写入。
- 针对第 2 行数据:
- 执行 BEFORE 行级触发器。
- 执行实际的 UPDATE 写入。
- 针对第 3 行数据:
- 执行 BEFORE 行级触发器。
- 执行实际的 UPDATE 写入。
- 执行所有受影响行的 AFTER 行级触发器(针对第1, 2, 3行分别执行)。
- 执行 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. 高级特性与最佳实践
- WHEN 条件(性能优化):
在创建触发器时,可以使用WHEN子句过滤不必要的触发。这比在触发器函数内部写IF性能更好,因为避免了调用函数的开销。sqlCREATE TRIGGER ... WHEN (OLD.salary IS DISTINCT FROM NEW.salary) ... - 事件触发器(Event Triggers):
除了上述针对 DML(增删改)的触发器,PostgreSQL 还支持针对 DDL(如CREATE TABLE,DROP TABLE)的事件触发器,常用于全局架构保护。 - 慎重使用:
- 不可见性(Hidden Logic): 触发器在后台默默运行,如果滥用,会让系统的业务逻辑变得难以追踪(被称为“幽灵代码”)。
- 性能损耗: 行级触发器在处理大批量数据(Bulk Operations)时会导致性能急剧下降,因为每行都要上下文切换执行一次函数。大批量数据处理时,优先考虑语句级触发器或在应用层解决。
右滑查看面试常问