🪟 第12章 触发器
📄 一句话概括

查触发器: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 秒实验

  1. 把 tb_log 上的 INSERT 删掉,只留一个触发器,对比一次。 触发器体里少一句写操作,开销会明显下来——这就是「别写重活」。
  2. 把批量插入的 5000 行改成 50000 行,两个数的倍数会变大还是变小? 先猜再跑。行级触发器的倍数基本稳定,语句级的会随行数增加而变大。
  3. 想一想:如果忘了 ENABLE 就交付了,会出什么问题? 触发器在,但不起作用——业务看起来正常,日志却一直不涨。 这种「静默失效」比报错难发现得多。所以禁用一定要配套「导完立刻启用」的检查。

常见误区

  1. 「删触发器就等于撤销它做过的事」。它做过的事已经落库了。 删触发器只是「以后不再自动跑」。
  2. 「禁用触发器不影响业务」。日志、级联、校验全停了。 数据看起来正常,痕迹和一致性没了——这种问题几周后才被发现。
  3. 「触发器很轻,随手加」。实测批量插入慢 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 的「集合处理」正好相反——多数时候该避免, 但少数场景非它不可。

相关笔记