第一章:Room数据库与SQLite索引基础
1.1 索引的核心作用
索引是数据库查询加速的基石。在没有索引时,SQLite 会对目标表执行全表扫描,逐行检查是否满足 WHERE 条件。对于百万级数据表,一次全表扫描可能消耗数百毫秒甚至数秒。索引相当于为数据列建立了一本「目录」:通过 B+ 树结构将数据有序组织,查询时只需要沿着树根向下查找,复杂度从 O(n) 降低到 O(log n)。
核心收益:索引减少磁盘 I/O 次数,降低 CPU 计算开销,显著提升查询响应速度。
1.2 B+ 树索引结构
SQLite 索引默认使用 B+ 树。所有数据存储在叶子节点,内部节点仅存储键值和指向子节点的指针。叶子节点之间通过双向链表链接,支持高效的范围查询(BETWEEN、>、<)。整棵树高度通常为 2~4 层,即使千万级数据也只需几次磁盘读取即可定位目标行。
关键理解:B+ 树的“扇出”特性使树高度非常低。一个 3 层 B+ 树足以支撑百万级数据的高效查找。
1.3 Room 中 @Index 注解的用法
Room 通过 @Entity 的 indices 属性声明索引,支持单列索引、复合索引和唯一索引。
1.3.1 实体定义示例
@Entity(tableName = "users",
indices = {
@Index(value = {"email"}, unique = true), // 唯一索引
@Index(value = {"last_name", "first_name"}), // 复合索引(非唯一)
@Index(value = {"created_at"}) // 单列索引
})
public class User {
@PrimaryKey(autoGenerate = true)
public long id;
@NonNull
public String email;
public String firstName;
public String lastName;
public long createdAt;
}
1.3.2 索引命名与自定义
Room 会自动生成索引名称(格式:index_表名_列名),也可以通过 name 属性自定义:
@Index(name = "idx_user_email", value = {"email"}, unique = true)
1.4 索引创建时机与 SQLite 底层语句
Room 在编译时根据 @Index 注解生成相应的 CREATE INDEX SQL 语句。迁移时如果不指定版本,Room 会在首次创建表时一并创建索引。手动迁移可以控制索引的添加或删除。
注意事项:索引并非越多越好。每个索引占用额外的磁盘空间,并会拖慢 INSERT、UPDATE、DELETE 操作的性能(因为需要同时维护索引树)。只有当查询 WHERE、JOIN、ORDER BY 频繁使用某列时,才应为该列创建索引。
SQLite 创建索引的典型语句:
-- 单列索引
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- 复合索引(列顺序影响查询利用)
CREATE INDEX IF NOT EXISTS idx_users_name ON users(last_name, first_name);
-- 唯一索引(同时具有唯一约束)
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_unique ON users(email);
1.5 查询执行计划分析(EXPLAIN QUERY PLAN)
SQLite 提供 EXPLAIN QUERY PLAN 命令,展示 SQL 优化器选择的执行策略。通过分析可以判断索引是否被命中,以及扫描方式(全表扫描 vs 索引扫描)。
示例:带索引 vs 不带索引的查询分析
-- 无索引时,对 email 字段查询
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'test@example.com';
-- 输出:SCAN TABLE users (全表扫描)
-- 耗时:约 120 ms (100 万行)
-- 添加索引后,再次分析
CREATE INDEX idx_email ON users(email);
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'test@example.com';
-- 输出:SEARCH TABLE users USING INDEX idx_email (email=?) (索引搜索)
-- 耗时:约 0.3 ms (100 万行)
分析要领:观察执行计划中是否出现 "USING INDEX" 字样。若出现 "SCAN TABLE" 且表数据量大,说明缺少合适的索引。在 Room 中可以通过
@RawQuery 或 SupportSQLiteDatabase 直接执行 EXPLAIN QUERY PLAN。
1.6 性能对比数据表格
以下数据基于 100 万行用户表的随机点查询测试(模拟 Room 中 @Query 条件筛选):
| 场景 | 查询方式 | 平均耗时 (ms) | 扫描行数 | 执行计划关键词 |
|---|---|---|---|---|
| 无索引 | WHERE email = ? |
~115 | 1,000,000 | SCAN TABLE |
| 单列索引 | WHERE email = ? |
~0.3 | 1 | USING INDEX |
| 复合索引(前缀列) | WHERE last_name = ? |
~0.4 | ~100 | USING INDEX |
| 复合索引(非前缀列) | WHERE first_name = ? |
~115 | 1,000,000 | SCAN TABLE |
从表格可知:索引使查询速度提升约 300~400 倍。复合索引必须遵循最左前缀原则:查询条件必须包含索引定义中最左侧的列,才能有效利用该索引。
实践建议:在 Room 中频繁执行的查询(如登录验证、列表分页过滤)务必添加合适索引。利用 Room 的
@DatabaseView 或 @RawQuery 执行 EXPLAIN QUERY PLAN 验证索引效果。
1.7 本章小结
- 索引是数据库查询加速的核心机制,底层基于 B+ 树实现。
- Room 通过
@Index注解在实体类中声明索引,支持单列、复合、唯一索引。 - 索引在首次创建表时自动生成,也可通过迁移脚本手动管理。
- 使用
EXPLAIN QUERY PLAN分析查询是否命中索引,避免全表扫描。 - 索引并非免费午餐:占用空间且降低写入性能,需权衡利弊。