DROP TRIGGER 名 和 DROP TRIGGER IF EXISTS 名 是两种写法,后者能跳过不存在的名字。
删完必须回头验一次:删错名字不会报错,日志表却悄悄停了。
DROP TABLE 会把表上的触发器一起带走,一条都不留。
教材 15.4 只有一句话:DROP TRIGGER 加名字。这一篇补四件事:写法、验证、排查、连带。
这一篇是第 15 章的收尾,后面 15.5 小结和 15.6 实践与练习并进本篇末尾。
上一篇讲用,这一篇讲删。
触发器最大的麻烦是「代码里搜不到它」——所以删之前那一步查证,比删本身重要得多。
一、三种写法,两种通
| 写法 | 结果 |
|---|---|
DROP TRIGGER dz |
成功 |
DROP TRIGGER IF EXISTS dz2 |
成功 |
DROP TRIGGER IF EXISTS nope_zzz |
成功(跳过) |
DROP TRIGGER nope_zzz |
no such trigger: nope_zzz |
DROP TRIGGER dz3 ON tb_g |
near "ON": syntax error |
第四行不带 IF EXISTS 删一个不存在的名字,报 no such trigger——这是好事,
说明它没假装成功。
第五行是 MySQL 的写法:它的 DROP TRIGGER 语法里必须带 ON 表名,
用来定位这张表上的触发器。
SQLite 不需要这一步,所以多出来的 ON 表名 直接语法错。
IF EXISTS 不是可有可无的装饰。
部署脚本里删一个「可能已经删过」的触发器,不带它第二次执行就中断。
代价是它会把「名字拼错了」也一起吞掉。
所以删完必须回头查一遍,别只看有没有报错。
二、删完要验:动作真的停了
光看「删除成功」不够,得看它原来干的活是不是真的停了。
| 时机 | 商品表行数 | 日志表行数 |
|---|---|---|
| 删之前插一行 | 3 | 1 |
| 删之后插一行 | 4 | 1 |
表里的行照旧在涨,日志表停在原地——触发器确实不干活了。 这一步必须验。删错名字时一句报错都没有,日志表却悄悄停了,这种故障要等到对账才对得出来。 验的办法很简单:删之前插一行看日志涨不涨,删之后再插一行看它涨不涨。 两次都插、两次都查,两分钟的事,能省掉后面几天的排查。
三、删之前先查:这张表上挂了几个
sqlite_master 的 tbl_name 列就是干这个用的。
想清一张表上的所有触发器,先按 tbl_name 筛一遍。
SELECT name, tbl_name
FROM sqlite_master
WHERE type = 'trigger';
-- 只看某一张表上的
SELECT name
FROM sqlite_master
WHERE type = 'trigger'
AND tbl_name = 'tb_g';
这一步的意义不只是「拿到名单」,更是「知道自己漏了什么」。 人工删的时候最常见的失误不是删错,是漏删:以为只有两个,实际挂了四个,剩下那两个继续跑, 出问题时谁都想不到它们还在。
四、批量删:拿到名单再动手
做法是先把名字查出来,再一个个删——不靠记忆、不写死名单。
故意把 t_nope 混在名单末尾:前三个删掉,最后一个报 no such trigger 被接住。
| 名字 | 结果 |
|---|---|
t_del |
已删除 |
t_ins |
已删除 |
t_upd |
已删除 |
t_nope |
no such trigger: t_nope |
这就是「跳过不存在的、删掉该删的」,脚本可以反复跑而不中断。
删完再看一眼:tb_g 上一个不剩,tb_h 上那个没被碰。
注意 tb_h 那个没被碰——这正是按 tbl_name 筛的价值。
如果图省事写成「删掉所有触发器」,另一张表上的守卫就一起没了,而且它也是静默消失的。
五、删表会连触发器一起带走
触发器是挂在表上的,表没了它没有立足之地,数据库会顺手清掉。
| 时机 | 触发器数 |
|---|---|
| 删表之前 | 2 |
| 删表之后 | 0 |
这两条触发器谁都没显式删过,跟着表一起消失了。 知道这一点有两面:手工删表时不用担心留下孤儿触发器; 但反过来也危险——删表是个「连带动作很大」的操作,比看上去重。 表上挂的触发器越多,删表的连带影响越大。 所以删表之前那一步「查有哪些触发器」,其实是在估这次操作的爆炸半径。
六、本章小结与实践与练习
第 15 章四篇,真正要带走的是五条:
一、触发器由时机、事件、表三样定位,六种组合;要「拦」用 BEFORE,要「记」用 AFTER。
二、OLD 和 NEW 不是什么时候都有,SQLite 建的时候不查、用的时候才炸。
三、INSTEAD OF 只给视图,MySQL 没有这个时机;UPDATE OF 列名 也是 SQLite 独有。
四、多个触发器的执行顺序两边相反(SQLite 后建先跑,MySQL 先建先跑),别靠它保证结果。
五、冗余汇总必须「先对齐基线、之后全走触发器、再配一个对账兜底」,三步缺一不可。
这一章最该记住的一句是:触发器是「看不见的代码」。
过程要有人 CALL,视图要有人 SELECT,触发器什么都不用——写表的时候它自己就动了。
好处是不会漏,坏处是排查时想不起来它存在。
所以建它的那天,就要把「它叫什么、挂在哪、干什么」写进文档;
删它的那天,要按本节这套流程查一遍、验一遍。
15.6 的练习可以挑这三道:
给商品表加一个「价格不许往下调」的 BEFORE UPDATE 守卫,用 WHEN 限定条件;
用一个 AFTER INSERT 触发器维护「每个分类的商品数」汇总表,装完记得做一次全量重算;
把一张表上的全部触发器导出成一段 CREATE TRIGGER 脚本,再在空库里回放一遍。
三道都动手做过,这一章就算真读完了。
七、动手 Demo:删之前、删之后、批量删、删表
这一段把删除的四件事跑一遍:三种 DROP 写法各试一次(含 MySQL 的 ON 表名);
删之前插一行、删之后再插一行,看日志是不是真的停了;
按 tbl_name 筛出名单再批量删,末尾故意混一个不存在的名字;最后看 DROP TABLE 的连带效果。
import sqlite3
c = sqlite3.connect(u":memory:", isolation_level=None)
c.executescript(u"""
CREATE TABLE tb_g (id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT, price REAL, stock INTEGER);
CREATE TABLE tb_log (id INTEGER PRIMARY KEY AUTOINCREMENT, act TEXT);
INSERT INTO tb_g(name, price, stock) VALUES ('机械键盘', 399.0, 12);
INSERT INTO tb_g(name, price, stock) VALUES ('无线鼠标', 129.0, 30);
""")
# ① 三种写法
c.execute(u"CREATE TRIGGER dz AFTER INSERT ON tb_g "
u"FOR EACH ROW BEGIN SELECT 1; END")
c.execute(u"DROP TRIGGER dz") # 通
c.execute(u"DROP TRIGGER IF EXISTS nope_zzz") # 通,跳过
try:
c.execute(u"DROP TRIGGER nope_zzz") # 报 no such trigger
except sqlite3.Error as e:
print(e)
try:
c.execute(u"DROP TRIGGER dz2 ON tb_g") # MySQL 写法,语法错
except sqlite3.Error as e:
print(e)
# ② 删完要验:动作是不是真停了
c.execute(u"CREATE TRIGGER trg_ins AFTER INSERT ON tb_g "
u"FOR EACH ROW BEGIN INSERT INTO tb_log(act) VALUES ('ins'); END")
c.execute(u"INSERT INTO tb_g(name, price, stock) VALUES ('丙', 30.0, 1)")
print(u"删之前日志", c.execute(u"SELECT count(*) FROM tb_log").fetchone()[0])
c.execute(u"DROP TRIGGER trg_ins")
c.execute(u"INSERT INTO tb_g(name, price, stock) VALUES ('丁', 40.0, 1)")
print(u"删之后日志", c.execute(u"SELECT count(*) FROM tb_log").fetchone()[0])
# ③ 按 tbl_name 筛名单,批量删
for nm, ev in ((u"t_ins", u"INSERT"), (u"t_upd", u"UPDATE"), (u"t_del", u"DELETE")):
c.execute(u"CREATE TRIGGER %s AFTER %s ON tb_g "
u"FOR EACH ROW BEGIN SELECT 1; END" % (nm, ev))
c.execute(u"CREATE TABLE tb_h (id INTEGER PRIMARY KEY, v REAL)")
c.execute(u"CREATE TRIGGER h_ins AFTER INSERT ON tb_h "
u"FOR EACH ROW BEGIN SELECT 1; END")
print(c.execute(u"SELECT name FROM sqlite_master WHERE type = 'trigger' "
u"AND tbl_name = 'tb_g' ORDER BY name").fetchall())
for nm in [u"t_del", u"t_ins", u"t_upd", u"t_nope"]:
try:
c.execute(u"DROP TRIGGER " + nm)
print(nm, u"已删除")
except sqlite3.Error as e:
print(nm, e)
# ④ 删表连带
c.execute(u"CREATE TRIGGER g1 AFTER INSERT ON tb_g FOR EACH ROW BEGIN SELECT 1; END")
print(u"删表前", c.execute(u"SELECT count(*) FROM sqlite_master "
u"WHERE type = 'trigger'").fetchone()[0])
c.execute(u"DROP TABLE tb_g")
print(u"删表后", c.execute(u"SELECT count(*) FROM sqlite_master "
u"WHERE type = 'trigger'").fetchone()[0])
实跑输出:
--------------------------------------------------------------------
删除触发器:删完记得回头看一眼
--------------------------------------------------------------------
三种写法,两种通
--------------------------------------------------------------------
写法 结果
------------------------------------------------------------
`DROP TRIGGER dz` 成功
`DROP TRIGGER IF EXISTS dz2` 成功
`DROP TRIGGER IF EXISTS nope_zzz` 成功
`DROP TRIGGER nope_zzz` no such trigger: nope_zzz
`DROP TRIGGER dz3 ON tb_g` near "ON": syntax error
**前三行通,后两行不通。**
**第四行不带 `IF EXISTS` 删一个不存在的名字,报 `no such trigger`——这是好事,说明它没假装成功。**
**第五行是 MySQL 的写法,`ON 表名` 那段在 SQLite 里是多余的,报 `near "ON": syntax error`。**
**`IF EXISTS` 不是可有可无的装饰。**
**部署脚本里删一个「可能已经删过」的触发器,不带它第二次执行就中断。**
**代价是它会把「名字拼错了」也一起吞掉。**
**所以删完必须回头查一遍,别只看有没有报错。**
删掉之后,动作真的停了
--------------------------------------------------------------------
**光看「删除成功」不够,得看它原来干的活是不是真的停了。**
时机 tb_g 行数 tb_log 行数
------------------------------------
删之前插一行 3 1
删之后插一行 4 1
**表里的行照旧在涨,日志表停在原地——触发器确实不干活了。**
**这一步必须验:删错名字的时候一句报错都没有,日志表却悄悄停了,这种故障要等到对账才对得出来。**
删之前先查:这张表上挂了几个
--------------------------------------------------------------------
**`sqlite_master` 的 `tbl_name` 列就是干这个用的。想清一张表上的所有触发器,先筛一遍。**
mysql> SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'
name tbl_name
--------------
t_ins tb_g
t_upd tb_g
t_del tb_g
h_ins tb_h
4 rows in set
mysql> 只筛 tb_g 上的:
mysql> SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = 'tb_g'
name
-----
t_ins
t_upd
t_del
3 rows in set
批量删:拿到名单再动手
--------------------------------------------------------------------
**实际运维里经常要「把这张表上的触发器全清掉」。**
**做法是先把名字查出来,再一个个删——不靠记忆、不写死名单。**
查到 3 个:t_del、t_ins、t_upd
名字 结果
-------------------------------
t_del 已删除
t_ins 已删除
t_upd 已删除
t_nope no such trigger: t_nope
**故意把 `t_nope` 混在名单末尾:前三个删掉,最后一个报 `no such trigger` 被接住。**
**这就是「跳过不存在的、删掉该删的」,脚本可以反复跑而不中断。**
**再看剩下的:`tb_g` 上一个不剩,`tb_h` 上那个没被碰。**
mysql> SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'
name tbl_name
--------------
h_ins tb_h
1 row in set
删表会连触发器一起带走
--------------------------------------------------------------------
**触发器是挂在表上的,表没了它没有立足之地,数据库会顺手清掉。**
时机 触发器数
------------------
删表之前 2
删表之后 0
**这两条触发器谁都没显式删过,跟着表一起消失了。**
**知道这一点有用:手工删表的时候不用担心留下孤儿触发器。**
**反过来也危险——删表是个「连带动作很大」的操作,比看上去重。**
**教材 15.4 这一节就三句话:`DROP TRIGGER` 加名字。**
**真正要在工程里用起来的,是本节这四件事:写法分两种、删完要验、删前先查、删表会连带。**
第一节五行报错里,no such trigger 和 near "ON": syntax error 是两回事。
前者是「语法对、名字不存在」,后者是「这条语法在这儿不成立」。
IF EXISTS 只吞前者,吞不掉后者。
第二节两行数字:删之前 1 条,删之后还是 1 条。
商品表从 3 行涨到 4 行,日志表一动不动——这才是「删掉了」的证据。
只看 DROP 成功,看不到这一层。
第三节最后那个 t_nope 报错被接住,脚本继续往下走,这就是「可反复执行的脚本」该有的样子。
而且注意 tb_h 上那个 h_ins 全程没被碰——按 tbl_name 筛选保住了它。
第四节两行 2 → 0:两条触发器谁都没显式删,跟着表一起没了。
删表之前数一下有几个触发器,等于在估这次操作会带走多少东西。
30 秒实验
- 把
DROP TRIGGER IF EXISTS换成DROP TRIGGER,再删一个不存在的名字, 看脚本是不是真的中断——把后面的语句接在同一段脚本里就能看出来。 - 写一个循环:从
sqlite_master读出所有触发器的sql,逐条打印。 再把这些sql在空库里执行一遍,看能不能完整还原——还原不了的,就是漏了依赖的表。 - 建一张表挂三个触发器,然后
DROP TABLE,比较删表前后的sqlite_master行数, 顺便确认剩下的对象(表、视图、索引)有没有被连带。
常见误区
- 以为
DROP TRIGGER成功就等于「不会再触发了」。删错名字时一句报错都没有。 正确做法是删完再插一行,看该记的日志有没有记。 - 图省事写成「删掉库里所有触发器」。别的表上那些守卫会一起消失,而且同样静默。
要按
tbl_name筛,只动目标表。 - 以为删表会留下孤儿触发器。反过来,删表会连触发器一起清掉。 真正该注意的是「删表带走了多少东西」这件事本身。
自测三问(点开对答案)
1. `DROP TRIGGER IF EXISTS` 有什么风险,怎么补? —— 风险是它会把「名字拼错了」也静默吞掉,看起来成功、实际什么都没删。 补法是删完回头查一次 `sqlite_master`,并插一行数据验证触发的动作确实停了。 2. 为什么批量删之前要先按 `tbl_name` 查一遍? —— 一是不靠记忆拿名单,二是避免误删别的表上的触发器。 「删掉所有触发器」和「删掉这张表上的触发器」听起来差不多,影响面差很多。 3. 删掉一张表,表上的触发器还在吗? —— 不在。触发器挂在表上,表没了它就没有立足之处,会跟着一起被清掉。 所以删表之前数一下有几个触发器,是在估这次操作的连带影响。第 15 章到这里结束。 下一篇进入第 16 章:事务——从「自动提交」讲到「回滚点」,再到锁机制和隔离级别。
- ☐ (本篇完,下一篇 16.1 事务机制)