🛢️ 第15章 触发器
📄 一句话概括

SHOW TRIGGERS 在沙箱里报语法错,等价物是 sqlite_master。 sqlite_master 只有四列有用:type、name、tbl_name、sql,第四列存的是建表原文。 触发器名和索引名一样,是全库唯一的。

📝 本章和前面几章的关系

教材 15.2 两个小节:SHOW TRIGGERS 语句、查 triggers 表。 两条路在沙箱里都断了,但断的方式不一样,这个区别本身有用。 能查触发器有三层用处:确认它建成了、看清它到底干了什么、删之前知道有哪些。 第三层最实用,下一篇结尾的批量删就靠这个。 这一篇也顺手补第 11 章的一个对照:索引名全库唯一,触发器名也一样。

一、SHOW TRIGGERS 报的是语法错

教材的两种查法,在沙箱里一条都不通,但报错分两类。

教材里的查法 沙箱的反应
SHOW TRIGGERS near "SHOW": syntax error
SHOW TRIGGERS FROM 库名 near "SHOW": syntax error
SHOW CREATE TRIGGER 名 near "SHOW": syntax error
SELECT * FROM information_schema.TRIGGERS no such table: information_schema.TRIGGERS
SELECT * FROM mysql.triggers no such table: mysql.triggers

前三行是 syntax error,说明语法不存在——解析阶段就被挡了。 后两行是 no such table,说明语法有、数据源没有——解析通过了,找表失败。 这个区别在实际排查里很有用:看到 syntax error 就别再试同一条语句的变体了,换语言; 看到 no such table 才是「查的地方不对」。 顺带说清教材那句「查看 triggers 表中触发器信息」: 旧版 MySQL 在 mysql 库里有一张 triggers 表,后来统一到了 information_schema.TRIGGERS。 两个名字都能在书里见到,指的是同一件事——「有一张系统表专门记触发器」。

二、等价物:sqlite_master 这张表

SQLite 把「自己有哪些表、视图、索引、触发器」全记在一张叫 sqlite_master 的表里。 它相当于 MySQL 的 information_schema,但只有一张表、四列有用。

列 含义
type table/view/index/trigger
name 对象名,这里就是触发器名
tbl_name 挂在哪张表(或视图)上
sql 建这个对象的语句原文

查触发器就是 WHERE type = 'trigger'。 查出来几行,就说明库里挂着几个触发器。 第四列 sql 最值钱:它存的是你当初敲的建触发器语句原文,等于把定义完整存下来了。 想看某个触发器到底干了什么,读这一列就够,不用去翻代码仓库。

三、把定义完整读出来

定义可能很长,一整行打下来超过一百字,得折开才看得清。 折开不影响内容,sql 里存的仍然是一整行。

CREATE TRIGGER trg_goods_upd AFTER UPDATE OF price ON tb_g
FOR EACH ROW
WHEN NEW.price > OLD.price
BEGIN
    INSERT INTO tb_log(act, ref, note)
    VALUES ('涨价', NEW.id, printf('%g->%g', OLD.price, NEW.price));
    UPDATE tb_stat
       SET total = total + (NEW.price - OLD.price) * stock
     WHERE id = 1;
END;

读这段定义能看出四件事:时机 AFTER、事件 UPDATE OF price、 条件 WHEN NEW.price > OLD.price、动作是两条语句。 这一段其实就是「触发器的全部语法」: CREATE TRIGGER 名 时机 事件 ON 表 FOR EACH ROW [WHEN 条件] BEGIN 语句; 语句; END。 方括号里那个 WHEN 是可选的,其余都是必写项。

四、UPDATE OF 只认列名

UPDATE OF price 这个写法要特别注意:只有改到 price 这一列才触发。 改库存、改名字都不触发。

执行的语句 日志累计
UPDATE tb_g SET stock = 999 0 条
UPDATE tb_g SET price = 388 1 条

这个机制在「只关心某几列变化」时特别省:一张二十列的表,只在价格变动时才要跑一段逻辑, 不加这个限定就得每列都触发再自己判断。 MySQL 没有 UPDATE OF 列名。 它任何一列变了都会触发, 要限制只能靠 BEGIN ... END 里自己写 IF NEW.price <> OLD.price THEN ... END IF。 所以这段定义搬到 MySQL 上是要改写的,不能直接照抄。

五、触发器名全库唯一

第 11 章讲索引时验过一次:索引名全库唯一,不是表内唯一。 触发器也一样。在两张不同的表上建同名触发器,第二句直接被挡:trigger dup already exists。 MySQL 那边是「库内唯一」,跨库可以重名。 两边都要求同一个库里不能撞名,这一条是一致的。 所以起名最好带上表名当前缀:trg_goods_ins、trg_order_upd。 只叫 ins、upd 这种,第二张表就没法起名了,只能改成 ins2,越改越乱。

六、视图上的触发器,tbl_name 记的是视图

在视图上建三个 INSTEAD OF 触发器,查 sqlite_master,三行的 tbl_name 全是视图名。

name tbl_name
vi_insert v_g
vi_update v_g
vi_delete v_g

