为何外层查询加DISTINCT后,含子查询的SQLite查询耗时暴增3万倍?
问题原因分析与解决方案
核心根源:执行顺序的差异导致子查询执行次数暴增
SQLite处理带外层DISTINCT的查询时,执行逻辑和你预期的完全相反——它会先对主表每一行执行子查询,再对结果集去重,而非先去重name再执行子查询。这直接导致子查询被执行15000次(对应主表所有行),而非预期的249次(对应唯一name数量),这就是性能暴跌的关键。
分情况拆解执行逻辑
1. 移除外层DISTINCT时的高效执行
当没有外层DISTINCT时,SQLite的优化器能精准识别:两个子查询的结果仅依赖name字段,相同name的行对应的子查询结果完全一致。
- 优化器会自动启用相关子查询缓存:同一个
name值的子查询只执行一次,后续重复出现的name直接复用缓存结果,实际子查询仅执行249次。 - 甚至会直接将查询重写为等价的
GROUP BY逻辑,只需扫描一次表就能完成所有聚合计算,这也是耗时仅1-3ms的核心原因。
2. 保留外层DISTINCT时的低效执行
加上外层DISTINCT后,SQLite优化器没有触发上述重写,而是按直观路径执行:
- 扫描主表
s1的全部15000行; - 对每一行执行两个子查询(计算
section_count和document_count); - 将15000行的结果(包含大量重复的
name和相同聚合值)去重,最终得到249行。
- 这里的致命开销是:子查询被执行了15000×2=30000次。尤其是第二个带
DISTINCT的子查询,每次执行都需要扫描对应name的所有行、去重计数,重复执行15000次的累积开销直接拉满耗时。
3. 单独SELECT DISTINCT name快的原因
单独的去重查询仅需利用name索引快速遍历并去重,不需要执行任何子查询,所以耗时仅1-3ms。但这个高效逻辑无法和带子女查询的SELECT DISTINCT复用,因为优化器没有关联两者的执行计划。
验证方法
可以用EXPLAIN QUERY PLAN查看两种查询的执行计划:
- 无
DISTINCT的查询会显示SCAN sections+USE TEMP B-TREE FOR GROUP BY(或类似分组逻辑); - 有
DISTINCT的查询会显示SCAN sections s1,然后重复执行CORRELATED SCAN sections s2和CORRELATED SCAN sections s3共15000次,最后USE TEMP B-TREE FOR DISTINCT。
最优解决方案
直接改用GROUP BY替代外层DISTINCT,这是最贴合业务需求且性能最优的写法:
SELECT name, COUNT(name) AS section_count, COUNT(DISTINCT document_key) AS document_count FROM sections GROUP BY name
这个写法让SQLite明确按name分组聚合,只需扫描一次表就能完成计算,性能和无DISTINCT的查询一致,同时自动得到唯一的name结果。
内容的提问来源于stack exchange,提问作者Deane
相关产品推荐
相关产品推荐

