SQLite 表达式索引:x+y 和 y+x 结果相同,为什么查询计划不同

10-01 3阅读

数学等价不保证走同一条访问路径

列表页按两个整数列的总和筛选,已经建立表达式索引,另一个接口却仍然扫描整张表。两条查询一个写 x+y,另一个写 y+x,返回结果看起来完全相同。SQLite 匹配表达式索引时不会做一般代数推导,表达式的写法也是需要核对的条件。

普通索引保存列值的有序结构;表达式索引则针对表中每行计算指定表达式,并把结果用于索引。查询中的表达式需要与索引定义匹配,除了空白等细小语法差异,不能假设任意数学等价的改写都会自动识别。

先比结果,再读计划

下面用 Python 标准库创建独立内存数据库,不需要安装 SQLite 命令行,也不会修改已有库。示例在 Python 3.12、SQLite 3.53.1 验证。插入一千行小整数,让查询结果和索引匹配关系容易人工核对,不把计时噪声混进实验。

SQLite 表达式索引:x+y 和 y+x 结果相同,为什么查询计划不同

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。

参考资料

  1. SQLite 官方文档:Indexes On Expressions

  2. SQLite 官方文档:EXPLAIN QUERY PLAN

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