SQLite last_insert_rowid:刚插入的编号,为什么换连接或写日志后就变了

前天 3阅读

程序创建一条记录,接着写入另一张日志表,最后才查询 last_insert_rowid,拿到的编号却属于日志。另一次把查询交给连接池借出的新连接,结果又变成零。这个函数没有表名参数,因为它读取的是当前连接记住的最近插入行号,并非某张表刚增加的记录。

让两个连接看同一个数据库

如果分别使用两个普通内存数据库,很容易把现象误解释成数据本来就不共享。下面改用自动清理的临时数据库文件,两个连接能读取同一张表,同时各自保留连接状态。保存为 demo.py 后运行 python demo.py;代码不会打开已有业务数据库。

示例使用明确给定的主键四十一、七十三和九百,使每次读到的编号都有可追踪来源。两张表都是普通行号表,INTEGER PRIMARY KEY 对应 rowid。这里研究的是“最近插入值属于谁”,所以不依赖自动编号算法,也不把最大编号作为查询依据。

import sqlite3
from pathlib import Path
from tempfile import TemporaryDirectory

def latest(db):
    return db.execute("SELECT last_insert_rowid()").fetchone()[0]

with TemporaryDirectory(prefix="rowid-scope-") as folder:
    path = Path(folder) / "demo.sqlite"
    a = sqlite3.connect(path, isolation_level=None)
    b = sqlite3.connect(path, isolation_level=None)
    try:
        a.execute("CREATE TABLE item(id INTEGER PRIMARY KEY, note TEXT)")
        a.execute("CREATE TABLE audit(id INTEGER PRIMARY KEY, note TEXT)")
        cursor = a.execute("INSERT INTO item VALUES (?, ?)", (41, "alpha"))
        item_id = cursor.lastrowid
        assert item_id == 41
        assert b.execute("SELECT id FROM item").fetchall() == [(41,)]
        assert (latest(a), latest(b)) == (41, 0)
        print("same database, separate state:", latest(a), latest(b))
        b.execute("INSERT INTO item VALUES (?, ?)", (73, "beta"))
        assert (latest(a), latest(b)) == (41, 73)
        print("after second connection inserts:", latest(a), latest(b))
        a.execute("INSERT INTO audit VALUES (?, ?)", (900, "created"))
        assert latest(a) == 900 and item_id == 41
        print("other table / saved item:", latest(a), item_id)
        try:
            a.execute("INSERT INTO audit VALUES (?, ?)", (900, "duplicate"))
        except sqlite3.IntegrityError:
            pass
        else:
            raise AssertionError("duplicate key was accepted")
        assert latest(a) == 900
        print("after failed insert:", latest(a))
        a.execute("BEGIN")
        a.execute("INSERT INTO audit VALUES (?, ?)", (901, "temporary"))
        a.execute("ROLLBACK")
        remaining = a.execute("SELECT count(*) FROM audit WHERE id=901").fetchone()[0]
        assert latest(a) == 901 and remaining == 0
        assert latest(b) == 73
        print("after rollback / matching rows:", latest(a), remaining)
    finally:
        b.close()
        a.close()

第一行应输出 41 0:连接乙已经读到了甲提交的记录,却没有继承甲的最近插入值。乙自己插入七十三以后,两边分别保持四十一与七十三。随后甲写入日志表,甲的连接值变成九百,而提前保存的 item_id 仍然是四十一。

SQLite last_insert_rowid:刚插入的编号,为什么换连接或写日志后就变了

AI生成概念示意图:每个连接有自己的最近插入记忆,表内记录的变化与这份记忆需要分别核对。

尽早把编号交回对应调用者

可靠的接口应在完成目标插入后,立即取得对应编号并保存到局部变量,再执行日志等其他工作。本例使用刚返回的游标读取 lastrowid,再取出普通整数。这样后续另一张表的写入不会改变已经保存的值,也不必让调用者猜测当前连接最后执行了什么。

这不意味着游标属性是所有执行方式通用的结果集合。Python 的 lastrowid 有明确适用范围,批量执行、脚本执行和无 rowid 表不能直接照搬这套假设。设计批量接口时要重新约定每条输入怎样对应输出,不能用一个最后值冒充全部记录的编号。

连接池还会把问题放大:同一个数据库地址不代表同一个连接。如果插入后归还连接,再借一个连接查这个函数,可能得到零,也可能是那个连接之前执行其他任务留下的值。应该在持有连接的同一段调用里完成取值,并直接返回结果。

最近执行成功,不等于当前记录仍在

重复插入日志主键九百触发约束错误,连接记住的值仍为九百。接着插入九百零一并回滚,日志表中已没有这条记录,连接值却仍是九百零一。这两个断言说明它是一份插入历史状态,不能被当作本次语句成功标志或现存记录检查。

因此,应用要分别处理执行异常、取回编号和事务结果。不要在异常处理里重新查询这个函数,然后因为拿到一个非零值就继续使用它。那可能正是较早操作遗留的编号,甚至可能指向另一张表;先保存的编号也需要与本次事务是否成功一起解释。

本文的日志写入是应用显式发出的第二条语句。触发器内部插入有自己的临时取值与恢复规则,不能直接用这个跨表例子概括;虚拟表也可能存在额外行为。实际排错时,先列出同一连接中真正执行的语句顺序,再决定需要补哪一种专门实验。

最后避免多个任务在同一连接上交错完成“插入、取值”两步。只给取值动作加锁不够,保护范围必须覆盖对应操作单元,或让任务各自持有连接。回归测试同时保留另一连接和另一张表,通常比只在空数据库里插入一次更容易发现错误关联。

资料核对日期:2026年10月2日(北京时间)。示例在 Python 3.12.14、SQLite 3.53.1 中独立运行。

官方参考:SQLite 最近插入行号接口;Python sqlite3 Cursor.lastrowid。

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