SQLite 表达式索引:x+y 和 y+x 结果相同,为什么查询计划不同
数学等价不保证走同一条访问路径
列表页按两个整数列的总和筛选,已经建立表达式索引,另一个接口却仍然扫描整张表。两条查询一个写 x+y,另一个写 y+x,返回结果看起来完全相同。SQLite 匹配表达式索引时不会做一般代数推导,表达式的写法也是需要核对的条件。
普通索引保存列值的有序结构;表达式索引则针对表中每行计算指定表达式,并把结果用于索引。查询中的表达式需要与索引定义匹配,除了空白等细小语法差异,不能假设任意数学等价的改写都会自动识别。
先比结果,再读计划
下面用 Python 标准库创建独立内存数据库,不需要安装 SQLite 命令行,也不会修改已有库。示例在 Python 3.12、SQLite 3.53.1 验证。插入一千行小整数,让查询结果和索引匹配关系容易人工核对,不把计时噪声混进实验。
AI概念示意图,非真实界面
import sqlite3
connection = sqlite3.connect(':memory:')
try:
connection.execute(
'CREATE TABLE points(x INTEGER, y INTEGER)'
)
connection.executemany(
'INSERT INTO points VALUES (?, ?)',
((i, i * 2) for i in range(1000)),
)
connection.execute('CREATE INDEX idx_sum ON points(x+y)')
queries = (
('same order', 'SELECT x, y FROM points WHERE x+y = ?'),
('swapped', 'SELECT x, y FROM points WHERE y+x = ?'),
)
all_results = []
for label, sql in queries:
rows = connection.execute(sql, (42,)).fetchall()
plan = connection.execute(
'EXPLAIN QUERY PLAN ' + sql, (42,)
).fetchall()
print(label, 'rows:', rows)
for node in plan:
print(label, 'plan:', node[3])
all_results.append(rows)
assert all_results == [[(14, 28)], [(14, 28)]]
print('same result:', all_results[0] == all_results[1])
finally:
connection.close()计划里的差异比一次耗时更直接
两条查询均得到 [(14, 28)]。本次运行的第一条计划包含 SEARCH points USING INDEX idx_sum,第二条是 SCAN points。前者可以按索引中的和定位,后者没有把交换顺序后的表达式匹配成这个索引的查找条件。
这里选用较小整数,避免数据类型或溢出影响计算,关注点是相同筛选结果背后的访问路径。判断优化是否正确时,应先对照完整结果,再检查计划;只看到查询变快,却没有验证结果一致,仍不足以确认改写安全。
实际修正通常是统一生成查询的表达式写法,而不是再创建一个交换顺序的重复索引。多个位置重复拼接计算公式容易漂移,可以把 SQL 模板集中管理,或在合适的数据模型中考虑有稳定名称的计算列。
匹配成功之后,优化器仍然要做选择
表达式匹配只意味着该索引可被考虑,不保证任何查询都会选择它。表的大小、筛选比例、排序需求和其他可用索引都会影响计划。本文不使用强制索引提示,展示的是这个数据集与当前引擎下真实选择的结果。
EXPLAIN QUERY PLAN 的输出是调试信息,格式会随 SQLite 版本改变。代码打印说明文字方便人工观察,但只对业务结果作断言,没有把一整段计划字符串固定成跨版本接口。升级时应重新阅读计划,而不是因文字排版变化误判业务失败。
表达式索引还有限制:只能引用被索引表中的列,不能包含子查询,函数需要满足确定性要求。随机数或依赖变化外部状态的计算,不适合放进这类索引。否则存储的索引值与以后计算的结果可能失去一致关系。
建立索引也有代价。新增和更新记录时需要维护计算结果,占用的磁盘空间和写入成本随数据增长。不要因为一个微型例子里出现 SEARCH 就给所有计算都建索引,应结合真实查询频率、选择性与写入负担做取舍。
表达式索引从 SQLite 3.9.0 起可用,使用此特性的数据库不能交给更早的引擎读取。部署前核对应用实际链接的 SQLite 版本,比只看操作系统里是否安装了某个命令更可靠;Python 程序可以直接查看 sqlite3.sqlite_version。


