3. Room 中索引注解:@Index 用法、unique 索引、索引命名、在 @Entity 中定义多索引

3.1 原理回顾:索引在 Room 中的角色

Room 使用 @Index 注解在编译时生成 SQLite CREATE INDEX 语句。索引可以显著加速 WHERE、JOIN、ORDER BY 和 GROUP BY 查询,但会牺牲写入性能并增加存储空间。Room 不支持在运行时动态创建索引,所有索引必须在 @Entity 中静态声明。

核心原则:索引应只为高频查询列创建。复合索引的列顺序必须匹配查询条件中最左前缀。

3.2 @Index 注解基础用法

@Index 是 @Entity 的 indices 属性中的一个元素,用于定义一个索引。基本语法:

@Entity(indices = {
    @Index(value = "user_name")
})
data class User(
    @PrimaryKey val uid: Long,
    val user_name: String,
    val email: String
)

以上代码在编译后生成:CREATE INDEX index_user_user_name ON User (user_name)。

3.3 为索引命名

默认索引名格式:index_{table}_{column}。若要自定义名称,使用 name 属性:

@Entity(indices = {
    @Index(value = "user_name", name = "idx_username")
})
data class User(
    @PrimaryKey val uid: Long,
    val user_name: String,
    val email: String
)

自定义命名规则建议:

命名模式示例说明
idx_{表名}_{列名}idx_user_username最常用,表名 + 列名
uq_{表名}_{列名}uq_user_email唯一索引专用前缀
idx_{表名}_{列1}_{列2}idx_user_dept_role复合索引用列名缩写

3.4 唯一索引(unique)

设置 unique = true 可创建唯一索引,同时 SQLite 会隐式添加 UNIQUE 约束:

@Entity(indices = {
    @Index(value = "email", unique = true)
})
data class User(
    @PrimaryKey val uid: Long,
    val email: String
)

生成的 SQL:CREATE UNIQUE INDEX index_user_email ON User (email)。唯一索引除了加速查询,还能防止重复数据插入。注意:NULL 值在 SQLite 唯一索引中允许多个存在(除非使用 UNIQUE 约束 + NULLS NOT DISTINCT),Room 默认不处理此细节。

常见错误:在声明唯一索引时同时使用 @ColumnInfo(index = true) 会导致重复索引,Room 编译器会给出警告。应统一使用 @Index 管理。

3.5 在 @Entity 中定义多个索引

indices 属性接受数组,可同时声明多个索引:

@Entity(
    tableName = "employees",
    indices = {
        @Index(value = "last_name", name = "idx_emp_lname"),
        @Index(value = "department_id", name = "idx_emp_dept"),
        @Index(
            value = ["last_name", "first_name"],
            unique = true,
            name = "uq_emp_fullname"
        ),
        @Index(
            value = ["department_id", "hire_date"],
            name = "idx_emp_dept_hiredate"
        )
    }
)
data class Employee(
    @PrimaryKey val emp_id: Int,
    @ColumnInfo(name = "last_name") val lastName: String,
    @ColumnInfo(name = "first_name") val firstName: String,
    @ColumnInfo(name = "department_id") val deptId: Int,
    @ColumnInfo(name = "hire_date") val hireDate: Long
)

上述定义在数据库中创建 4 个独立的索引:

  1. 普通索引 idx_emp_lname(单列)
  2. 普通索引 idx_emp_dept(单列)
  3. 唯一复合索引 uq_emp_fullname(两列)
  4. 复合索引 idx_emp_dept_hiredate(两列)

3.6 复合索引与列顺序

复合索引的值顺序至关重要。例如 value = ["last_name", "first_name"] 能加速 WHERE last_name = ? 和 WHERE last_name = ? AND first_name = ?,但无法加速只查询 first_name 的查询。在定义多列索引时,应将选择性最高的列或最常用于过滤的列放在前面。

3.7 调试与验证索引

在 Room 编译后,可以通过以下方式确认索引是否生成正确:

  • 查看生成的 java 或 kt 源文件(build 目录下的 generated/source/room),搜索 CREATE INDEX。
  • 在 Room.inMemoryDatabaseBuilder 上加 .enableMultiInstanceInvalidation() 后运行 SQLite 命令:PRAGMA index_list('employees'); 和 PRAGMA index_info('idx_emp_lname');。
  • 使用 AS Profiler 的 Database Inspector 查看表结构和索引。
性能优化建议:
  • 避免为同一列创建冗余索引(如单列索引 + 复合索引包含该列)。
  • 唯一索引可替代手动唯一约束检查,减少应用层逻辑。
  • 复合索引的列数不宜超过 5 列,否则维护成本过高。
  • 频繁增删改的表应减少索引数量,因为索引维护会拖慢写入。

3.8 常见错误与规避

错误场景表现解决方案
索引名冲突编译报错 Duplicate index name为每个索引显式指定不同的 name
唯一索引字段允许 NULL 重复数据插入时不报错,但语义错误配合 @NonNull 或校验逻辑
复合索引列顺序不合理查询未走索引通过 EXPLAIN QUERY PLAN 分析,调整列序
索引过多导致插入性能骤降写入慢,ANR按实际查询模式精简索引,使用 CONCURRENT 事务批次

3.9 多索引定义流程图(SVG)

下图展示了在 @Entity 中定义多个索引的声明到 SQL 映射关系:

多索引定义流程 @Entity( tableName = "employees" ) @PrimaryKey emp_id, last_name, first_name, ... @Index("last_name") name="idx_emp_lname" @Index("dept_id") name="idx_emp_dept" @Index (unique) [last_name, first_name] @Index [dept_id, hire_date] 编译后生成的 SQL 索引语句 CREATE INDEX idx_emp_lname ON employees(last_name); CREATE INDEX idx_emp_dept ON employees(department_id); CREATE UNIQUE INDEX uq_emp_fullname ON employees(last_name, first_name); CREATE INDEX idx_emp_dept_hiredate ON employees(department_id, hire_date);

3.10 部署与稳定性考量

索引定义在数据库版本升级时自动应用。若新版本增加索引,Room 会在 Migration 中执行 CREATE INDEX(前提是迁移脚本手动包含索引创建)。如果使用自动迁移(AutoMigration),Room 会根据 @Index 变化自动生成迁移语句。注意:自动迁移仅支持新增索引,不支持删除或修改索引。删除索引需要手动编写 Migration,避免破坏旧版本用户的数据。

稳定性最佳实践:
  • 在 @Entity 中显式命名所有索引,避免自动命名导致的迁移混淆。
  • 唯一索引应在设计阶段确定,避免后期添加导致已有重复数据报错。
  • 使用 @Database(entities = [...] , version = 2) 时,务必在 MIGRATION_1_2 中包含 CREATE INDEX IF NOT EXISTS 以保证幂等。