触发器执行时隐式开启事务,导致wal日志和页面缓存持续累积;批量操作未显式包裹事务会引发n次重复开销,内存易线性增长甚至oom,需严格控制事务边界、cache_size及journal_mode。

触发器执行时会隐式开启事务,导致WAL日志和页面缓存持续累积
SQLite 触发器本身不直接分配大量内存,但它的执行上下文会强制启用隐式事务——哪怕你没写 BEGIN。在 INSERT/UPDATE 触发器中,每条被触发的语句都可能引发额外的索引更新、外键检查、级联操作,这些都会写入 WAL 日志缓冲区,并不断填充页面缓存(Page Cache)。尤其当触发器嵌套或递归调用时,缓存不会及时释放,cache_size 设置再大也挡不住内存线性增长。
- 默认
cache_size = 2000(约 8MB),对批量触发场景完全不够用 -
journal_mode = WAL下,WAL 文件虽在磁盘,但其映射页仍驻留内存,且未 checkpoint 前不回收 - 触发器内若含
SELECT或临时表操作,还会额外占用工作内存(Working Memory)
触发器中使用子查询或跨表操作会放大临时内存开销
比如一个 BEFORE INSERT 触发器里写了 SELECT COUNT(*) FROM logs WHERE user_id = NEW.user_id,SQLite 就得为这个子查询构建独立的 B-tree 扫描路径、哈希聚合结构,甚至把整张 logs 表部分加载进内存——尤其是没索引时,全表扫描 + 排序会直接触发 temp_store = DEFAULT 回退到磁盘临时文件,但初始化阶段仍会先尝试内存分配,造成尖峰。
- 避免在触发器里做聚合、
ORDER BY、GROUP BY,除非对应字段有覆盖索引 - 用
EXPLAIN QUERY PLAN检查触发器内 SQL 是否出现USING TEMP B-TREE或USING TEMP HASH TABLE - 把复杂逻辑提到应用层,用单次批量查询替代 N 次触发器内查询
内存数据库(:memory:)下触发器更危险:无磁盘回退机制
在 :memory: 模式中,所有触发器产生的中间数据、临时索引、WAL 缓冲都只能吃 RAM。没有磁盘临时文件兜底,一旦某次触发器操作需要 50MB 工作内存,而进程只剩 60MB 可用,就容易触发 OOM 或 SQLite 返回 SQLITE_NOMEM 错误——这在磁盘模式下通常只会变慢,而非崩溃。
-
:memory:中cache_size是硬上限,超了就失败;磁盘模式还能 fallback 到文件交换 - 不要在
:memory:数据库上部署含批量插入触发器的生产逻辑 - 测试时可用
PRAGMA mmap_size = 0关闭内存映射,让内存压力更早暴露
批量写入时触发器未合并,导致 N 次重复开销
这是最常被忽略的一点:哪怕你用 INSERT INTO ... VALUES (...), (...), (...) 一次插 1000 行,只要没显式包在事务里,SQLite 默认按行逐条提交——意味着触发器会被执行 1000 次,每次都要重建执行计划、分配临时结构、刷 WAL。看起来是“批量”,实际是“伪批量”。
- 必须用
BEGIN IMMEDIATE; ... INSERT ...; COMMIT;包裹批量操作 - 触发器逻辑越重,越要控制单事务内的行数(如分批 100–500 行)
- 用
sqlite3_stmt_busy()或监控sqlite3_db_status(db, SQLITE_DBSTATUS_CACHE_USED, ...)实时观察内存水位
PRAGMA journal_mode、PRAGMA cache_size 和是否显式事务这三点。










