SQLite deterministic 函数标志:两处参数完全相同,为什么回调仍可能执行两遍
把昂贵的Python函数注册给SQLite,并加上deterministic=True以后,直觉上可能期待相同参数只计算一次。但这个标志首先是在承诺函数结果具有确定性,不是在申请一个跨表达式、跨行乃至跨查询的缓存。若性能设计建立在“相同参数必然只调用一次”上,查询改写或SQLite升级就可能打破预期。
本文在Linux、Python 3.12.14、SQLite 3.53.1中使用固定样本实跑。官方文档核验于2026年10月3日;在线文档显示的补丁版本可能不同于这里记录的实际解释器。程序不访问真实业务数据,也不连接远程服务。
AI模型生成的原创概念图:两个相同输入分别经过计算得到相同结果,缓存作为独立的存储机制放在旁边;用于解释程序模型,不是软件截图、测试截图或实拍照片,精确行为以程序和实际输出为准。
完整程序与实际输出
将下面程序保存为tech15.py,执行python tech15.py。预期出现的异常已在例子里处理;退出码为零才表示本次演示正常完成。
import sqlite3
calls = []
def double(value):
calls.append(value)
return value * 2
connection = sqlite3.connect(":memory:")
try:
connection.create_function("double_value", 1, double, deterministic=True)
result = connection.execute("SELECT double_value(7), double_value(7)").fetchone()
print("values=", result)
print("observed_calls=", calls)
assert result == (14, 14)
calls.clear()
cache = {}
for value in (7, 7, 8):
if value not in cache:
cache[value] = double(value)
print("explicit_values=", [cache[value] for value in (7, 7, 8)])
print("explicit_calls=", calls)
print("sqlite=", sqlite3.sqlite_version)
finally:
connection.close()本次实际标准输出:
values= (14, 14) observed_calls= [7, 7] explicit_values= [14, 14, 16] explicit_calls= [7, 8] sqlite= 3.53.1
测试只记录调用,不让调用次数决定返回值
示例函数double始终返回输入乘二,只把参数追加到列表里用作观察。查询在同一行选择两次double_value(7),本次结果是两个14,调用记录为[7, 7]。这已经足以反驳“标成确定性就自动对所有相同参数去重”的假设,但不能反过来宣称所有版本、所有查询都必定执行两遍。
代码只断言返回值等于(14, 14),没有把观察到的调用次数写成跨版本正确性断言。这样做刻意区分语义与实现:结果稳定是函数的承诺,具体算了几次可能受表达式形态和优化器影响。测试输出保留SQLite版本,便于将来比较性能变化,同时避免把变化误报成数据库破坏了SQL语义。
确定性是一份承诺,不是引擎替你证明
真正的确定函数必须在相同输入下提供相同结果。当前时间、随机状态、可变配置文件和外部服务响应都可能让同参调用发生变化,不能为了获得优化机会而随手打上标记。本例追加日志只用于本地诊断,不参与返回值;实际生产函数最好避免依赖必须发生的外部副作用,因为优化器可能减少或调整求值。
这个标记还影响函数能否参与某些持久化表达式,例如表达式索引。若函数后来对同一输入给出不同答案,索引保存的结果可能与当前函数规则不一致。代码审查需要问的不是“这一分钟似乎很稳定”,而是数据库所依赖的生命周期内是否保持同一意义;规则升级时也应明确索引等派生数据如何重建。
需要明确复用,就设计明确缓存
程序后半段不通过SQL隐含求值,而在应用层维护一个小字典。输入顺序为7、7、8,第一次遇到键才计算,输出值为14、14、16,实际计算参数只有7和8。这里复用来自显式的if和字典存储,控制关系直接可见,和数据库是否选择某种优化无关。
这段缓存只适用于示例中的小整数与短生命周期。真实缓存必须定义键的全部组成、最大容量、失效规则、异常是否缓存以及并发重复计算的处理。若函数结果还依赖版本化规则,规则版本也应参与键。漏掉一个依赖,即使缓存命中率再高,也可能稳定地返回错误结果;不要把性能目标置于语义之前。
把优化观察与业务验收分开记录
性能测试可以统计执行时间和回调次数,但应在固定SQLite版本、固定数据量和查询计划条件下解释这些数字。调整WHERE条件、别名或子查询结构后,要重新测量;不能仅凭SQL文字上只写了一次函数,就推断实际求值一定只有一次。优化器可以进行合法变换,表达式外观不是执行账本。
若需求必须保证某个动作恰好发生一次,例如发消息、计费或调用远程接口,就不应把动作藏在SQL标量函数里等待优化器安排。先由应用明确计算或执行,再把稳定结果作为参数写入数据库,通常更容易审计。本教程使用纯内存数据库并显式关闭连接,验证的是函数标志的边界,没有改变任何真实表或索引。
验证记录与参考资料
本次完整程序退出码为0,标准错误为空。回调次数是当前SQLite版本及这条查询的观察结果,不是deterministic标志对调用次数的保证。