INSTEAD OF 是视图专用这件事,在元数据里也写得清清楚楚。 这张表还有个用法:按 tbl_name 筛一遍,就知道某张表上挂了哪些触发器。 删表之前、改表结构之前,先这么筛一次,比凭记忆可靠。 下一篇结尾的批量删,就是从这一步开始的。

七、动手 Demo:五种查法挨个试,再从 sqlite_master 读定义

这一段先按教材的写法挨个试:SHOW TRIGGERS、SHOW CREATE TRIGGER、 information_schema.TRIGGERS、mysql.triggers,把五种报错摆在一起对照; 再改用 sqlite_master 把定义完整读出来,最后验 UPDATE OF 和触发器名的唯一性。

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, ref INTEGER, note TEXT);
""")
c.execute(u"CREATE TRIGGER trg_goods_ins AFTER INSERT ON tb_g FOR EACH ROW "
          u"BEGIN INSERT INTO tb_log(act, ref) VALUES ('新增', NEW.id); END")

# ① 教材的查法,五种挨个试
for q in (u"SHOW TRIGGERS", u"SHOW TRIGGERS FROM db_database28",
          u"SHOW CREATE TRIGGER trg_goods_ins",
          u"SELECT * FROM information_schema.TRIGGERS",
          u"SELECT * FROM mysql.triggers"):
    try:
        c.execute(q).fetchall()
        print(q, u"-> 成功")
    except sqlite3.Error as e:
        print(q, u"->", e)

# ② 等价物:sqlite_master 里筛 type = 'trigger'
for row in c.execute(u"SELECT type, name, tbl_name "
                     u"FROM sqlite_master WHERE type = 'trigger'"):
    print(row)

# ③ 读定义:sql 列存的是原文
print(c.execute(u"SELECT sql FROM sqlite_master "
                u"WHERE name = 'trg_goods_ins'").fetchone()[0])

# ④ UPDATE OF 只认列名
c.execute(u"CREATE TRIGGER trg_price AFTER UPDATE OF price ON tb_g "
          u"FOR EACH ROW BEGIN INSERT INTO tb_log(act) VALUES ('碰了价格'); END")
c.execute(u"UPDATE tb_g SET stock = 999 WHERE id = 1")   # 不触发
c.execute(u"UPDATE tb_g SET price = 388 WHERE id = 1")   # 触发
print(c.execute(u"SELECT count(*) FROM tb_log").fetchone())

# ⑤ 触发器名全库唯一:换一张表再叫同名
c.execute(u"CREATE TABLE tb_h (id INTEGER PRIMARY KEY, v REAL)")
try:
    c.execute(u"CREATE TRIGGER trg_price AFTER INSERT ON tb_h "
              u"FOR EACH ROW BEGIN SELECT 1; END")
except sqlite3.Error as e:
    print(e)

实跑输出:

--------------------------------------------------------------------
查看触发器:SHOW 不认,得看 sqlite_master
--------------------------------------------------------------------


  SHOW TRIGGERS 在沙箱里报的是语法错
  --------------------------------------------------------------------
**教材 15.2 给了两种查法: `SHOW TRIGGERS`,和直接查系统表。**
**沙箱这两种都不通,报错的位置都在关键字上,说明它根本没这套语法。**

  教材里的查法                                 沙箱的反应
  ---------------------------------------------------------------------------------------
  `SHOW TRIGGERS`                              near "SHOW": syntax error
  `SHOW TRIGGERS FROM db_database28`           near "SHOW": syntax error
  `SHOW CREATE TRIGGER trg_goods_ins`          near "SHOW": syntax error
  `SELECT * FROM information_schema.TRIGGERS`  no such table: information_schema.TRIGGERS
  `SELECT * FROM mysql.triggers`               no such table: mysql.triggers

**注意报错分两类。**
**`SHOW` 开头的报 `near "SHOW": syntax error`,解析阶段就被挡了。**
**`information_schema` 那两条报 `no such table`,解析通过、找表失败。**
**这个区别有用:前者说明语法不存在,后者说明语法有、数据源没有。**

  等价物:sqlite_master 这张表
  --------------------------------------------------------------------
**SQLite 把「自己有哪些表、视图、索引、触发器」全记在一张叫 `sqlite_master` 的表里。**
**它相当于 MySQL 的 `information_schema`,但只有一张表、四个有用的列。**

  mysql> SELECT type, name, tbl_name FROM sqlite_master WHERE type = 'trigger'
  type    name          tbl_name
  ------------------------------
  trigger trg_goods_ins tb_g
  1 row in set

**`type` 是 `trigger`,`name` 是触发器名,`tbl_name` 是它挂在哪张表上。**
**第四列 `sql` 存的是你当初敲的建触发器语句原文——这一列最值钱,等于把定义完整存下来了。**

  把定义完整读出来
  --------------------------------------------------------------------
**定义可能很长,一整行打下来超过一百字,得折开才看得清。**

  mysql> 把 sql 这一列原样打出来(原文一整行,这里按空格折开看):
  | CREATE TRIGGER trg_goods_upd AFTER UPDATE OF price ON tb_g FOR EACH ROW WHEN
  | NEW.price > OLD.price BEGIN INSERT INTO tb_log(act, ref, note) VALUES ('涨价',
  | NEW.id, printf('%g->%g', OLD.price, NEW.price)); UPDATE tb_stat SET total = total +
  | (NEW.price - OLD.price) * stock WHERE id = 1; END

**读这段定义能看出四件事:**
**时机 `AFTER`、事件 `UPDATE OF price`、条件 `WHEN NEW.price > OLD.price`。**
**动作是两条语句,用分号隔开,一起包在 `BEGIN` 和 `END` 之间。**
**`UPDATE OF price` 要特别注意:只有改到 `price` 这一列才触发。**

  UPDATE OF 只认列名,改别的列不触发
  --------------------------------------------------------------------

  执行的语句                     日志累计
  ---------------------------------------
  `UPDATE tb_g SET stock = 999`  0 条
  `UPDATE tb_g SET price = 388`  1 条

**改库存没反应,改价格才记一条。**
**MySQL 没有 `UPDATE OF 列名` 这个写法,任何一列变了都会触发。**
**要限制只能靠 `BEGIN ... END` 里自己写 `IF`。**
**所以这段定义搬到 MySQL 上是要改写的,不能直接照抄。**

  触发器名是全库唯一的
  --------------------------------------------------------------------
**第 11 章讲索引的时候验过一次:索引名全库唯一,不是表内唯一。**
**触发器也一样。在两张不同的表上建同名的触发器,第二句直接被挡。**

  在 tb_h 上再建一个叫 dup 的:trigger dup already exists

**MySQL 的触发器名是「库内唯一」,跨库可以重名。**
**两边都要求同一个库里不能撞名,这一条是一致的,所以起名时最好带上表名当前缀。**

  视图上的触发器,tbl_name 记的是视图
  --------------------------------------------------------------------

  mysql> SELECT name, tbl_name FROM sqlite_master WHERE type = 'trigger'
  name      tbl_name
  ------------------
  vi_insert v_g
  vi_update v_g
  vi_delete v_g
  3 rows in set

**三个触发器全挂在 `v_g` 上,`INSTEAD OF` 是视图专用,元数据里写得很清楚。**
**这张表还有个用法:按 `tbl_name` 筛一遍,就知道某张表上挂了哪些触发器。**

第一节那张表一摆出来,syntax error 和 no such table 就分得很清楚了。 前三行是「这套语法压根没有」,后两行是「语法有、那张表不在」。 第三节打出来的定义只有一行,说明 sql 列存的是原文:你当初怎么敲的、空格怎么留的, 它就怎么存。 所以「从库里读到的定义」和「代码仓库里那份」可能不一致——两边改过一边没同步, 这种事在运维里很常见。 第五节那次同名失败,报的是 trigger dup already exists。 注意它没说「这张表上已经有了」,说的是「这个名字已经存在」——一句话点明了作用域是全库。

30 秒实验

  1. 在 sqlite_master 里 WHERE type IN ('table','view','index','trigger') 各查一次, 看这张表到底记了几类对象,type 有哪几个取值。
  2. 建一个 UPDATE 触发器(不加 OF),改 stock 和改 price 各一次,看触发几次; 再改成 UPDATE OF price,同样两次改动,看差在哪。
  3. 把 sqlite_master 里某个触发器的 sql 列整段复制出来,直接再执行一遍,看能不能重建成功、 报什么错——然后删掉原来的再试一次。

常见误区

  1. 把 SHOW TRIGGERS 的报错理解成「权限不够」或「版本太老」。 syntax error 说明这条语法在这套引擎里不存在,不是权限问题,换权限也解决不了。
  2. 以为触发器名是表内唯一,于是两张表都起了 trg_ins。第二张表直接建不上。 trigger 名 already exists 这句报错已经说明了作用域。
  3. 以为 sqlite_master.sql 里存的是规范化之后的标准写法。它存的是原文, 包括你当初的大小写和空白。要判断「定义到底长什么样」,得读它,而不是回忆自己当初怎么写的。
自测三问(点开对答案) 1. `syntax error` 和 `no such table` 这两个报错,能推出什么不同的结论? —— `syntax error` 说明这条语法在引擎里不存在(`SHOW` 全家都是),改参数没用,得换写法; `no such table` 说明语法对了、只是那张表不在(`information_schema` 就是这样), 该查的是「数据源」而不是「语法」。 2. 想删某张表上的全部触发器,第一步该做什么? —— 查 `sqlite_master`: `SELECT name FROM sqlite_master WHERE type = 'trigger' AND tbl_name = '表名'`。 拿到名单再逐个删,别凭记忆写死名字。 3. `UPDATE OF price` 这个写法,MySQL 上怎么写? —— MySQL 没有 `UPDATE OF`,只能写 `UPDATE`, 然后在触发器体里用 `IF NEW.price <> OLD.price THEN ... END IF` 自己判断。

下一篇讲怎么用触发器:先看清 BEFORE 和 AFTER 的先后、同类型多个谁先跑, 再拿它做拦截和冗余汇总这两件实事。

相关笔记