1. Room 索引基础:索引的概念、B-tree 结构、单列索引定义、复合索引的作用域

1.1 索引的核心概念

在 Room 中,索引是加速数据库查询的独立数据结构。它通过额外维护一份排序后的键值副本,让查询可以跳过全表扫描,直接定位目标数据行。没有索引时,SQLite 必须逐行遍历整个表(复杂度 O(n));有了合适的索引,查找复杂度可降至 O(log n) 甚至接近 O(1)。

核心收益:索引将随机 I/O 转化为顺序 I/O + 二分查找,对 WHERE、ORDER BY、JOIN 和 GROUP BY 均有加速效果。但索引会占用存储空间,并拖慢写入(INSERT/UPDATE/DELETE)速度——你需要为高频查询场景做取舍。

1.2 B-tree 结构——索引的物理载体

Room 底层使用 SQLite,而 SQLite 的索引几乎全部基于 B-tree(Balanced Tree,平衡树)实现。每个索引本质上是一个独立的 B-tree 文件页组织。B-tree 的特点:

  • 多路平衡:每个内部节点可以拥有多个子节点(通常 50~500 个),树的高度很低(3~5 层),即使表有上亿行。
  • 有序存储:叶节点包含真实的键值和对应的 rowid,且按键值升序排列,支持范围查询(BETWEEN、>、<)。
  • 自平衡:插入和删除后自动分裂或合并节点,保持树的高度平衡,保证最坏情况下的查询效率。
SQLite 的 B-tree 变种:主键索引使用 B+tree(数据仅在叶节点),普通索引使用 B-tree(叶节点存键值+rowid)。但在 Room/SQLite 语境下通常统一称为 B-tree。
B-tree 索引结构示意(3层,5阶) 根节点 [50, 120] 内部节点 [10, 30] 内部节点 [70, 90] 内部节点 [140, 200] 叶:[5,7,10,12] 叶:[20,25,30] 叶:[55,60,70] 叶:[80,85,90] 叶:[140,150,180] B-tree 平衡特性: • 根到每个叶节点的路径长度相同(高度平衡)。 • 每个节点内键值有序,便于二分查找;叶节点之间通过指针可顺序遍历。 • 此处简图展示了 3 层 5 阶结构,实际 SQLite 页大小为 4KB,单个节点可存数百个键。

1.3 在 Room 中定义单列索引

Room 通过注解 @Index 在实体类上声明索引。单列索引是最基本的索引形式,针对单个字段加速等值查询或范围查询。

@Entity(
    tableName = "user",
    indices = [
        @Index(value = ["email"], unique = false)
    ]
)
data class User(
    @PrimaryKey val uid: Long,
    @ColumnInfo(name = "email") val email: String,
    @ColumnInfo(name = "display_name") val displayName: String
)

关键参数说明:

参数 类型 含义 使用频率
valueArray<String>索引包含的列名(可多个组成复合索引)必填
uniqueBoolean是否为唯一索引,默认 false常用
nameString自定义索引名称,默认由 Room 自动生成建议显式指定方便调试
常见错误:在 @Index 的 value 中写了不存在的列名,或者忘了在实体类上加 @Entity(indices = [...] ) 而写在了字段注解上——Room 只在实体级别识别 indices 数组。

1.4 复合索引及其作用域

复合索引(也叫多列索引)指按多个列的顺序建立的索引。其核心规则:最左前缀法则——查询条件只有从索引最左列开始匹配,才能有效利用该索引。

@Entity(
    tableName = "order",
    indices = [
        @Index(value = ["customer_id", "status", "create_time"], name = "idx_customer_status_time")
    ]
)
data class Order(
    @PrimaryKey val orderId: Long,
    val customerId: Long,
    val status: String,
    val createTime: Long
)

复合索引的作用域矩阵(三列索引 (a, b, c) 的扫描能力):

查询条件 是否走索引 扫描范围 说明
WHERE a = ?✅精确单点完全利用第一列
WHERE a = ? AND b = ?✅精确 a,b 范围利用前两列
WHERE a = ? AND b = ? AND c = ?✅精确三列(或 unique 单行)完全利用
WHERE b = ?❌全表扫描跳过最左列,索引无效
WHERE a = ? AND c = ?⚠️ 部分利用通过 a 过滤后,在结果内扫描 c不能直接利用 c 列的排序
设计建议:将区分度最高的列放在最左边(通常为 ID 或高频筛选字段)。复合索引的列数不宜超过 4~5 列——列数越多,索引 B-tree 的宽度越大,写入开销剧增,而查询收益可能边际递减。

1.5 查看索引与查询计划(调试方法)

在 Room 中,无法直接执行 EXPLAIN QUERY PLAN,但可以在数据库浏览器或通过 Room.databaseBuilder.addCallback 打开原生 SQLite 接口使用。典型调试命令:

-- 查看表上的所有索引
PRAGMA index_list('user');

-- 查看索引具体定义
PRAGMA index_info('index_user_email');

-- 查看查询计划,判断是否使用索引
EXPLAIN QUERY PLAN SELECT * FROM user WHERE email = 'test@example.com';

-- 输出结果示例(期望“SEARCH TABLE user USING INDEX index_user_email”)

在 Android Studio 的 App Inspection → Database Inspector 中,可以直接查看索引定义和查询计划,建议开发阶段频繁检查。

1.6 性能与稳定性优化要点

  1. 只建必要索引:每张表的索引数量控制在 5~8 个以内,过多会严重拖慢插入和更新。
  2. 长度限制:对长字符串列(如 TEXT 类型)建索引时,考虑使用部分索引(SQLite 支持 CREATE INDEX ... WHERE),Room 原生不直接支持,但可以通过 @DatabaseView 或原始 SQL 迁移变通。
  3. 唯一索引优先:如果业务上字段值是唯一的(如 email、phone),一定要加 unique = true——SQLite 会自动使用唯一索引做去重校验,速度比应用层检查快数十倍。
  4. 复合索引顺序:将等值查询的列放在前面,范围查询的列放在后面,避免索引跳跃扫描。
  5. 定期分析 ANALYZE:在迁移后或大量数据变更后,执行 PRAGMA optimize 或 ANALYZE 更新索引统计信息,帮助查询优化器选择正确的索引。

1.7 本章小结