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 个独立的索引:
- 普通索引
idx_emp_lname(单列) - 普通索引
idx_emp_dept(单列) - 唯一复合索引
uq_emp_fullname(两列) - 复合索引
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 映射关系:
3.10 部署与稳定性考量
索引定义在数据库版本升级时自动应用。若新版本增加索引,Room 会在 Migration 中执行 CREATE INDEX(前提是迁移脚本手动包含索引创建)。如果使用自动迁移(AutoMigration),Room 会根据 @Index 变化自动生成迁移语句。注意:自动迁移仅支持新增索引,不支持删除或修改索引。删除索引需要手动编写 Migration,避免破坏旧版本用户的数据。
- 在
@Entity中显式命名所有索引,避免自动命名导致的迁移混淆。 - 唯一索引应在设计阶段确定,避免后期添加导致已有重复数据报错。
- 使用
@Database(entities = [...] , version = 2)时,务必在MIGRATION_1_2中包含CREATE INDEX IF NOT EXISTS以保证幂等。