在实际项目中,PostgreSQL触发器是数据写入前的最后一道防线,能干不少活儿。但它有个先决条件:只对BEFORE INSERT或BEFORE UPDATE这类操作生效,并且必须配合函数来封装逻辑——直接塞多行清洗语句,数据库会直接报错。
先说几个关键点:
- 触发器定义体只认函数调用,不能内联SQL语句
- 函数返回类型必须是
TRIGGER,最后一行要么RETURN NEW(允许修改),要么RETURN NULL(丢弃整行) - 强校验的可靠方式是
RAISE EXCEPTION,它能中断整个事务
为什么触发器直接写清洗逻辑会报错?
PostgreSQL的触发器体不支持多语句块,比如连续写SET多个字段就会报ERROR: syntax error at or near "SET"。所有复杂操作都必须封装进PL/pgSQL函数。这其实是PostgreSQL设计上的约束——触发器本身只是个“钩子”,真正的逻辑要由函数来承载。

怎么写一个带清洗和校验的触发器函数?
以用户表users为例,我们希望清洗邮箱小写、抹空格、校验格式,同时拒绝非法手机号。下面这段代码是实战中验证过的写法:
CREATE OR REPLACE FUNCTION clean_and_validate_user() RETURNS TRIGGER AS $$BEGIN
-- 清洗
NEW.email := TRIM(LOWER(NEW.email));
NEW.phone := REGEXP_REPLACE(NEW.phone, '[^0-9]', '', 'g');
-- 校验
IF NEW.email !~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$' THEN
RAISE EXCEPTION 'invalid email format';
END IF;
IF LENGTH(NEW.phone) != 11 THEN
RAISE EXCEPTION 'phone must be 11 digits after cleaning';
END IF;
RETURN NEW;
END;$$ LANGUAGE plpgsql;
这里有几个容易被忽略的细节:
!~*是大小写不敏感的正则否定匹配,比先转小写再匹配高效很多REGEXP_REPLACE(..., 'g')中g参数不能省略,否则只会替换第一个非数字字符- 校验失败必须用
RAISE EXCEPTION,而不是RAISE NOTICE——后者不会中断事务,数据照样写进去
COPY导入时触发器不生效?这是设计如此
另一个常见场景:用COPY命令批量导入数据时,会发现触发器根本没被调用。这不是bug,是PostgreSQL的设计行为。想让清洗逻辑覆盖批量导入,可行的方案只有两个:
- 改用
INSERT INTO ... SELECT ... FROM ...的方式拆解导入流程,比如先从临时表批量读取,再逐行触发 - 换工具,用
pg_bulkload(需额外安装)或干脆在应用层预清洗后再COPY
千万别在触发器函数里加IF TG_OP = 'INSERT'判断来“兼容COPY”——它根本不会触发,加了也白加。
容易被忽略的性能陷阱
触发器每行都会执行一次逻辑,一旦里面包含子查询或函数调用,吞吐量会断崖式下跌。真实踩过的坑包括:
- 在触发器函数里写
(SELECT COUNT(*) FROM orders WHERE user_id = NEW.id)——每插一条就扫一遍该用户的全部订单 - 调用
CONVERT_FROM(..., 'GBK')解码乱码时没加WHERE条件过滤,导致对每行都做无意义转换 - 用
EXECUTE动态拼SQL,不仅慢,查询计划器还无法缓存
简单来说:清洗逻辑尽量用内置函数(TRIM、REGEXP_REPLACE、UPPER),校验只查必要字段,复杂逻辑移出触发器。这才是经得起高并发考验的做法。
