3、查询性能基础:索引的作用、查询计划、EXPLAIN QUERY PLAN
很多朋友一上来就问:Room 怎么优化?加索引就够了?嗯,没那么简单。但离开索引,后面所有的优化手段都像空中楼阁。我先带你把“索引”这颗地基打牢。
这一章,我不打算给你堆概念。咱们直接从数据库怎么干活的角度,聊聊索引到底帮了什么忙,以及你怎么用 EXPLAIN QUERY PLAN 来“看”它干活。相信我,搞懂这个,后面你调优就顺手多了。
索引为什么能加速查询?
你想想看,SQLite 本身是个小型文件数据库。没有索引的时候,一条 WHERE 条件,它就把整张表从头到尾扫一遍——这叫全表扫描。我见过一个日志表,数据量才 10 万行,一个不带索引的查询跑了 3 秒多。用户直接骂娘了。
索引的本质,就是一个按列排好的有序结构。最常见的是 B-Tree 索引。查的时候,它不扫描全表,而是走树查找——复杂度从 O(n) 降到 O(log n)。说白了,就像你从一堆乱放的书里找一本, vs 按字母顺序排列的书架上找。哪个快,一目了然。
我在项目里遇到过的一个坑:给所有字段都加了索引,结果插入一条记录花了 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。你甚至可以在单元测试里写一段代码,自动抓取计划并断言它是索引查找而不是全表扫描。这样一跑回归测试,要是有新查询忘加索引,立马就报红。
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 到执行查询计划的完整链路,以及索引如何参与决策:
记住这个流程,以后你写 Room 的 @Query 时,心里就有底了。遇到慢查询,别急着堆索引,先跑一遍 EXPLAIN QUERY PLAN,看看数据库到底在干什么,再对症下药。