SQLite changes 与 total_changes:插入两行,触发器多写的两行算在哪里

前天 3阅读

一次插入两条主记录,触发器又为每条写一条日志。应用拿 changes 得到二,另一个统计却显示四,看起来像数据库多执行了插入。其实两个计数器的范围不同:一个看最近完成的主语句直接影响多少行,另一个累计这个连接执行过的变更。

把主表和附加写入同时查出来

下面建立 item 主表和 audit 日志表,AFTER INSERT 触发器只负责保存新编号。主语句使用一次多行 INSERT,而不是多次独立调用,这样 changes 的最近语句范围清楚。所有实验都发生在新建内存库,计数起点也从连接读取。

保存为 demo.py,执行 python demo.py。counters 同时调用两个 SQL 函数;Python 的 total_changes 属性提供同一连接的累计观察入口。代码还实际查询日志表数量,避免只看计数差,就断言触发器一定写到了预期位置。

SQLite changes 与 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 中独立运行。

SQLite:changes 的直接变化与触发器边界

SQLite:total_changes 的连接累计范围

SQLite:changes 与 total_changes SQL 函数

Python:sqlite3 Connection.total_changes

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。