SQLite changes 与 total_changes:插入两行,触发器多写的两行算在哪里
一次插入两条主记录,触发器又为每条写一条日志。应用拿 changes 得到二,另一个统计却显示四,看起来像数据库多执行了插入。其实两个计数器的范围不同:一个看最近完成的主语句直接影响多少行,另一个累计这个连接执行过的变更。
把主表和附加写入同时查出来
下面建立 item 主表和 audit 日志表,AFTER INSERT 触发器只负责保存新编号。主语句使用一次多行 INSERT,而不是多次独立调用,这样 changes 的最近语句范围清楚。所有实验都发生在新建内存库,计数起点也从连接读取。
保存为 demo.py,执行 python demo.py。counters 同时调用两个 SQL 函数;Python 的 total_changes 属性提供同一连接的累计观察入口。代码还实际查询日志表数量,避免只看计数差,就断言触发器一定写到了预期位置。
AI概念示意图:较小范围只包含主语句写入,较大范围还包含触发器产生的附加写入。
import sqlite3
db = sqlite3.connect(":memory:", isolation_level=None)
try:
db.executescript("""
CREATE TABLE item(id INTEGER PRIMARY KEY);
CREATE TABLE audit(item_id INTEGER NOT NULL);
CREATE TRIGGER record_insert AFTER INSERT ON item
BEGIN
INSERT INTO audit VALUES(NEW.id);
END;
""")
def counters():
return db.execute("SELECT changes(), total_changes()").fetchone()
baseline = db.total_changes
db.execute("INSERT INTO item VALUES(1), (2)")
direct, total = counters()
assert direct == 2 and total - baseline == 4
assert db.execute("SELECT count(*) FROM audit").fetchone() == (2,)
assert counters() == (2, 4)
print("after insert, direct / total:", direct, total)
db.execute("DELETE FROM item WHERE id=999")
assert counters() == (0, 4)
print("no matched row:", counters())
db.execute("BEGIN")
db.execute("INSERT INTO item VALUES(3)")
assert counters() == (1, 6)
db.execute("ROLLBACK")
counts = db.execute("""
SELECT (SELECT count(*) FROM item),
(SELECT count(*) FROM audit)
""").fetchone()
assert counts == (2, 2)
assert counters() == (1, 6)
print("after rollback, counters:", counters())
print("after rollback, stored rows:", counts)
finally:
if db.in_transaction:
db.execute("ROLLBACK")
db.close()第一行的 direct 为二,total 为四。主语句向 item 写两行,触发器向 audit 写两行,累计增量因此是四。changes 不把这些下层触发器语句算进主语句的直接行数,所以返回二并不表示日志没有写入。
最近语句与累计窗口分别管理
普通 SELECT 不会把 changes 清成零,因此查询日志数量以后,它仍为二。随后执行匹配不到记录的 DELETE,changes 才变成零,total 仍为四。这个对照说明计数绑定的是最近完成的写语句,不是最近一次任意 SQL 调用。
若要统计某段工作的累计变更,可以在开始前保存 total_changes,在结束后相减。不过窗口里同一连接执行的其他写入也会进入差值,不能在连接交错处理别的任务时,把整个增量都归给当前请求。需要让测量窗口和业务操作范围一致。
连接累计值也不会自动跨连接合并。换了连接就换了计数历史,其他连接对同一数据库做的修改不在这个值里。它适合观察当前连接的工作量,不能直接充当全库写入监控或某张表的实时记录数。
回滚后的计数不能当作保存结果
最后一段在显式事务中再插入编号三,连同日志一共增加两次行变更,然后主动回滚。两张表各自仍只有两行,但计数器保持一和六。本次实跑说明累计值记录过已执行的动作,并不会随回滚自动变成当前仍保存的行数。
因此“增量大于零”既不能证明提交成功,也不能证明业务对象新增了同样多。一个主对象可能带来多条日志,事务又可能最终撤销。对用户展示成功处理数量时,应采用业务结果及事务完成状态,技术计数可作为旁证。
total_changes 包含触发器及外键动作带来的变更,但并非所有内部动作都计入。例如 REPLACE 解决冲突时的附加删除有排除规则;changes 对这类辅助变化也有边界。需要精确审计时,应查对应机制,不能把两者简化成“小计”和无遗漏的“全计”。
在触发器内部调用 changes 还涉及进入和退出触发器时保存、恢复计数的规则。本例特意只在主语句完成之后查询,验证应用侧的常见用法;不要把得到的二直接搬成触发器每一行内部都应看到的值。
保留三个回归样本:带触发器的多行写入、没有匹配项的写入、执行后回滚的写入。验收同时看主表、附加表和两个计数器,才能说明每个数字具体属于哪个范围,避免后来增加触发器时悄悄改变统计口径。
参考资料
资料核对日期:2026年10月2日(北京时间)。示例在 Linux、Python 3.12.14、SQLite 3.53.1 中独立运行。


