梁叔经营着一家大型物流仓库。每天有无数包裹从传送带进进出出,他给仓库装了两套自动化设备:第一套是安置在通道口的自动感应器,只要包裹经过,机器就自动扫描、贴标签、记台账;第二套是主管手里的“订单改写规则”,例如凡是从 A 区发出的包裹,都自动把“陆运”改成“空运”。在 PostgreSQL 里,前一套设备叫触发器,后一套叫规则系统。触发器像传感器一样逐包响应,规则系统则在 SQL“订单”真正执行前就把它改写好。理解这两套机制,是掌握 PostgreSQL 高级特性的关键一步,本文会用梁叔仓库的故事,把它们讲明白。

岗位关联

对 AI 从业者来说,掌握触发器与规则系统,意味着你能把数据一致性、审计、同步等逻辑从应用层下沉到数据库层,写出更可靠、更自动化的数据架构。

目录
  1. 可视化演示
  2. 触发器基础:门口自动感应器
  3. 触发时机:出发前检查还是到站后记录
  4. 触发粒度:逐个扫码还是一单扫码
  5. 规则系统:会改写订单的主管
  6. 触发器VS规则:各就各位
  7. 性能调优:让传送带不拥堵
  8. 事务约束:同一本账本
  9. 应用模式:从审计台账到数据同步
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

本节配有交互动效(进入视口后自动加载,约 3KB)。

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

触发器基础:门口自动感应器

梁叔的仓库入口新装了一台自动感应器,只要包裹经过,扫码枪就会自动记录并为包裹贴上一张“易碎品”标签。它不需要仓库员每次手动操作,也不会漏掉任何一个包裹。这个设备的角色,就相当于 PostgreSQL 里的触发器:一种绑定在表上的数据库对象,当 INSERT、UPDATE、DELETE 发生时,数据库自动调用预先写好的函数。

在 PostgreSQL 中,触发器由“事件”和“函数”两部分组成。事件告诉数据库什么时候触发,函数告诉数据库触发后做什么。例如:

这段 SQL 的意思是:每当有包裹要插入 packages 表时,在真正写入之前,逐行执行 add_label() 函数。函数里可以访问 NEW 代表新包裹,也可以访问 OLD 代表被替换的旧包裹,从而实现“放进仓库前先贴标签”的效果。

不过设备不会凭空工作,每次触发都要付出成本。可以用下面的公式估计一个触发器的开销:

梁叔清楚,每多一道自动感应门就多一分等待;一旦包裹批量进仓,感应器的累计开销就不能忽视。所以,理解触发器基础之后,下一步就要看如何选择“什么时候感应、怎样感应”。

Ctrigger=Cparse+Nrow×(Ccheck+Cfn)\displaystyle C_{\text{trigger}} = C_{\text{parse}} + N_{\text{row}} \times \left( C_{\text{check}} + C_{\text{fn}} \right)
一条 SQL 的触发器总开销,等于固定解析成本,加上每一行数据的检查成本和函数执行成本。
  1. 固定解析成本C_{\text{parse}} 代表 PostgreSQL 在语句进入时解析触发器定义、绑定关系的固定开销,与行数无关。
    CparseC_{\text{parse}}
  2. 受影响行数N_{\text{row}} 是本次 INSERT、UPDATE、DELETE 涉及的数据行数,行数越多,总开销越大。
    NrowN_{\text{row}}
  3. 行级检查成本C_{\text{check}} 表示数据库判断“这一行是否需要执行触发器函数”的开销。
    CcheckC_{\text{check}}
  4. 函数执行成本C_{\text{fn}} 表示真正执行用户触发器函数的成本,比如计算、写入日志等。
    CfnC_{\text{fn}}
sql4 行
CREATE TRIGGER before_insert_pkgBEFORE INSERT ON packagesFOR EACH ROWEXECUTE FUNCTION add_label();

触发时机:出发前检查还是到站后记录

梁叔给仓库定了两条规矩:包裹出发前,必须检查重量并改贴正确标签;包裹到达货架后,再由扫码枪记录到货时间。前一种“出发前改数据”对应 PostgreSQL 的 BEFORE 触发器,后一种“到站后记录”对应 AFTER 触发器。

