DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

SQLAlchemy 执行 VACUUM 报事务错误:先检查自定义 BEGIN 钩子

SQLite VACUUM 仍报事务错误时,核对自定义 BEGIN 钩子、驱动自动提交与维护连接的边界,避免裸 COMMIT 提前提交业务数据。

SQLAlchemy 连接 SQLite,执行 VACUUM 提示 cannot VACUUM from within a transaction,即使刚调用过 Session.commit,或者手工发过 COMMIT,下一次操作仍报同样的错。这时应检查执行 VACUUM 的那条连接是否又被事务钩子打开了事务。

SQLite 不允许在当前连接的活动事务中执行 VACUUM。SQLAlchemy 的逻辑事务状态、驱动的自动提交模式和应用主动发送的 BEGIN 是三个需要分开检查的地方。以下示例使用 SQLAlchemy 2.0 的同步 sqlite3 驱动;旧 1.4 应用也应先核对自己的连接事件,而不是直接把升级视为根因。

提交了业务 Session,为什么维护连接仍在事务里

Session.commit 只处理这个 Session 管理的事务,不保证另一条维护连接没有事务。更容易忽略的是,应用可能注册了这样的事件:

@event.listens_for(engine, "begin")
def emit_begin(conn):
    conn.exec_driver_sql("BEGIN")

这是说明问题的配置片段。它常与 sqlite3 事务控制调整配套使用,不能因为 VACUUM 出错就从整个应用中删掉。它意味着 SQLAlchemy 开始一段逻辑事务时,应用会向 SQLite 明确发送 BEGIN。

检查创建 Engine 的公共模块,以及 connect、begin 事件注册范围。事件若绑定到 Engine 类,另建一个 Engine 也可能受影响。尤其要留意显式 BEGIN、修改驱动 isolation_level,以及 Python 新版 sqlite3 的 autocommit 配置。

驱动级 AUTOCOMMIT 不会抵消应用自行发送的 BEGIN。SQLAlchemy 对 sqlite3 的 AUTOCOMMIT 方案与这些非默认事务控制钩子存在兼容边界,不能在原 Engine 上加一个参数就假定问题解决。

SQL 日志中的 BEGIN (implicit) 也不能单独证明 SQLite 已开启真实事务:在没有自定义 BEGIN 的驱动自动提交模式下,它可能只是 SQLAlchemy 的逻辑状态。应结合事件代码、实际发送的 SQL 和驱动状态判断,避免只凭日志里出现 BEGIN 就反复提交。

用明确隔离的维护连接执行

先由业务代码按自己的事务边界提交或回滚,并结束相关 Session 和未关闭的结果集。不要在业务事务中插入一条裸 COMMIT 来“解锁” VACUUM:它可能提前提交业务数据,同时让 ORM 仍保留与数据库不一致的事务状态。

维护任务可以使用专用 Engine,前提是它指向已确认存在的目标文件,并且不安装上述自定义事务钩子。下面的结构示例只适用于满足这些条件的 SQLAlchemy 2.0 同步 sqlite3 连接:

from sqlalchemy import create_engine

maintenance = create_engine(
    "sqlite:////absolute/path/app.db",
    isolation_level="AUTOCOMMIT",
)
try:
    with maintenance.connect() as conn:
        conn.exec_driver_sql("VACUUM")
finally:
    maintenance.dispose()

替换成核对过的数据库绝对路径。SQLite 连接不存在的路径可能创建新文件,不能以“执行成功但文件没变”判断维护完成。先核对文件位置、业务库身份和当前进程的配置。

此维护 Engine 不要挂载业务 begin 钩子,也不要添加会强制驱动进入事务的 connect 配置。若项目在 Engine 类上全局注册事件,应先调整事件适用范围,再使用维护任务;不能为一次维护关闭整个应用需要的事务保障。

这个任务不需要 ORM Session 或额外的 engine.begin 包装。业务 Engine 保持原事务配置;维护结束后销毁维护 Engine,不把它交回普通业务写入流程。

还有报错时,分清事务和其他连接的占用

当前连接有尚未结束的语句,也可能使 VACUUM 失败。检查游标、流式查询和结果集是否关闭,而不只是检查有没有显式 BEGIN。

其他连接占用数据库可能导致锁或 busy 类错误,这是另一个分支。关闭当前事务后仍出现锁等待,应安排短维护窗口,协调业务连接并检查长事务。WAL 模式也不会允许 VACUUM 在当前活动事务内运行,不需要为此直接切换日志模式。

VACUUM 会重建数据库,并需要写入权限、额外磁盘空间和相应的锁。根据操作方式,空闲空间可能需要达到原数据库大小的两倍。数据库持续增长或磁盘不足时,不要循环重试挤满剩余空间。

验收包括数据,而不只是文件变小

操作前保留一致的数据库备份和原连接配置。活跃的 WAL 数据库不能仅复制主文件就当成完整备份,应使用 SQLite 支持的一致备份方式,或在确认数据库关闭后处理所需文件。

完成后确认维护的是目标库、原事务错误已经消失、业务数据和关键查询正常。没有明确 INTEGER PRIMARY KEY 的表,VACUUM 可能改变隐含 ROWID;依赖 ROWID 持久标识的业务需要先评估影响。数据库文件不一定明显缩小,不能把缩小比例当作唯一成功标准。

恢复原代码配置不撤销已经执行的 VACUUM。若需要从备份恢复,必须先协调停写,并处理备份之后的新业务数据,避免为了恢复空间布局覆盖已经提交的新记录。