AFTER UPDATE行级触发器是否与所属UPDATE操作具备原子性?
我希望在account表的原始行被修改(更新)时,向account_balance_change历史表中插入一条记录,并使用原始行的ID作为该表的FOREIGN KEY。但该ID可能会变更或被删除。
示例代码如下:
CREATE TABLE account ( id BIGSERIAL PRIMARY KEY, username TEXT NOT NULL, balance BIGINT NOT NULL DEFAULT 0 ); CREATE TABLE account_balance_change ( account_id BIGINT NOT NULL, diff BIGINT NOT NULL, ts TIMESTAMPTZ NOT NULL, FOREIGN KEY (account_id) REFERENCES account (id) ON DELETE CASCADE ON UPDATE CASCADE ); CREATE FUNCTION dump_diff() RETURNS TRIGGER LANGUAGE plpython3u AS $$ from datetime import datetime now = datetime.now() old = TD['old'] new = TD['new'] diff = new['balance'] - old['balance'] query = f""" INSERT INTO account_balance_change ( account_id, diff, ts ) VALUES ($1, $2, $3) ; """ # i know it should be cached, but for the sake of simplicity... stmt = plpy.prepare(query, ["BIGINT", "BIGINT", "TIMESTAMPTZ"]) # see how it's using new['id'], so that there can be no mistake in the new record stmt.execute([new['id'], diff, now]) $$; CREATE TRIGGER after_account_balance_update_trigger AFTER UPDATE OF balance ON account FOR EACH ROW EXECUTE FUNCTION store_diff() ;
预期行为是每当有人UPDATE任意account的balance值时,余额变动差值及变动时间会被写入account_balance_change表。
但根据系统要求,ID可能会变更或被删除。因此,我们能否安全假设在触发器执行时new['id']不会发生变化?是否会出现竞态条件导致new['id']在数据库中不复存在?
这本质上可以归结为一个核心问题:AFTER UPDATE TRIGGER是否与其所属的UPDATE操作具备原子性?
核心结论
AFTER UPDATE触发器与其所属的UPDATE操作完全具备原子性,你可以安全依赖new['id']的有效性,不会出现竞态条件导致该ID在触发器执行时消失或变更。
具体解释
事务原子性保障
PostgreSQL中,触发器(包括AFTER类型)是所属DML操作(这里是UPDATE)所在事务的一部分。整个UPDATE操作加上触发器内的INSERT操作会作为一个原子单元执行:要么全部成功提交,要么全部回滚。new和old的稳定性
在AFTER UPDATE触发器中,new变量代表的是UPDATE操作执行后、事务提交前的行状态。只要UPDATE操作本身成功修改了行(包括ID的变更,如果有的话),new['id']就是最终的、确定的ID值,不会在触发器执行过程中被其他事务修改或删除——因为PostgreSQL的MVCC机制会保证当前事务看到的行版本是一致的,其他事务的修改不会影响当前事务内的触发器执行。外键联动的安全性
你的外键设置了ON UPDATE CASCADE和ON DELETE CASCADE,但这只会在事务提交后生效(或者说,是事务内的一部分)。触发器内插入的account_id引用的是new['id'],而这个ID对应的行在当前事务中是存在的,所以外键约束不会触发错误。关于ID变更的场景
如果UPDATE操作同时修改了id和balance,触发器中的new['id']会是更新后的ID,插入到account_balance_change表中的记录会自动关联到新ID,外键的ON UPDATE CASCADE也会确保后续如果ID再变更时历史记录的ID同步更新——不过更合理的做法通常是不允许修改主键ID,因为主键应该是行的唯一标识,修改主键可能带来一系列问题。
额外建议
- 尽量避免修改主键
id:主键设计的初衷是唯一标识行,频繁修改主键会破坏数据的稳定性,也会增加外键联动的开销。 - 触发器函数中可以添加
diff为0时跳过插入的逻辑,避免无意义的历史记录。 - 把
datetime.now()替换为PostgreSQL的CURRENT_TIMESTAMP,避免Python和数据库的时间不一致问题,修改后的插入语句可以简化为:
INSERT INTO account_balance_change (account_id, diff, ts) VALUES ($1, $2, CURRENT_TIMESTAMP);
内容的提问来源于stack exchange,提问作者winwin

