SQL触发器批量插入性能差的根本原因是逐行同步执行机制,10万行即10万次调用;MySQL不支持语句级触发器,PostgreSQL 10+需用FOR EACH STATEMENT+NEW TABLE重写;优化无效时应改用生成列、应用层传参或异步日志。

SQL触发器在批量插入时性能极差,根本原因不是写得不够好,而是它被设计为逐行同步执行——10 万行插入,就是 10 万次触发器调用,再轻的逻辑也会雪崩。所有“优化”本质都是止损:砍逻辑、绕开、或禁用。
为什么批量 INSERT 触发器会逐行执行而不是一次执行?
MySQL 和 PostgreSQL 默认都按 FOR EACH ROW 执行触发器。哪怕你写的是 INSERT INTO t VALUES (), (), ()(3 行),触发器逻辑也执行 3 次;INSERT INTO t SELECT * FROM staging LIMIT 10000 就是 1 万次。这不是 bug,是机制——数据库无法把行级上下文(NEW)抽象成集合,也就无法合并执行。
- PostgreSQL 10+ 支持
FOR EACH STATEMENT+NEW TABLE过渡表,可将 N 次调用压成 1 次,但需重写函数并放弃直接读NEW.id - MySQL 完全不支持语句级触发器,
NEW只能在行级作用域访问,没有过渡表替代方案 - SQL Server 的
inserted表虽是集合,但无统计信息,JOIN 容易退化为全表扫描
触发器里哪些操作会让单行耗时从 0.2ms 拉到 10ms+?
看似简单的一句 SELECT COUNT(*) FROM config 或 JSON_EXTRACT(NEW.payload, '$.user_id'),在批量场景下会被放大 N 倍。真正拖垮性能的往往是那些“只查一次就没事”的错觉。
-
SELECT类:任何对其他表的访问(哪怕只查一行)都会引入额外锁、解析、网络往返;IF EXISTS(SELECT 1 FROM ref WHERE id = NEW.ref_id)在批量中只校第一行,其余静默通过,还锁全表 - JSON/字符串解析类:
json_extract_path_text(NEW.payload, 'user', 'id')每调用一次就解析整段 JSON,10 万行 = 10 万次解析,无缓存、不复用 - 函数调用类:自定义函数若声明为
READS SQL DATA,每次调用都启内部子事务;NOW()在高并发下争用系统时钟 - 先赋值给局部变量:PostgreSQL 用
DECLARE v_id TEXT := json_extract_path_text(NEW.payload, 'user', 'id');;MySQL 用SET @uid := JSON_UNQUOTE(JSON_EXTRACT(NEW.payload, '$.user.id'));
禁用触发器比优化它更有效,但字段自动填充怎么办?
禁用后最常问的就是时间戳、审计字段怎么填。别再依赖触发器或会话变量——它们不可靠、难追踪、且自带开销。正确路径是让 SQL 本身带值,或用数据库原生能力兜底。
- 时间字段改用生成列:
ALTER TABLE orders ADD COLUMN updated_at DATETIME AS (NOW()) STORED;(MySQL 8.0+),写入时自动计算,零触发器开销 - 审计字段必须由应用层显式传参:
INSERT INTO orders (..., created_by) VALUES (..., ?);,而非在触发器里查@user_id或调用CURRENT_USER() - 如果业务强要求服务端统一注入,用中间件拦截 SQL 重写,而不是塞进触发器——避免权限校验、上下文切换、隐式事务三重开销
- 日志类逻辑一律异步:用 Canal / Maxwell 解析 BINLOG,或应用层发 Kafka 消息,消费者聚合后写库;同步写
audit_log是最典型的自我拖慢
真正容易被忽略的点是:你花一整天给触发器加索引、拆函数、缓存查询结果,性能可能毫无改善——因为瓶颈不在“怎么写”,而在“不该这么用”。只要批量写入量超过几千行,就该默认把触发器视为禁用项,优先检查是否能用 INSERT ... ON DUPLICATE KEY UPDATE、LOAD DATA INFILE、生成列或应用层预处理替代。


