BEFORE 触发器最大的特点,是可以在行真正写入前修改 NEW 的内容。比如检查到重量超标的包裹时,直接改写它的物流等级。AFTER 触发器则在行写入或更新完成之后执行,适合做审计、通知、汇总,但它不能再回头修改已经写入的数据。此外还有 INSTEAD OF 触发器,专门用在视图上,代替视图上原本无法执行的写入操作,就像客户下单要求“把包裹送到月球”,仓管员就用一套替代流程处理。

触发时机的选择可以用下面的逻辑表示:

梁叔的比喻依然成立:BEFORE 是在包裹出发前动手改货,AFTER 是在包裹入库后记账。选对时机,既能保证数据正确,又能避免无意义的重复劳动。

rfinal={f(NEW),BEFORENEW,AFTER\displaystyle r_{\text{final}} = \begin{cases} f(NEW), & \text{BEFORE} \\ NEW, & \text{AFTER} \end{cases}
对于 BEFORE 触发器,最终写入的是函数加工后的值;对于 AFTER 触发器,函数不再修改最终写入值。
  1. BEFORE 分支当触发器声明为 BEFORE 时,数据库先调用函数 f(NEW),再用函数返回的行作为最终写入数据。
    f(NEW)f(NEW)
  2. AFTER 分支当触发器声明为 AFTER 时,写入动作已经完成,NEW 不再被触发器修改,最终值就是原始 NEW。
    NEWNEW
  3. 选择条件PostgreSQL 根据事件时机选择对应分支,决定触发函数是在写前参与改造,还是在写后做旁路记录。
    BEFORE→f(NEW),AFTER→NEW\text{BEFORE} \rightarrow f(NEW), \quad \text{AFTER} \rightarrow NEW

触发粒度:逐个扫码还是一单扫码

梁叔发现,感应器有两种工作模式:一种是每一个包裹经过都单独扫码,另一种是一整批包裹凭一张出库单统一记录。前者对应 行级触发器,后者对应 语句级触发器。

行级触发器对每一条受影响的数据行执行一次函数,因此可以拿到每一行的 NEW 和 OLD,适合逐行校验和逐行审计。语句级触发器只在整条 SQL 语句执行完毕后触发一次,拿不到逐行的 NEW/OLD,但处理大批量数据时开销低得多。换句话说,同样是一千个包裹,行级感应需要扫一千次码,语句级感应只需要刷一次出库单。

两种粒度的成本关系可以写成:

如果梁叔每次都让感应器逐包扫码,传送带迟早会堵住;只有偶尔需要精确处理每一件包裹时,行级触发器才值得使用。理解了粒度,就能为触发器选择合适的“感应精度”。

Crow=n×crow,Cstmt=cstmt\displaystyle C_{\text{row}} = n \times c_{\text{row}}, \qquad C_{\text{stmt}} = c_{\text{stmt}}
行级触发器的成本随行数线性增长,语句级触发器固定触发一次,所以批量场景下语句级通常更划算。
  1. 行级总成本n 是处理行数,c_{\text{row}} 是每行的触发器执行成本,两者相乘得到行级触发器的总开销。
    Crow=n×crowC_{\text{row}} = n \times c_{\text{row}}
  2. 语句级总成本c_{\text{stmt}} 是一条 SQL 只触发一次的固定成本,与影响行数无关。
    Cstmt=cstmtC_{\text{stmt}} = c_{\text{stmt}}
  3. 对比选择当 n 很大时,n \times c_{\text{row}} 会远大于 c_{\text{stmt}},所以大批量导入、更新通常优先考虑语句级触发器。
    n×crow≫cstmtn \times c_{\text{row}} \gg c_{\text{stmt}}

规则系统:会改写订单的主管

梁叔的仓库里还有一位主管,手里常年拿着一支红笔。只要看到订单上写着“空运”,他就自动改成“陆运”;看到目的地在冷库区,就顺手加上“需要冷藏”的备注。这个过程发生在包裹发出之前,订单本身被改写了。PostgreSQL 的 规则系统 就是这种机制:它在 SQL 语句真正执行前,把原始查询重写成另一个查询。

规则用 CREATE RULE 创建。例如:

这段规则表示:当有人更新 packages 表里已完成的包裹时,不执行原来的更新,而是把新行插入历史表。与触发器不同,规则不依赖每一行的触发器函数,而是直接在查询解析和重写阶段修改 SQL。正因为如此,规则还能处理触发器做不到的事,比如改写 SELECT 查询,并且经常用于实现可写视图。

一个查询可以被多条规则依次改写,因此最终执行计划可以表示为:

主管会把所有订单先改一遍再下发,规则系统也是如此。它站在“语句层面”工作,所以非常适合做查询重写和历史数据重定向,而不是逐行处理。

Qexec=Rn(⋯R2(R1(Q0))⋯ )\displaystyle Q_{\text{exec}} = R_n\left( \cdots R_2\left( R_1(Q_0) \right) \cdots \right)
原始查询 Q_0 经过一条条规则 R_1, R_2, \dots, R_n 的嵌套改写,最终得到真正执行的查询 Q_{\text{exec}}。
  1. 原始查询Q_0 是应用层提交给数据库的原始 SQL,可以理解为梁叔收到的第一版订单。
    Q0Q_0
  2. 单条规则改写每条规则 R_i 接收一个查询并返回一个改写后的查询,比如把“空运”改成“陆运”。
    Qi=Ri(Qi−1)Q_i = R_i(Q_{i-1})
  3. 规则链规则按顺序嵌套执行,最后得到 Q_{\text{exec}},数据库只执行改写后的查询。
    Qexec=Rn(Qn−1)Q_{\text{exec}} = R_n(Q_{n-1})
sql4 行
CREATE RULE redirect_old ASON UPDATE TO packagesWHERE OLD.status = 'done'DO INSTEAD INSERT INTO packages_history VALUES (NEW.*);

触发器VS规则:各就各位

梁叔常被问到:既然都有自动化能力,为什么还要分两套设备?答案很简单:感应器适合处理“每一件包裹”,主管的红笔适合处理“整张订单”。触发器与规则系统各有分工。

触发器绑定在表上,随着每一行数据的变化而触发,天然适合做逐行校验、审计日志、级联更新。它能访问 NEW/OLD,能参与事务内的细粒度判断。规则系统则在查询开始前改写 SQL,适合把复杂的查询逻辑下沉到数据库层,例如把对视图的查询重写到真实表,或者把更新操作重定向到历史表。

两者的成本模型也可以对比:

梁叔不会让感应器去改写整张订单,也不会让主管去逐个检查包裹。工程上也是如此:审计、完整性校验默认选触发器;视图改写、查询重定向默认选规则系统。

Ctrigger=n×crow+Cfix,Crule=crewrite+Cplan\displaystyle C_{\text{trigger}} = n \times c_{\text{row}} + C_{\text{fix}}, \qquad C_{\text{rule}} = c_{\text{rewrite}} + C_{\text{plan}}
触发器成本与行数成正比,规则系统成本主要是查询重写和计划生成的固定开销,批量场景下规则往往更有优势。
  1. 触发器总成本n 行数据每行付出 c_{\text{row}},再加上固定开销 C_{\text{fix}}。
    Ctrigger=n×crow+CfixC_{\text{trigger}} = n \times c_{\text{row}} + C_{\text{fix}}
  2. 规则总成本规则系统只需付出查询改写成本 c_{\text{rewrite}} 和计划生成成本 C_{\text{plan}}。
    Crule=crewrite+CplanC_{\text{rule}} = c_{\text{rewrite}} + C_{\text{plan}}
  3. 适用判断当 n 很小且需要精细逐行逻辑时,触发器更适合;当需要改写整个查询或处理大批量重定向时,规则更合适。
    n×crowvscrewrite+Cplann \times c_{\text{row}} \quad \text{vs} \quad c_{\text{rewrite}} + C_{\text{plan}}

性能调优:让传送带不拥堵

随着订单量暴增,梁叔仓库的传送带开始频繁卡顿。原因很简单:每个包裹都在感应器前做太多额外动作。触发器性能调优的本质,就是减少不必要的“逐包处理”。

常见的优化手段包括:在触发器定义中加 WHEN 条件,让无关包裹直接滑过感应器;大批量操作改用语句级触发器或临时表缓存,只在语句结束后统一处理;把多个简单触发器合并成一个函数,减少重复扫描;用 pg_trigger_depth() 判断递归层级,避免触发器自己调用自己导致死循环。

经过优化后,触发器在批量场景下的平均单行开销可以写成:

梁叔在传送带前装上“分拣器”,把不需要处理的包裹提前分流;数据库里的 WHEN 条件和语句级触发器就是同样的分流器。优化不是让触发器消失,而是让它只在必要的时候才工作。

Cavg=crow+Cfixn\displaystyle C_{\text{avg}} = c_{\text{row}} + \frac{C_{\text{fix}}}{n}
随着处理行数 n 增加,固定成本被摊薄,但每行的函数执行成本 c_{\text{row}} 依然存在。
  1. 平均单行成本C_{\text{avg}} 表示每个包裹最终分摊到的平均触发开销。
    Cavg=crow+CfixnC_{\text{avg}} = c_{\text{row}} + \frac{C_{\text{fix}}}{n}
  2. 固定成本摊薄C_{\text{fix}} 除以行数 n,行数越多,这部分越小。
    Cfixn\frac{C_{\text{fix}}}{n}
  3. 行成本无法消失如果每行都要执行触发器函数,c_{\text{row}} 会一直保留,所以要重点优化函数本身和 WHEN 过滤。
    crowc_{\text{row}}

事务约束:同一本账本

梁叔要求仓库里的所有操作都记在同一本账本上:如果最后一笔记录写错了,前面所有改动都要一起作废。数据库中的触发器也运行在同一个事务里,触发器函数一旦抛出异常,整个事务都可能回滚。这既是安全保证,也是需要小心的风险。

比如在触发器里调用外部短信服务,包裹数据随后回滚,但短信已经发出去了,外部副作用无法撤销。因此,工程上会把“发短信”这类操作写入一张待发送表,真正发送交给外部任务,这样事务回滚时只会回滚待发送记录。

约束触发器是另一种常用工具,它把检查推迟到事务提交前执行。可以用 CREATE CONSTRAINT TRIGGER ... DEFERRABLE INITIALLY DEFERRED 实现。事务能否提交,取决于所有约束条件是否满足:

梁叔的账本逻辑让仓库不会出现半截对不上的记录;在 PostgreSQL 中,事务与约束触发器共同保证数据最终一致。记住:不能只想着“多做事”,还要想着“做错事之后能不能干净地撤销”。

commit(τ)  ⟺  ∀r∈T:  C(r)=true\displaystyle \text{commit}(\tau) \iff \forall r \in T:\; C(r) = \text{true}
一个事务只有在它涉及的所有数据行都满足约束条件 C 时才能提交,否则整笔交易作废。
  1. 事务集合T 表示当前事务中所有被修改或插入的数据行。
    TT
  2. 约束条件C(r) 是一个布尔条件,比如外键必须存在、金额必须大于零。
    C(r)=trueC(r) = \text{true}
  3. 提交判定commit(\tau) 当且仅当每一行都满足约束,等价于所有条件做逻辑“与”。
    ∀r∈T:  C(r)=true\forall r \in T:\; C(r) = \text{true}

应用模式:从审计台账到数据同步

梁叔经过多年实践,摸索出一套高效用法:出口处放一台自动拍照机,每个包裹离开都拍照留档,这就是审计日志;货架数据一变,前台大屏立即刷新,这就是数据同步;一个订单取消,关联的拣货单自动作废,这就是级联操作。这些都适合用触发器实现。

典型的审计触发器结构是:AFTER 行级触发器把操作类型、用户名、旧值、新值写入审计表。写入审计表可以表示为:

除了审计,还常用触发器做字段自动填充、统计汇总、缓存刷新。使用时要加上递归防护,例如用 pg_trigger_depth() 判断嵌套深度;上线前也要像梁叔试运行新设备一样,在测试库完整演练插入、更新、删除三条链路。自动化能力越强,越需要提前设计好“万一出错怎么办”。

audit←audit∪{(u,t,op,old,new)}\displaystyle \text{audit} \leftarrow \text{audit} \cup \left\{ (u, t, op, old, new) \right\}
每一次数据变化都会在审计集合中追加一条记录,包括用户 u、时间 t、操作类型 op、旧值和新值。
  1. 审计集合audit 是一张不断增长的日志表,类似梁叔的拍照留档册。
    audit\text{audit}
  2. 追加记录\cup 表示把新元组加入集合,而不是覆盖旧记录。
    audit∪{…}\text{audit} \cup \{\ldots\}
  3. 元组内容(u, t, op, old, new) 分别记录用户、时间、操作、旧值和新值,完整还原一次变更。
    (u,t,op,old,new)(u, t, op, old, new)

AI 应用场景

电商库存自动扣减与审计

阶段:数据工程与存储层

解决的问题:传统方式在应用层写扣库存逻辑,容易出现并发超卖,也无法在所有入口统一记录库存变更。通过 AFTER 行级触发器,每次订单修改库存时自动扣减并写入库存变更流水,保证审计完整。

具体案例:某电商平台在 inventory 表上创建 AFTER UPDATE 触发器,库存变化时自动清洗低库存告警;同时把变更写入 inventory_log,供 AI 库存预测模型读取历史序列。

数据仓库实时同步

阶段:数据接入与预处理

解决的问题:传统定时批处理同步延迟高,且容易重复抽取。使用规则系统把业务表上的写入请求重写到实时同步表,让下游分析任务更快拿到新数据。

具体案例:某零售团队把会员表的更新通过 CREATE RULE ... DO ALSO 同步到分析库的会员增量表,供推荐系统的特征管道实时消费。

金融风控中的延迟约束

阶段:数据质量与一致性保障

解决的问题:风控系统需要在一次交易中多次更新账户余额,如果每一步都立即校验,会出现中间状态不合法。使用可延迟约束触发器,把校验推迟到事务提交前。

具体案例:某支付系统用 CREATE CONSTRAINT TRIGGER ... DEFERRABLE INITIALLY DEFERRED 校验“转出账户余额不低于零”,保证整笔事务原子提交或整体回滚。

Python 演示

python66 行
class MiniPG:    """极简的 PostgreSQL 触发器/规则语义模拟器。"""     def __init__(self):        self.rows = {}        self.audit = []        self.before_triggers = []        self.after_triggers = []        self.rules = []        self.serial = 0     def add_before_trigger(self, event, fn):        self.before_triggers.append({"event": event, "fn": fn})     def add_after_trigger(self, event, fn):        self.after_triggers.append({"event": event, "fn": fn})     def add_rule(self, event, rewrite):        self.rules.append({"event": event, "rewrite": rewrite})     def apply(self, event, data):        for rule in self.rules:            if rule["event"] == event:                data = rule["rewrite"](data)        old = self.rows.get(data.get("id"))        for t in self.before_triggers:            if t["event"] == event:                data = t["fn"](data)        if event == "INSERT":            self.serial += 1            data["id"] = data.get("id", self.serial)            self.rows[data["id"]] = data        elif event == "UPDATE":            if data.get("id") in self.rows:                self.rows[data["id"]] = {**self.rows[data["id"]], **data}                data = self.rows[data["id"]]        self.audit.append({"event": event, "id": data.get("id"), "row": data})        for t in self.after_triggers:            if t["event"] == event:                t["fn"](data, old)        return data  def fill_default_region(data):    data.setdefault("region", "default")    return data  def set_vip(data):    data["level"] = "vip"    return data  def write_audit(data, old=None):    print("AFTER:", data.get("id"), data.get("level"), "old=", old)  if __name__ == "__main__":    db = MiniPG()    db.add_rule("INSERT", fill_default_region)    db.add_before_trigger("INSERT", set_vip)    db.add_after_trigger("INSERT", write_audit)    db.apply("INSERT", {"id": 101, "name": "robot"})    db.apply("INSERT", {"name": "sensor"})    print("rows:", db.rows)    print("audit_count:", len(db.audit))
目的:用一个小型 Python 类模拟 PostgreSQL 的执行顺序:规则先改写请求,BEFORE 触发器修改数据,写入动作完成后 AFTER 触发器记录审计。
  1. self.before_triggers.append({"event": event, "fn": fn})注册一个 BEFORE 触发器,相当于在仓库入口安装自动感应器。
  2. for rule in self.rules:在触发器和写入之前遍历规则,模拟 PostgreSQL 的查询重写阶段。
  3. data = rule["rewrite"](data)规则函数接收原始数据并返回改写后的数据,就像主管在订单上改字。
  4. for t in self.before_triggers:在真正写入前执行所有 BEFORE 触发器,允许修改包裹内容。
  5. self.audit.append({"event": event, "id": data.get("id"), "row": data})保存审计记录,模拟 AFTER 触发器的核心用途。
  6. db.add_rule("INSERT", fill_default_region)注册一条规则:插入时自动补全地区字段。
  7. db.add_before_trigger("INSERT", set_vip)注册 BEFORE 触发器:插入前把包裹标记为 VIP。
  8. db.add_after_trigger("INSERT", write_audit)注册 AFTER 触发器:插入后打印审计日志。

常见误区

误区我以为触发器像任务队列一样,等整条 SQL 跑完才统一触发,所以它在事务里是安全的后置操作。

正解触发器分为 BEFORE 和 AFTER 两种,BEFORE 甚至可以在行写入前修改数据。更准确地说,触发器依然是在同一条语句的执行过程中触发的,只是 AFTER 在相关行写完以后才执行。如果不分时机,可能误以为 AFTER 能修改写入值,实际上不能。

为何常见:从“事后通知”的直觉出发,容易把触发器理解成纯粹的异步事件,忽略了数据库内部的同步执行模型。

误区既然规则和触发器都能自动做事,那我只要学规则就行,触发器可以全部用规则代替。

正解两者机制完全不同。触发器是逐行绑定到 DML 事件上的函数调用,规则是预处理阶段对 SQL 的静态重写。审计、逐行校验适合触发器;视图改写、更新重定向适合规则。用规则做逐行审计,反而复杂且难以维护。

为何常见:规则能改写 SELECT,看起来能力更强,于是容易认为它“包含”了触发器,其实它们解决的问题维度不同。

误区触发器里可以调用外部接口,这样数据一变化我就自动收到通知,很方便。

正解触发器与触发它的语句同属一个事务,如果事务回滚,外部邮件已经发出就无法撤回。更稳妥的做法是把“待发送”任务写入表,由外部 worker 读取并发送;这样外部投递失败也不会破坏数据库一致性。

为何常见:人们习惯把数据库函数当成普通后端函数,忽略了数据库事务对外部系统没有魔法隔离能力。

误区大批量 UPDATE 很慢,一定是索引或磁盘问题,跟触发器没关系。

正解行级触发器会让每行数据都执行一次函数,批量导入时开销巨大。优化方案包括改语句级触发器、加 WHEN 过滤、用临时表聚合后再统一处理,这些往往比调索引更有效。

为何常见:触发器是透明的,执行计划里不会直接显示函数执行时间,因此问题容易被归因到其他数据库组件。

误区触发器自己调用自己的情况很少见,生产环境出了问题再修也不迟。

正解在互相联动的表上,A 表触发器更新 B 表,B 表触发器又更新 A 表,很容易形成递归甚至死循环。应该用 pg_trigger_depth() 限制嵌套深度,或在触发器函数里加标记字段切断回路。

为何常见:单表触发器好理解,但多表关联触发器的递归链条难以一眼看清,所以容易被忽略。

面试题

Q1. PostgreSQL 触发器无法直接在以下哪类语句上触发?

单选题 · easy

  • A.SELECT 查询语句← 正确答案
  • B.INSERT 插入语句
  • C.UPDATE 更新语句
  • D.DELETE 删除语句

答案:A。PostgreSQL 触发器主要绑定在 INSERT、UPDATE、DELETE 等 DML 事件上。SELECT 不修改数据,也没有 NEW/OLD 语义,所以不能作为普通触发器事件。就像仓库里的感应器只在包裹进出时工作,不会因为有人看了一眼仓库就启动。

Q2. BEFORE 行级触发器最典型的作用是什么?

单选题 · easy

  • A.修改即将写入的 NEW 行← 正确答案
  • B.直接执行 SELECT 查询
  • C.重写整条 SQL 语句
  • D.修改数据库表结构

答案:A。BEFORE 触发器在数据行写入前执行,可以拿到并修改 NEW,让最终写入的是修改后的值。规则系统才负责改写 SQL;修改表结构属于 DDL,不是普通触发器的职责。简单说,BEFORE 触发器是在包裹上车前重新贴标签。

Q3. INSTEAD OF 触发器通常定义在什么对象上?

单选题 · mid

  • A.视图上替代原操作← 正确答案
  • B.系统表上做权限控制
  • C.临时表上加速写入
  • D.物化视图上做增量更新

答案:A。INSTEAD OF 触发器主要用于视图,让原本不可写的复杂视图支持 INSERT、UPDATE、DELETE。它像仓管员看到“送往月球”的特殊订单后,启用一套替代流程。物化视图有自己的刷新机制,系统表和临时表一般不需要这种触发方式。

Q4. 关于行级触发器和语句级触发器,下列说法正确的有?

多选题 · mid

  • A.语句级对每个 SQL 只执行一次← 正确答案
  • B.行级对每个数据行都执行← 正确答案
  • C.行级一定比语句级执行更快
  • D.大批量写时语句级通常更划算← 正确答案

答案:ABD。语句级触发器只会执行一次,行级触发器则对每行执行一次。因此行级触发器的总成本随行数增长,大批量场景下语句级触发器通常开销更低。但“行级一定更快”是错误的,行数少时两者差异小,行数多时行级反而更慢。

Q5. PostgreSQL 规则系统的重写发生在哪个阶段?

单选题 · mid

  • A.查询解析与重写阶段← 正确答案
  • B.存储引擎页面写入
  • C.事务提交前的刷盘
  • D.WAL 日志回放阶段

答案:A。规则系统在查询解析之后、执行之前对 SQL 进行重写,所以它操纵的是查询本身,而不是数据行。就像主管在订单传给仓库工人之前先改单,而不是等包裹送出去后再追回。

Q6. 以下场景中,更适合用规则系统而非逐行触发器有哪些?

多选题 · hard

  • A.将 SELECT 视图查询自动改写← 正确答案
  • B.把 UPDATE 请求重定向到日志表← 正确答案
  • C.对插入的每一行做 NOT NULL 校验
  • D.对每行变更生成审计明细

答案:AB。规则系统擅长在语句层面改写查询,例如视图自动展开、更新重定向到历史表。NOT NULL 校验和逐行审计需要感知每一行的 NEW/OLD,触发器更合适。规则不是“更高级的触发器”,而是另一种工作方式。

Q7. 触发器函数抛出异常时,会直接影响什么?

单选题 · mid

  • A.当前事务的原子性← 正确答案
  • B.触发器的编译缓存
  • C.数据库索引的持久性
  • D.查询计划的统计信息

答案:A。触发器与触发它的语句处于同一事务中,异常会让当前事务进入失败状态,最终整体回滚。这保证账本不会被改一半。编译缓存、索引持久性和统计信息不会因为一次触发器异常而改变。

Q8. 以下哪些是降低大批量 DML 中触发器开销的有效手段?

多选题 · hard

  • A.改用语句级触发器聚合处理← 正确答案
  • B.在 WHEN 条件中过滤无关行← 正确答案
  • C.删除表上的全部索引
  • D.将逐行处理改为批量合并← 正确答案

答案:ABD。语句级触发器一次执行,WHEN 条件提前放行无关行,批量合并减少逐行调用次数,都能显著降低开销。删除全部索引会破坏查询性能,不是通用优化手段,是面试中的干扰项。

Q9. CREATE CONSTRAINT TRIGGER 常配合哪种子句实现延迟检查?

单选题 · easy

  • A.可延迟且初始延迟检查← 正确答案
  • B.可序列化且只读事务
  • C.非日志且带 OID 标识
  • D.指定表空间并内联存储

答案:A。约束触发器通过 DEFERRABLE INITIALLY DEFERRED 把检查推迟到事务提交前,适合需要中间状态暂时不满足约束的场景。其他选项与延迟检查无关。就像梁叔允许账本在一天中出现短暂不平,但收工时必须对账成功。

Q10. 规则系统可能导致查询被多次重写,业务端最应如何规避?

单选题 · hard

  • A.谨慎设计规则并用条件限制← 正确答案
  • B.为所有表关闭自动清理进程
  • C.把规则改成事件触发机制
  • D.在应用层缓存所有执行语句

答案:A。多重规则可能形成递归重写,设计时应使用条件、有限规则链和充分的测试来避免失控。关闭自动清理、缓存所有 SQL 都不能解决重写语义问题;事件触发器也不是普通规则系统的替代方案。