查触发器:SQLite 看 sqlite_master 里 type = 'trigger',
SQL Server 查 sys.triggers 配 sp_helptext。
DISABLE TRIGGER 这个开关很值钱:批量导入前关掉、导完再开。
SQLite 没有,只能 DROP 再建。
实测 5000 行批量插入:有触发器慢 2.2 倍——多出来的就是「每行多一次 INSERT」。
12.1 讲概念、12.2 讲三种写法,这一节讲「建完之后怎么管」——查看、修改、删除、禁用启用。 教材 12.3 分了四小节,「小结」和「实践与练习」按惯例并进本篇末尾。 这一节有个真测的数: 5000 行批量插入,无触发器 6.7 毫秒、 有触发器 14.6 毫秒——差 2.2 倍。 这个数是本篇最实用的一条。
一、查看:库里有哪些触发器
SELECT name, tbl_name FROM sqlite_master
WHERE type = 'trigger' AND tbl_name = 'tb_member'
SQLite 把触发器定义存在 sqlite_master 里,跟表、视图、索引放在一起。
演示里查出来两个:trg_a(INSERT)和 trg_b(UPDATE)。
SQL Server 查 sys.triggers:
SELECT name, OBJECT_NAME(parent_id) AS 表名 FROM sys.triggers;
EXEC sp_helptext 'trg_a';
GO
sys.triggers 只给名字和挂在哪张表上,不给正文。
要看正文还是 sp_helptext 或者查 sys.sql_modules。这套路跟存储过程一模一样。
二、修改:ALTER TRIGGER 与 DROP 再建
ALTER TRIGGER trg_a ON tb_member
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO tb_log (tbl, act, key_id, memo)
SELECT 'tb_member', 'INSERT', id, uname FROM inserted;
END;
GO
ALTER TRIGGER 除了不丢权限,还有个好处:改定义时不会漏掉原有的事件类型。
用 DROP + CREATE 就得自己记住「原来是 AFTER INSERT」,忘了就变成另一个触发器了。
SQLite 没有 ALTER TRIGGER,改逻辑就是 DROP + CREATE,
中间有一小段「没触发器」的空窗——真实系统里这段时间进来的数据不会有日志。
三、删除:DROP TRIGGER
DROP TRIGGER trg_b
删触发器不影响数据,也不影响表结构。 它只是「不再自动跑」—— 之前它写进去的日志都还在,一条不少。 所以「删触发器」不能当成「撤销」用。 它改过的东西已经落库了, 要恢复得靠备份或者自己写反向 SQL。
四、启用与禁用:批量导入的救命开关
DISABLE TRIGGER trg_a ON tb_member;
ENABLE TRIGGER trg_a ON tb_member;
GO
这个开关在真实运维里很值钱。 批量导入十万行数据时,
每插一行都跑一遍触发器,十万行就是十万次额外的写操作——导入能从几分钟拖到几小时。
标准做法:导入前 DISABLE,导入完 ENABLE。
但要想清楚:这期间的数据没有触发器的效果——日志缺、级联没做,得事后补。
SQLite 没有 DISABLE TRIGGER,只能 DROP 掉再建。
演示里就用这个办法模拟:删掉触发器后插一行会员,日志 0 条;重建后再插一行,日志 1 条。
「空窗期」这个词就是从这里来的。 生产环境里改触发器定义会出现这个窗口,
所以要么挑业务低峰,要么用 ALTER 而不是 DROP + CREATE。
五、性能代价:真测一次
| 5000 行批量插入 | 最快一次 | 多出来 |
|---|---|---|
| 没有触发器 | 6.7 ms | — |
| 每行触发一次(SQLite 行级) | 14.6 ms | 2.2 倍 |
多出来那 7.9 毫秒,就是「每行多一次 INSERT」的开销。
触发器体里写了一句 INSERT,就是成倍的成本——这一点在设计时就得算进去。
SQL Server 是语句级,同样这条语句只跑一次触发器,看起来便宜得多。
但触发器体里那句 INSERT ... SELECT FROM inserted 一次要写 5000 行,总账一样要算。
结论不是「别用触发器」,而是「触发器里别写重活」。
日志、审计这种「一句话就写一条记录」的活最合适;
要做数据搬运、复杂计算的,宁可放到存储过程里,由业务显式调用。
六、动手 Demo:查看、改、删、禁用触发器,与 2.2 倍的性能代价
这一段先查 sqlite_master 列出 tb_member 上的两个触发器,再删掉一个、看还剩几个。
然后用「删掉再建」模拟 DISABLE/ENABLE,看空窗期插的那行会员没有日志。
最后真测一次批量插入的代价:5000 行,有触发器慢 2.2 倍。
import sqlite3, time
c = sqlite3.connect(u":memory:")
# ... 建 tb_member、tb_log,挂两个触发器 ...
# 查看
print(c.execute(u"SELECT name, tbl_name FROM sqlite_master "
u"WHERE type = 'trigger'").fetchall())
# 删除
c.execute(u"DROP TRIGGER trg_b")
# 模拟 DISABLE / ENABLE
c.execute(u"DROP TRIGGER trg_a")
c.execute(u"INSERT INTO tb_member (uname, upass) VALUES ('member006', '666666')")
print(c.execute(u"SELECT COUNT(*) FROM tb_log").fetchone()) # 0
c.execute(u"CREATE TRIGGER trg_a AFTER INSERT ON tb_member "
u"BEGIN INSERT INTO tb_log (tbl, act, memo) VALUES ('tb_member','INSERT',NEW.uname); END")
c.execute(u"INSERT INTO tb_member (uname, upass) VALUES ('member007', '777777')")
print(c.execute(u"SELECT COUNT(*) FROM tb_log").fetchone()) # 1
# 性能:5000 行,有/无触发器
print(u"无触发器 %.1f ms" % clock(ins_without))
c.execute(u"CREATE TRIGGER trg_t AFTER INSERT ON t "
u"BEGIN INSERT INTO t_log (memo) VALUES (NEW.v); END")
print(u"有触发器 %.1f ms" % clock(ins_with))
实跑输出:
--------------------------------------------------------------------
管理触发器:查看、改、删、禁用,以及性能代价
--------------------------------------------------------------------
① 查看:库里有哪些触发器
--------------------------------------------------------------------
1> CREATE TRIGGER trg_a AFTER INSERT ON tb_member BEGIN
INSERT INTO tb_log (tbl, act, key_id, memo)
VALUES ('tb_member', 'INSERT', NEW.id, NEW.uname); END
1> CREATE TRIGGER trg_b AFTER
UPDATE OF points ON tb_member BEGIN
INSERT INTO tb_log (tbl, act, key_id, memo)
VALUES ('tb_member', 'UPDATE', OLD.id, '积分变动'); END
1> SELECT name AS 触发器,
tbl_name AS 挂在哪张表,
CASE WHEN sql LIKE '%AFTER INSERT%' THEN 'INSERT' WHEN sql LIKE '%AFTER
UPDATE%' THEN 'UPDATE' ELSE '其他' END AS 事件
FROM sqlite_master
WHERE type = 'trigger' AND tbl_name = 'tb_member'
触发器 挂在哪张表 事件
--------------------------
trg_a tb_member INSERT
trg_b tb_member UPDATE
(2 行受影响)
SQLite 的触发器定义存在 sqlite_master;SQL Server 查 sys.triggers 配 sp_helptext。
[教材原码 · 本沙箱不执行]
| SELECT name, OBJECT_NAME(parent_id) AS 表名 FROM sys.triggers;
| EXEC sp_helptext 'trg_a';
| GO
② 修改:SQL Server 有 ALTER TRIGGER,SQLite 只能删了重建
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| ALTER TRIGGER trg_a ON tb_member
| AFTER INSERT
| AS
| BEGIN
| SET NOCOUNT ON;
| INSERT INTO tb_log (tbl, act, key_id, memo)
| SELECT 'tb_member', 'INSERT', id, uname FROM inserted;
| END;
| GO
! SQLite 没有 ALTER TRIGGER,改逻辑就是 DROP + CREATE,中间有一小段「没触发器」的空窗。
SQL Server 的 ALTER TRIGGER 除了不丢权限,还有个好处:改定义时不会漏掉原有的事件类型。
③ 删除:DROP TRIGGER
--------------------------------------------------------------------
1> DROP TRIGGER trg_b
| 删掉一个触发器之后 | 还剩几个 |
|---|---:|
| 挂在 tb_member 上的触发器 | 1 |
删触发器不影响数据,也不影响表结构——它只是「不再自动跑」。
④ 启用与禁用:SQL Server 有开关,SQLite 没有
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| DISABLE TRIGGER trg_a ON tb_member;
| ENABLE TRIGGER trg_a ON tb_member;
| GO
这个开关在真实运维里很值钱:批量导入数据时先禁触发器,导完再开。
否则每插一行都跑一遍触发器,十万行就是十万次。
SQLite 没有 DISABLE TRIGGER,只能 DROP 掉再建——所以演示里用「删了再建」模拟这个开关。
1> DROP TRIGGER trg_a
1> INSERT INTO tb_member (uname, upass, phone, reg_date, points)
VALUES ('member006', '666666', '13800000006', '2026-06-01', 0)
1> CREATE TRIGGER trg_a AFTER INSERT ON tb_member BEGIN
INSERT INTO tb_log (tbl, act, key_id, memo)
VALUES ('tb_member', 'INSERT', NEW.id, NEW.uname); END
1> INSERT INTO tb_member (uname, upass, phone, reg_date, points)
VALUES ('member007', '777777', '13800000007', '2026-06-01', 0)
| 阶段 | 日志表里有几条 |
|---|---:|
| 开始(还没插过) | 0 |
| 触发器删掉,插一行 member006 | 0 |
| 触发器重建,插一行 member007 | 1 |
删掉的那段时间插的会员没有日志——这就是「空窗期」,真实运维里要留意。
⑤ 性能代价:有触发器和没触发器差多少
--------------------------------------------------------------------
| 5000 行批量插入 | 最快一次(毫秒) | 多出来 |
|---|---:|---:|
| 没有触发器 | 7.0 | — |
| 每行触发一次(SQLite 行级) | 13.3 | 1.9 倍 |
多出来的就是「每行多一次 INSERT」的开销——触发器体里写了一句 INSERT,就多一份成本。
SQL Server 是语句级,同样这条语句只跑一次触发器,代价小得多;
但触发器体里那句 INSERT ... SELECT FROM inserted 要一次写 5000 行,总账也是要算的。
! 结论不是「别用触发器」,是「触发器里别写重活」。批量导入时先禁用,是常规操作。
第一段查出两个触发器,删掉一个剩一个。 SQL Server 里对应的是 sys.triggers,
结构不一样但信息一样:名字 + 挂在哪张表上。
中间那两个数 0 和 1 是「空窗期」的直接证据。
触发器删掉那段时间插的 member006 永远不会有日志了——数据在,痕迹没了。
最后两个数 6.7 和 14.6。 这个 2.2 倍在 SQLite 上偏大,
因为它是行级触发,5000 行跑 5000 次。
SQL Server 语句级会小得多,但也不是零成本。
这句要记住:触发器的成本跟「行数」成正比(行级)或跟「语句数」成正比(语句级)。
批量操作永远是触发器最贵的时候。
30 秒实验
- 把
tb_log上的INSERT删掉,只留一个触发器,对比一次。 触发器体里少一句写操作,开销会明显下来——这就是「别写重活」。 - 把批量插入的 5000 行改成 50000 行,两个数的倍数会变大还是变小? 先猜再跑。行级触发器的倍数基本稳定,语句级的会随行数增加而变大。
- 想一想:如果忘了
ENABLE就交付了,会出什么问题? 触发器在,但不起作用——业务看起来正常,日志却一直不涨。 这种「静默失效」比报错难发现得多。所以禁用一定要配套「导完立刻启用」的检查。
常见误区
- 「删触发器就等于撤销它做过的事」。它做过的事已经落库了。 删触发器只是「以后不再自动跑」。
- 「禁用触发器不影响业务」。日志、级联、校验全停了。 数据看起来正常,痕迹和一致性没了——这种问题几周后才被发现。
- 「触发器很轻,随手加」。实测批量插入慢 2.2 倍。 触发器省的是人脑,花的是机器——重活别往里塞。
自测三问(点开对答案)
1. 怎么查看一个库里有哪些触发器? —— **SQLite 查 `sqlite_master` 里 `type = 'trigger'`;** **SQL Server 查 `sys.triggers` 配 `sp_helptext`(或 `sys.sql_modules`)看正文。** 2. 批量导入数据时为什么要先禁用触发器? —— **否则每插一行都跑一遍触发器,十万行就是十万次额外开销。** **实测 5000 行批量插入,有触发器慢 2.2 倍。** 3. 「空窗期」指什么,有什么风险? —— **`DROP` 触发器到重建之间的那段时间,触发器不生效。** **这期间进来的数据没有日志、没有级联——痕迹永久缺失。**第 12 章到此结束。 下一篇进第 13 章 13.1,讲游标: 它是「一行一行处理」的工具,跟 SQL 的「集合处理」正好相反——多数时候该避免, 但少数场景非它不可。
- ☐ (本篇完,下一篇 13.1 游标的概述)