3、查询性能基础:索引的作用、查询计划、EXPLAIN QUERY PLAN

很多朋友一上来就问:Room 怎么优化?加索引就够了?嗯,没那么简单。但离开索引,后面所有的优化手段都像空中楼阁。我先带你把“索引”这颗地基打牢。

这一章,我不打算给你堆概念。咱们直接从数据库怎么干活的角度,聊聊索引到底帮了什么忙,以及你怎么用 EXPLAIN QUERY PLAN 来“看”它干活。相信我,搞懂这个,后面你调优就顺手多了。

索引为什么能加速查询?

你想想看,SQLite 本身是个小型文件数据库。没有索引的时候,一条 WHERE 条件,它就把整张表从头到尾扫一遍——这叫全表扫描。我见过一个日志表,数据量才 10 万行,一个不带索引的查询跑了 3 秒多。用户直接骂娘了。

索引的本质,就是一个按列排好的有序结构。最常见的是 B-Tree 索引。查的时候,它不扫描全表,而是走树查找——复杂度从 O(n) 降到 O(log n)。说白了,就像你从一堆乱放的书里找一本, vs 按字母顺序排列的书架上找。哪个快,一目了然。

重要概念: 索引不是越多越好。写操作(INSERT/UPDATE/DELETE)需要维护索引,反而变慢。平衡点才是王道。

我在项目里遇到过的一个坑:给所有字段都加了索引,结果插入一条记录花了 200ms。后来删掉一半冗余索引,插入降到 20ms,查询也只慢了一丁点。所以我的习惯是——只给 WHERE、JOIN、ORDER BY 里频繁用到的列加索引。

查询计划:数据库的“路线图”

你写了一条 SQL,数据库不会直接傻跑。它先制定一个执行计划,就是优先走哪个索引、用什么方式扫描、怎么连接表。这个计划如果不理想,你的查询就等着慢吧。

SQLite 的查询计划可以简单归类为几种扫描类型:

扫描类型 含义 性能
SCAN TABLE 全表扫描,无索引 最慢
SEARCH TABLE USING INDEX 通过索引查找 快(O(log n))
SEARCH TABLE USING COVERING INDEX 覆盖索引,直接读索引就能拿到数据 极快(无需回表)
SEARCH TABLE USING INTEGER PRIMARY KEY 通过整型主键直接定位(SQLite 主键就是索引) 最快

你看,最理想的是 USING INTEGER PRIMARY KEY 或 COVERING INDEX。我曾经帮一个团队优化报表查询,把 SELECT * 改为只查索引中已有列,瞬间从 800ms 降到 30ms。嗯,覆盖索引就是这么暴力。

EXPLAIN QUERY PLAN 实战

光说不练假把式。SQLite 提供了一个非常实用的命令:EXPLAIN QUERY PLAN。在 Room 的调试阶段,你完全可以用它来验证自己的索引设得对不对。

用法很简单:

EXPLAIN QUERY PLAN
SELECT * FROM user WHERE name = '张三';

结果会返回几行文本,描述了 SQLite 打算怎么执行。比如:

SEARCH TABLE user USING INDEX idx_name (name=?)

看到这句,说明索引被用上了,好迹象。如果出来的是:

SCAN TABLE user

那就说明你的 WHERE 条件没有命中任何索引。可能原因:

  • 根本没有建索引
  • 用 LIKE '%张三%' 前置通配符导致索引失效
  • 对列做了函数运算:WHERE LOWER(name) = '张三'

我个人习惯是在 Room 的 DAO 方法里,直接通过 @RawQuery 或 Android Studio 的 Database Inspector 来跑 EXPLAIN QUERY PLAN。你甚至可以在单元测试里写一段代码,自动抓取计划并断言它是索引查找而不是全表扫描。这样一跑回归测试,要是有新查询忘加索引,立马就报红。

小技巧: 在 Application 的调试版 build 中,用 Room 的 setQueryCallback 可以拦截所有 SQL,自动执行 EXPLAIN QUERY PLAN。不过注意会影响性能,只开 debug 就够。

常见索引优化误区

我见过不少踩坑的例子,列几个最典型的:

  • 所有表都加 id 自增主键就完了? 不,如果你的 WHERE 条件是 phone 列,不对 phone 加索引,照样全表扫描。
  • 索引越多查得越快? 写操作会变慢,索引文件也会变大。我曾经看到一个表有 12 个索引,文件体积翻了两倍,插入一条记录要 1 秒。
  • 没用上索引就是索引建得不对? 有时是 SQL 写法问题。比如 WHERE age + 1 > 20,SQLite 不会对这个表达式使用索引。改写为 WHERE age > 19 才行。
避坑指南: 我曾经一个同事把 ORDER BY date DESC 的日期列没加索引,结果每次排序都要先全表扫描再磁盘排序,耗时 2 秒。加上索引后,ORDER BY 直接走索引排序,降到 50ms。记住:ORDER BY 也需要索引。

好了,索引和查询计划这两个概念,是你优化 Room 性能的起点。下面我用一张图把整个流程串起来,方便你记忆。

下面这张图展示了从输入 SQL 到执行查询计划的完整链路,以及索引如何参与决策:

查询计划分析流程 SQL 查询语句 是否有索引? SCAN TABLE 全表扫描(慢) SEARCH USING INDEX 索引查找(快) 生成查询计划 EXPLAIN QUERY PLAN 验证 根据结果调整索引或SQL 说明:索引缺失 → 全表扫描 索引命中 → 快速定位

记住这个流程,以后你写 Room 的 @Query 时,心里就有底了。遇到慢查询,别急着堆索引,先跑一遍 EXPLAIN QUERY PLAN,看看数据库到底在干什么,再对症下药。