SQLite CASE 求值次数:同一个函数写进两个 WHEN,为什么会执行两遍
给查询结果换一个状态名称,常见写法是连续判断某个表达式是否等于一、是否等于二。表达式如果只是读取普通列,看起来没什么区别;换成较昂贵的自定义函数后,第二个条件可能让同一函数再次执行。结果文字相同,并不说明求值过程也相同。
让调用次数成为可以断言的结果
SQLite 的 CASE 有两种形式。把共同表达式放在 CASE 后、首个 WHEN 前,数据库会先求值一次,再拿这个结果逐项比较。省略共同表达式、在各个 WHEN 中分别写完整条件时,每个条件负责自己的求值。选择哪种形式,应取决于条件是否真的在比较同一个值。
下面保存为 demo.py,运行 python demo.py。数据库只在内存中创建,两个 Python 函数也只追加诊断列表。read_state 固定返回二;touch 记录哪个结果分支被调用。每轮开始前清空列表,避免把前一次实验的记录误算进去。
import sqlite3
db = sqlite3.connect(":memory:")
events = []
def read_state():
events.append("read")
return 2
def touch(label):
events.append(label)
return label
def fail():
raise RuntimeError("unselected branch executed")
db.create_function("read_state", 0, read_state)
db.create_function("touch", 1, touch)
db.create_function("fail", 0, fail)
try:
simple = db.execute("""
SELECT CASE read_state()
WHEN 1 THEN touch('first')
WHEN 2 THEN touch('second')
ELSE touch('other') END
""").fetchone()[0]
assert simple == "second" and events == ["read", "second"]
print("simple:", simple, events)
events.clear()
searched = db.execute("""
SELECT CASE
WHEN read_state() = 1 THEN touch('first')
WHEN read_state() = 2 THEN touch('second')
ELSE touch('other') END
""").fetchone()[0]
assert searched == "second" and events == ["read", "read", "second"]
print("searched:", searched, events)
events.clear()
first = db.execute("""
SELECT CASE WHEN 1 THEN touch('first')
WHEN fail() THEN touch('second') ELSE fail() END
""").fetchone()[0]
assert first == "first" and events == ["first"]
print("first match:", first, events)
assert db.execute("SELECT CASE 2 WHEN 1 THEN fail() ELSE 'other' END").fetchone() == ("other",)
assert db.execute("SELECT CASE 2 WHEN 1 THEN 'first' END").fetchone() == (None,)
print("unselected branches: not evaluated")
finally:
db.close()第一行应是 simple: second ['read', 'second'],第二行的 searched 则包含两次 read。两种写法都选中第二项,但第一种只读取一次共同输入。第三行只出现 first,说明第一项命中后,后面的条件及其他结果都没有被执行。
AI生成概念示意图:一次读取的结果可以用于多次比较,最终只进入选中的结果分支。
选择结果之前,不必先算完所有结果
两种 CASE 都采用惰性求值。按顺序找到第一个成立的条件后,只计算它对应的 THEN;没有匹配时才使用 ELSE。最后一组测试把会失败的函数放在未选中的位置,查询仍正常完成。这里验证的是表达式的实际执行,不能据此认为非法语法也能藏在未选中的分支里。
读代码时,可以先把“取得待判断的值”和“计算选中结果”分开标记。前者重复,可能增加成本;后者提前执行,可能触发本来不需要的解析。这个实验不测性能,也不据两次调用推算耗时比例,它只固定一个能够解释差异的执行事实。
相等比较与独立条件各有用途
状态码映射适合使用共同基表达式。若每个分支判断的是不同字段,或者使用区间、存在性等条件,逐项 WHEN 往往更清楚。不要为了减少文字,把几种不同判断硬改成相等比较;也不要把所有搜索式 CASE 都称为低效,真正的重复取决于写进去的表达式。
我们的计数函数故意保留可观察记录,用来解释文档约定。正式业务的查询函数应尽量避免发送消息、写文件等外部副作用,否则查询重试、重复读取或别处再次引用函数,都可能执行额外动作。CASE 对本次表达式的保证,不是整条业务流程只调用一次的保证。
给函数注册 deterministic 标记也不是缓存开关。只有函数确实满足相同输入产生相同结果的要求时才能使用它,不能为了压低计数而添加标记,再把某个执行计划的行为当成长期契约。本例保留接口默认设置,把需要检查的规则直接写进 SQL。
验收时同时核对返回文字和调用列表。只比较最终值会漏掉重复计算,只数调用又可能漏掉选错分支。可以继续把固定状态改成第一项和不匹配值,分别确认提前结束与 ELSE 路径;若查询来自模板生成器,还应检查生成后的完整表达式有没有重复粘贴共同输入。
资料核对日期:2026年10月2日(北京时间)。示例在 Python 3.12.14、SQLite 3.53.1 中独立运行。


