第2章 Room查询优化核心原则
Room 查询性能优化的本质是减少数据库引擎的无效工作。本章总结的五条核心原则,每条都会直接影响查询延迟、内存占用和 CPU 开销。理解这些原则,是后续所有调优技术的基础。
2.1 核心原则总览
| 原则 | 目标 | 典型反例 |
|---|---|---|
| 减少数据库访问次数 | 降低 Binder 往返 + 事务开销 | 循环中逐条 insert |
| 避免 N+1 查询 | 消除笛卡尔积或重复查询 | 先查列表再逐条查关联 |
| 合理使用索引 | 加速 WHERE/JOIN/ORDER BY | 未给查询字段建索引 |
| 选择合适的数据类型 | 减少存储空间和比较开销 | 用 String 存布尔值 |
| 控制返回数据量 | 减少序列化 + 内存拷贝 | SELECT * 返回全部列 |
2.2 减少数据库访问次数
每次 Room 数据库操作都会经过 Binder 传输到 SQLite 进程,频繁的小事务使性能瓶颈从 SQL 执行转向 I/O 和进程间通信。
优化建议:使用
@Transaction 注解将多条写入合并为单次事务;查询时尽量用一次 SQL 完成关联,而非多次逐条查。
// 反例:循环内逐条插入
for (User user : userList) {
userDao.insert(user); // 每次独立事务
}
// 优化:单事务批量插入
@Transaction
void insertAll(List<User> users) {
for (User user : users) {
userDao.insert(user);
}
}
2.3 避免 N+1 查询
N+1 是指先执行 1 次查询获取 N 条主记录,然后对每条记录再执行一次关联查询,最终产生 N+1 次数据库访问。这会严重拉低列表页加载速度。
典型场景:在 RecyclerView 的 onBindViewHolder 中调用数据库查询获取关联数据。请务必使用 Room 的
@Relation 或 @Embedded 一次加载完整关系。
// 反例:N+1
@Query("SELECT * FROM `Order`")
LiveData<List<Order>> getAllOrders();
// 在 Adapter 中逐条查用户
for (Order o : orders) {
User u = userDao.getUserById(o.userId);
}
// 优化:使用 Relation 一次查询
@Transaction
@Query("SELECT * FROM `Order`")
LiveData<List<OrderWithUser>> getOrdersWithUsers();
验证方法:在 Android Studio Profiler 的 Database Inspector 中观察 SQL 执行次数。N+1 会导致 ORDER BY 等简单页面产生数十条 SQL 语句。
2.4 合理使用索引
索引的本质是以空间换时间。Room 不自动创建除主键外的索引,开发者必须为 WHERE、JOIN、ORDER BY 中频繁使用的字段手动添加索引。
| 索引类型 | 适用场景 | 创建语法 |
|---|---|---|
| 单列索引 | 精确等于、范围查询 | @Index("email") |
| 复合索引 | 多条件过滤 | @Index(value = {"status","create_time"}) |
| 唯一索引 | 唯一约束加速 | @Index(unique = true) |
@Entity(indices = {@Index("email"), @Index(value = {"status", "create_time"})})
public class User {
@PrimaryKey(autoGenerate = true)
public long id;
public String email;
public int status;
public long createTime;
}
索引代价:写入时需同步维护索引树,对于高频写入表,不应为所有字段建索引。建议先通过
EXPLAIN QUERY PLAN 检查查询计划,再决定索引列。
2.5 选择合适的数据类型
SQLite 支持 INTEGER、REAL、TEXT、BLOB、NUMERIC 五种存储类。错误的数据类型会导致存储膨胀、序列化开销及索引效率下降。
| 业务含义 | 推荐类型 | 避免类型 | 原因 |
|---|---|---|---|
| 布尔值 | INTEGER (0/1) | TEXT (true/false) | 整数比较快且占 1 字节 |
| 枚举 | INTEGER (ordinal) | TEXT (name) | 字符串长度不固定,索引更大 |
| 时间戳 | INTEGER (ms) | TEXT ("yyyy-MM-dd") | 整数范围查询更快 |
| 小数字 | REAL / INTEGER (分) | TEXT | 避免浮点精度问题 |
// 反例:String 存储布尔
@Entity
public class Setting {
@ColumnInfo(name = "is_enabled")
public String isEnabled; // "true"/"false" 占 4~5 字节
}
// 优化:INTEGER 存储布尔
@Entity
public class Setting {
@ColumnInfo(name = "is_enabled")
public boolean isEnabled; // Room 自动映射为 0/1
}
2.6 控制返回数据量
Room 的 @Query 返回全部匹配行时,若表行数很大,会触发大量 Java 对象创建和垃圾回收。应通过分页、限定列、限制行数来“瘦身”结果集。
三种策略:
1. 投影查询:只查询需要的列,减少序列化字段;
2. LIMIT + OFFSET:分页避免一次性加载万级数据;
3. 使用 Flow / LiveData 并配合 Paging 3:增量加载,按需展示。
1. 投影查询:只查询需要的列,减少序列化字段;
2. LIMIT + OFFSET:分页避免一次性加载万级数据;
3. 使用 Flow / LiveData 并配合 Paging 3:增量加载,按需展示。
// 反例:全量加载
@Query("SELECT * FROM `Article`")
List<Article> getAllArticles();
// 优化:分页 + 投影
@Query("SELECT id, title, summary FROM `Article` ORDER BY id ASC LIMIT :limit OFFSET :offset")
List<ArticleSummary> getArticlesPage(int limit, int offset);
2.7 核心原则关系图
五条原则并非孤立,它们共同影响数据库整体的 I/O、CPU、内存开销。下图展示了原则之间的作用链路:
2.8 常见错误与调优检查清单
- 错误1:在
doInBackground或 RxJava 的subscribeOn中频繁调用 DAO 方法,未合并事务。 - 错误2:给
LIKE '%keyword%'的列建索引——SQLite 不支持前缀模糊索引。 - 错误3:使用
String存储时间,导致ORDER BY按字典序而非时间序排序。 - 错误4:未使用
@Transaction包装多表关联插入,导致重复创建事务。
快速自查清单:
□ 所有 DAO 写入操作是否已包裹在
□ 列表页查询是否避免了 N+1?可用
□ WHERE/JOIN/ORDER BY 字段是否已建索引?
□ 布尔、枚举、时间是否使用了最小存储类型?
□ 大量数据查询是否加了 LIMIT 或使用了 Paging 3?
□ 是否在 Profiler 中确认了查询次数和扫描行数?
□ 所有 DAO 写入操作是否已包裹在
@Transaction 中?□ 列表页查询是否避免了 N+1?可用
@Relation 或 @Embedded 代替?□ WHERE/JOIN/ORDER BY 字段是否已建索引?
□ 布尔、枚举、时间是否使用了最小存储类型?
□ 大量数据查询是否加了 LIMIT 或使用了 Paging 3?
□ 是否在 Profiler 中确认了查询次数和扫描行数?