触发器中直接使用游标是反模式。因其冗余、性能差(o(n²))、作用域受限,且易引发锁表、事务不一致等问题;应改用集合操作、物化视图或外部worker等替代方案。

触发器里直接用游标是反模式
PostgreSQL 触发器函数中显式声明、打开、循环游标,几乎总是错的。触发器本身已按行(FOR EACH ROW)或语句(FOR EACH STATEMENT)粒度执行,再套一层游标不仅冗余,还会严重拖慢性能——尤其是 AFTER INSERT OR UPDATE ON big_table 这类场景,每插入一行就开一次游标、查一次子集,O(n²) 就来了。
常见错误现象:INSERT INTO orders VALUES (...) 耗时从 2ms 涨到 800ms,pg_stat_activity 显示大量 idle in transaction;日志里反复出现 cursor "xxx" does not exist —— 因为游标作用域仅限当前函数调用,无法跨触发器实例复用。
- 真正需要游标的地方,是批量后台任务(如 Rails 的
postgresql_cursorgem),不是单行 DML 的触发器 - 如果逻辑必须“对某条件集合做聚合更新”,应改用单条
UPDATE ... FROM (SELECT ...) AS sub或INSERT ... SELECT ... ON CONFLICT - 游标在触发器里唯一勉强合理的情形:极特殊调试用途(
RAISE NOTICE打印中间结果),且必须加PERFORM pg_sleep(0.001)防阻塞
复杂统计该用 AFTER 触发器 + 集合操作
比如要实时维护「每个用户最近 3 条订单的平均金额」,不能在触发器里对 NEW.user_id 去 SELECT ... ORDER BY created_at DESC LIMIT 3 再算均值——这会锁表、不可并发、随数据增长线性变慢。
正确做法是把聚合逻辑下沉到 SQL 层,让触发器只负责“标记需重算”或“原子增减”:
- 用
AFTER INSERT OR UPDATE OR DELETE触发器往一个轻量任务表插入记录:INSERT INTO user_avg_queue (user_id, op_type) VALUES (NEW.user_id, 'INSERT') - 另起一个
pg_cron任务,每 5 秒跑一次:UPDATE users SET recent_avg = (SELECT avg(amount) FROM orders WHERE user_id = users.id ORDER BY created_at DESC LIMIT 3) WHERE id IN (SELECT DISTINCT user_id FROM user_avg_queue) - 最后清空队列表:
TRUNCATE user_avg_queue
这样既避开触发器内复杂查询,又保证统计最终一致,还方便监控积压(查 user_avg_queue 行数即可)。
真要用游标,必须关掉事务自动提交
某些遗留系统硬要求触发器内遍历关联表更新,此时游标不是性能最优解,但可工作。关键前提是:函数必须声明为 VOLATILE,且内部不能有隐式事务边界。
典型错误写法:FOR rec IN SELECT * FROM related_table WHERE ref_id = NEW.id LOOP UPDATE ... END LOOP —— 这会在每次 UPDATE 后隐式提交,导致部分成功部分失败,破坏原子性。
- 必须显式用
BEGIN ... EXCEPTION WHEN OTHERS THEN ... END包裹整个游标块 - 游标声明前加
DECLARE cur CURSOR FOR SELECT ...,别用FOR循环语法糖 - 每次
FETCH后立刻检查FOUND,否则最后一轮会重复处理旧值 - 绝对不要在游标循环里调用另一个触发器函数,极易死锁
替代游标的三种更稳方案
99% 的“触发器+游标”需求,其实有更简洁、可测、易维护的替代方式:
-
CTE + INSERT ... ON CONFLICT:统计用户订单数变更,用
WITH delta AS (SELECT user_id, COUNT(*) FILTER (WHERE tg_op = 'INSERT') - COUNT(*) FILTER (WHERE tg_op = 'DELETE') AS diff FROM ...)一次性更新计数器表 -
物化视图 + REFRESH:对低频变化的复杂统计(如“各城市TOP10热销品类”),建
MATERIALIZED VIEW并用REFRESH MATERIALIZED VIEW CONCURRENTLY定期更新,比触发器更可控 -
LISTEN/NOTIFY + 外部 worker:触发器里只发通知:
PERFORM pg_notify('order_updated', NEW.id::text),由 Python/Go worker 订阅后做任意复杂计算,彻底解耦数据库与业务逻辑
游标在触发器里的最大陷阱,是让人误以为“逐行处理=精确控制”,实际它放大了锁竞争、隐藏了事务边界、且无法被单元测试覆盖。真正复杂的统计更新,从来不在数据库里做完。











