第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:增量加载,按需展示。
// 反例:全量加载
@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、内存开销。下图展示了原则之间的作用链路:

Room 查询优化核心原则作用链路 ① 减少访问次数 ② 避免 N+1 ③ 合理索引 ④ 合适数据类型 ⑤ 控制返回数据量 最终收益 减少 Binder 往返 · 降低 CPU 序列化 · 缩小内存占用 提升列表滑动流畅度 · 延长应用后台存活时间

2.8 常见错误与调优检查清单

  • 错误1:在 doInBackground 或 RxJava 的 subscribeOn 中频繁调用 DAO 方法,未合并事务。
  • 错误2:给 LIKE '%keyword%' 的列建索引——SQLite 不支持前缀模糊索引。
  • 错误3:使用 String 存储时间,导致 ORDER BY 按字典序而非时间序排序。
  • 错误4:未使用 @Transaction 包装多表关联插入,导致重复创建事务。
快速自查清单:
□ 所有 DAO 写入操作是否已包裹在 @Transaction 中?
□ 列表页查询是否避免了 N+1?可用 @Relation 或 @Embedded 代替?
□ WHERE/JOIN/ORDER BY 字段是否已建索引?
□ 布尔、枚举、时间是否使用了最小存储类型?
□ 大量数据查询是否加了 LIMIT 或使用了 Paging 3?
□ 是否在 Profiler 中确认了查询次数和扫描行数?