第一章:Room数据库与SQLite索引基础

1.1 索引的核心作用

索引是数据库查询加速的基石。在没有索引时,SQLite 会对目标表执行全表扫描,逐行检查是否满足 WHERE 条件。对于百万级数据表,一次全表扫描可能消耗数百毫秒甚至数秒。索引相当于为数据列建立了一本「目录」:通过 B+ 树结构将数据有序组织,查询时只需要沿着树根向下查找,复杂度从 O(n) 降低到 O(log n)。

核心收益:索引减少磁盘 I/O 次数,降低 CPU 计算开销,显著提升查询响应速度。

1.2 B+ 树索引结构

SQLite 索引默认使用 B+ 树。所有数据存储在叶子节点,内部节点仅存储键值和指向子节点的指针。叶子节点之间通过双向链表链接,支持高效的范围查询(BETWEEN、>、<)。整棵树高度通常为 2~4 层,即使千万级数据也只需几次磁盘读取即可定位目标行。

B+ 树索引结构示意图(3 层) 50 根节点 (内部) 20, 35 内部节点 68, 82 内部节点 95 内部节点 1, 7, 15, 20 叶子节点 25, 30, 35, 42 叶子节点 50, 55, 68, 75 叶子节点 82, 88, 95, 99 叶子节点 ... 叶子节点包含实际数据行指针,内部节点仅作路由。树高 = 3,查找任意键只需 3 次 I/O。
关键理解:B+ 树的“扇出”特性使树高度非常低。一个 3 层 B+ 树足以支撑百万级数据的高效查找。

1.3 Room 中 @Index 注解的用法

Room 通过 @Entity 的 indices 属性声明索引,支持单列索引、复合索引和唯一索引。

1.3.1 实体定义示例

@Entity(tableName = "users",
        indices = {
            @Index(value = {"email"}, unique = true),     // 唯一索引
            @Index(value = {"last_name", "first_name"}),  // 复合索引(非唯一)
            @Index(value = {"created_at"})                // 单列索引
        })
public class User {
    @PrimaryKey(autoGenerate = true)
    public long id;

    @NonNull
    public String email;

    public String firstName;
    public String lastName;
    public long createdAt;
}

1.3.2 索引命名与自定义

Room 会自动生成索引名称(格式:index_表名_列名),也可以通过 name 属性自定义:

@Index(name = "idx_user_email", value = {"email"}, unique = true)

1.4 索引创建时机与 SQLite 底层语句

Room 在编译时根据 @Index 注解生成相应的 CREATE INDEX SQL 语句。迁移时如果不指定版本,Room 会在首次创建表时一并创建索引。手动迁移可以控制索引的添加或删除。

注意事项:索引并非越多越好。每个索引占用额外的磁盘空间,并会拖慢 INSERT、UPDATE、DELETE 操作的性能(因为需要同时维护索引树)。只有当查询 WHERE、JOIN、ORDER BY 频繁使用某列时,才应为该列创建索引。

SQLite 创建索引的典型语句:

-- 单列索引
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);

-- 复合索引(列顺序影响查询利用)
CREATE INDEX IF NOT EXISTS idx_users_name ON users(last_name, first_name);

-- 唯一索引(同时具有唯一约束)
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_unique ON users(email);

1.5 查询执行计划分析(EXPLAIN QUERY PLAN)

SQLite 提供 EXPLAIN QUERY PLAN 命令,展示 SQL 优化器选择的执行策略。通过分析可以判断索引是否被命中,以及扫描方式(全表扫描 vs 索引扫描)。

示例:带索引 vs 不带索引的查询分析

-- 无索引时,对 email 字段查询
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'test@example.com';
-- 输出:SCAN TABLE users (全表扫描)
-- 耗时:约 120 ms (100 万行)

-- 添加索引后,再次分析
CREATE INDEX idx_email ON users(email);
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = 'test@example.com';
-- 输出:SEARCH TABLE users USING INDEX idx_email (email=?) (索引搜索)
-- 耗时:约 0.3 ms (100 万行)
分析要领:观察执行计划中是否出现 "USING INDEX" 字样。若出现 "SCAN TABLE" 且表数据量大,说明缺少合适的索引。在 Room 中可以通过 @RawQuery 或 SupportSQLiteDatabase 直接执行 EXPLAIN QUERY PLAN。

1.6 性能对比数据表格

以下数据基于 100 万行用户表的随机点查询测试(模拟 Room 中 @Query 条件筛选):

场景 查询方式 平均耗时 (ms) 扫描行数 执行计划关键词
无索引 WHERE email = ? ~115 1,000,000 SCAN TABLE
单列索引 WHERE email = ? ~0.3 1 USING INDEX
复合索引(前缀列) WHERE last_name = ? ~0.4 ~100 USING INDEX
复合索引(非前缀列) WHERE first_name = ? ~115 1,000,000 SCAN TABLE

从表格可知:索引使查询速度提升约 300~400 倍。复合索引必须遵循最左前缀原则:查询条件必须包含索引定义中最左侧的列,才能有效利用该索引。

实践建议:在 Room 中频繁执行的查询(如登录验证、列表分页过滤)务必添加合适索引。利用 Room 的 @DatabaseView 或 @RawQuery 执行 EXPLAIN QUERY PLAN 验证索引效果。

1.7 本章小结

  • 索引是数据库查询加速的核心机制,底层基于 B+ 树实现。
  • Room 通过 @Index 注解在实体类中声明索引,支持单列、复合、唯一索引。
  • 索引在首次创建表时自动生成,也可通过迁移脚本手动管理。
  • 使用 EXPLAIN QUERY PLAN 分析查询是否命中索引,避免全表扫描。
  • 索引并非免费午餐:占用空间且降低写入性能,需权衡利弊。