SQLite progress_handler:查询怎样主动停下来,为什么回调次数不是秒数

10-01 3阅读

先给长查询一个可观察的停止条件

交互式工具允许用户取消查询时,不能只把界面的加载图标关掉。数据库计算本身也要收到停止信号。Python 的 SQLite 连接可以注册进度回调,运行到检查点时读取一个轻量状态;回调返回非零值,当前操作就会被中断。它适合协作式取消,但不是按行数计算的完成百分比。

实验使用 Python 3.12.14 与 SQLite 3.53.1,仅建立内存数据库。先放入一百个整数,再执行两个表别名的笛卡尔积求和。我们不等待真实时间过去,而是在第三次回调时请求中断,这样停止条件容易复核,也不会把机器快慢混进结论。保存完整代码后运行 python demo.py。

注册、捕获和清理放在同一处

set_progress_handler 的第二个参数表示检查之间大约经历的虚拟机指令数量,不是每隔多少毫秒,也不是每执行几条 SQL。不同计划、SQLite 版本和操作阶段都会影响检查发生的位置。代码选用一百只是小实验参数,不应直接当作生产查询的统一预算。

回调只增加计数并返回布尔值,不执行其他 SQL。外层确认异常属于 SQLITE_INTERRUPT,避免把语法错误或其他数据库异常也误称为主动取消。无论查询正常完成还是失败,finally 都移除处理器,然后才开始新的查询,避免同一个取消状态误伤后续操作。

SQLite progress_handler:查询怎样主动停下来,为什么回调次数不是秒数

AI概念示意图:指令块经过检查点时遇到停止杆,清理后另一条路径仍可继续。图片只说明概念,不是运行截图。

import sqlite3

con = sqlite3.connect(":memory:")
calls = 0


def progress():
    global calls
    calls += 1
    return calls >= 3


try:
    con.execute("CREATE TABLE nums(n INTEGER NOT NULL)")
    con.executemany("INSERT INTO nums VALUES (?)", [(n,) for n in range(100)])
    con.commit()
    con.set_progress_handler(progress, 100)
    try:
        con.execute("SELECT sum(a.n + b.n) FROM nums a CROSS JOIN nums b").fetchone()
    except sqlite3.OperationalError as exc:
        assert exc.sqlite_errorcode == sqlite3.SQLITE_INTERRUPT
        print("cancelled:", exc.sqlite_errorname)
    else:
        raise AssertionError("query should be interrupted")
    finally:
        con.set_progress_handler(None, 0)

    assert calls == 3
    saved_calls = calls
    total = con.execute("SELECT sum(n) FROM nums").fetchone()[0]
    assert total == 4950
    assert calls == saved_calls
    print("callbacks:", calls)
    print("next query:", total)
finally:
    con.close()

用三行结果验证真正的取消

本次输出依次为 cancelled: SQLITE_INTERRUPT、callbacks: 3、next query: 4950。第一行确认数据库操作确实被中断;第二行对应我们设置的停止规则;第三行验证清理以后,连接还能够读取原有数据。最后的计数断言还检查了新查询没有继续调用旧处理器。

这不表示查询准确执行了三百条指令,也不能换算为固定时间。本例先提交样本插入,再中断只读查询,因此后续求和是在清楚的数据状态下进行。若把同样方法用于写入流程,需要另外核对事务状态和重试语义,不能把这次只读结果推广成所有写入都可直接接着做。

调试时可以先保留这里固定的第三次检查,再替换成真实取消标记。这样能分别验证中断通道和界面事件传递;若前者有效而按钮无效,就应追踪标记何时被更新,而不是先改数据库查询。

把检查点与执行时间分开

如果真实需求是响应取消按钮,可以让回调读取专门的取消标记;若要检查截止时刻,则需要自己比较时钟。检查只在回调获得执行机会时发生,耗时的自定义函数等环节可能推迟响应,不能把它宣传成严格的墙钟时间上限。间隔过小还会增加回调开销,应使用代表查询测量。

每条连接同时只有一个进度处理器,新注册会覆盖旧处理器,连接池尤其要明确谁负责安装与移除。SQLite 官方还要求回调不要修改触发它的连接,准备或执行另一条语句也属于这里需要避开的操作。较新 SQLite 还可能在复杂语句准备阶段触发回调,因此记录宜称操作检查次数,而不是已返回记录数。

参考资料

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