先聊点实际的。我以前刚接触SQLite那会儿,看到“触发器”三个字,第一反应是数字电路里的D触发器、JK触发器,脑子里全是波形图和边沿触发那套东西。后来被同事纠正,才明白SQLite里的触发器完全是另一码事——它是在数据库表上挂的“自动回调函数”。
要说这东西冷门吧,其实每个做客户端开发、嵌入式开发、甚至用Python写小工具的人,早晚都得撞上它。最常见的一个需求:业务表里每条记录的“最后更新时间”字段,每次UPDATE都要手动带上当前时间戳,写得多了就烦,漏写一次数据就脏了。再比如给用户表做操作审计,谁在什么时候改了哪个字段,如果全靠业务代码里挨个埋点,代码库能脏成什么样,想想都知道。SQLite触发器就是干这个的,它能在INSERT、UPDATE、DELETE发生时自动执行一段预定义的SQL逻辑,不需要你改一行业务代码。
这篇文章,我就把SQLite触发器的设计思路、语法细节、实操案例、以及我实际踩过的坑一次性讲透。适合用它做本地存储的客户端开发者、刚入手SQLite的初学者。我会从最简单的场景讲起,一直讲到一个相对完善的生产级触发器方案,尽量把每个“为什么这么做”都交代清楚。
1. 先把概念拧清楚:SQLite触发器到底是什么
1.1 它不是数字电路里的触发器
搜“SQLite触发器”的时候,很多人会连带搜到“D触发器”“JK触发器”“RS触发器工作原理”。这是两套完全同名的东西,但毫无关系。数字电路里的触发器是存储电路元件,靠着时钟边沿保存一位数据;SQLite里的触发器,可以理解成一张表上的“哨兵”——当指定的数据变更操作发生,哨兵就去执行一段你提前写好的SQL。
为什么搜SQLite的人会搜到数字电路的东西呢?我猜测是很多入门者一开始没搞明白这两者的区别,在搜索引擎里反反复复试关键词,结果被带偏了。这里就直接说结论:如果你的目标是学数据库触发器,看到时序图、波形图、真值表之类的内容,直接关掉,那跟你要做的事情没关系。
SQLite的触发器属于“行级触发器”,它在数据行的操作层面生效,每处理一行数据就会触发一次。逻辑上它做得非常像现代编程语言里的“事件监听器”——但比事件监听器更底层,它是数据库引擎自己执行的,不受应用层是否调用、是否忘记调用的影响,天然具备“不可绕过”的属性。这对于维护数据一致性来说,价值非常大。
1.2 SQLite触发器的三类触发时机
SQLite支持三类触发时机,分别是BEFORE、AFTER和INSTEAD OF。每一个都对应着不同的执行阶段。
BEFORE触发器在数据变更动作真正执行之前运行。如果你在BEFORE触发器里用RAISE(ABORT)抛出异常,这个数据变更会被整体取消。比较典型的用法是复杂校验、加密字段值、给字段设定默认值——这些操作必须在数据真正落盘前完成。
AFTER触发器在数据变更动作完成之后运行。它不会影响已经被修改的数据行(因为数据已经变了),适合做审计日志、同步更新冗余表、清理关联数据。我用它做得最多的就是操作审计。
INSTEAD OF触发器比较特殊,它只适用于视图(VIEW)。SQLite里的视图默认是不可写的,但你可以给视图创建INSTEAD OF触发器,在应用层看起来是对视图做INSERT/UPDATE,实际上由触发器把操作分散到真正的基础表上。这对那些有复杂JOIN视图更新需求的业务,几乎是标准解法。
触发粒度方面,SQLite只有FOR EACH ROW,不支持FOR EACH STATEMENT。也就是说,一条UPDATE语句如果影响了100行,这个触发器就会被执行100次。这个特性有好有坏:好处是每行数据都能用NEW和OLD分别拿到当前行和旧行的值,逻辑可以写得非常细;坏处是如果你在大量数据上做批量操作,触发器的执行开销会被放大,后面我会给一个性能优化思路。
1.3 触发器的“新旧行”概念
这是SQLite触发器最核心、也最容易搞混的概念。
在触发器内部,SQLite向用户暴露了四个特殊变量:
| 变量 | 含义 | 适用场景 |
|---|---|---|
| NEW | 变更后的数据行 | INSERT / UPDATE 触发器(部分列可修改) |
| OLD | 变更前的数据行 | DELETE / UPDATE 触发器(只读) |
| NEW.列名 | 变更后某列的值 | UPDATE时通常取它做后续写入 |
| OLD.列名 | 变更前某列的值 | 审计时需要拿它记录“改前内容” |
举个例子。假设你有一张用户表users,字段有id、name、credit。你想记录积分credit的每次变动,那么在UPDATE触发器里,OLD.credit就是变动前的积分,NEW.credit就是变动后的积分,两者之差就是本次变动幅度。你这个审计功能甚至不需要业务侧提供任何额外参数,完全由数据库自动推导。
需要特别注意的是,在BEFORE INSERT触发器里,NEW.列名是可以被赋值的。这意味着你可以在数据真正插入前,改写某个字段的内容。比如一个字段要求必须小写存储,但业务侧漏做了lower处理,BEFORE INSERT触发器可以直接SET NEW.name = LOWER(NEW.name),把“保险丝”放在最靠近数据的一环。
但OLD在DELETE触发器里是只读的,你不能通过MODIFY OLD.xxx去修改正在被删除的行的内容,因为它已经没有修改的意义了,你只能“读取”它然后决定做什么,比如把它复制到另一张历史表里。
注意,所有引用到OLD/NEW的赋值都要求目标列是实际存在的,如果拼错了列名,SQLite不会在第1行报错,而是在触发器执行到那一行的时候才给你报错。这类错误隐蔽性很高,所以后面我会强烈建议你在写完每一个触发器后至少做一次完整触发链路的联调,而不是只看建表是否成功。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心语法拆解与设计思路
2.1 CREATE TRIGGER 标准语法
创建一个触发器,SQLite的语法整体长这样:
sql复制CREATE [TEMP | TEMPORARY] TRIGGER [IF NOT EXISTS] 触发器名
[BEFORE | AFTER | INSTEAD OF] [INSERT | DELETE | UPDATE [OF 列名列表]]
ON 表名
[FOR EACH ROW]
[WHEN 条件表达式]
BEGIN
-- 触发器要执行的SQL逻辑
END;
方括号里的都是可选内容。这里逐项解释容易显得干巴,我先重点说几个容易忽略的语法细节。
第一个是UPDATE OF 列名列表。如果你写的是UPDATE OF credit,那么这个触发器只在UPDATE语句明确涉及到credit列时才会触发。如果业务侧执行的是UPDATE users SET name = 'x',credit列没有被牵涉,触发器就不触发。这个细粒度控制在审计场景中
