在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、前置思考

1.1 关系型数据库"能用"和"好用"的鸿沟

很多开发者的 SQLite 用法停留在"能跑"层面:建表不加索引、所有写操作直接 executeSql、遇到并发就全表锁、数据量一上去查询从毫秒变秒级。RDB 的性能问题 90% 出在索引缺失、锁竞争、事务滥用三个点上。

典型症状:

症状1: 用户搜索列表要 2 秒
   → 每条查询都是全表扫描 (WHERE name LIKE '%xx%' 无索引)

症状2: 写入 100 条数据耗时 8 秒
   → 每条 insert 都开独立事务 + 每次 fsync

症状3: 多线程并发读写直接报 database is locked
   → 没有用 WAL 模式, 读写互斥

症状4: 数据库文件膨胀到 500MB
   → 大量 UPDATE/DELETE 产生页碎片, 从未 VACUUM

1.2 本文路线

本文深入 SQLite 引擎的索引结构、锁模型、事务隔离、WAL 机制,并给出鸿蒙 RelationalStore 上的企业级优化方案。

二、核心原理

2.1 SQLite 存储引擎结构

┌──────────────────────────────────────────────┐
│               SQL 解析器                        │
├──────────────────────────────────────────────┤
│              查询优化器 (Query Planner)         │
│   全表扫描 vs 索引扫描 vs 覆盖索引 决策          │
├──────────────────────────────────────────────┤
│               B-Tree 存储层                     │
│    表数据页 / 索引页 / 溢出页                   │
├──────────────────────────────────────────────┤
│               Pager 页管理                     │
│    页缓存 / WAL 日志 / 锁管理                   │
├──────────────────────────────────────────────┤
│               OS 文件层 (mmap / 常规IO)         │
└──────────────────────────────────────────────┘

2.2 B-Tree 索引原理

  • 表数据与索引都存为 B-Tree;
  • 索引扫描时间复杂度 O(logN),全表扫描 O(N);
  • 覆盖索引(索引列即查询列)可免回表,性能再翻倍;
  • 复合索引遵循最左前缀原则(a, b, c) 索引可加速 aa,ba,b,c 查询,但加速不了 bc 单独查询。
无索引: SELECT * FROM user WHERE name = '张三'
  → 全表扫描 N 行, 每行比较 name

有索引: CREATE INDEX idx_user_name ON user(name)
  → B-Tree 查找 O(logN) 定位行id → 回表取整行

覆盖索引: CREATE INDEX idx_user_name_id ON user(name, id)
  → SELECT id FROM user WHERE name = '张三'
  → 索引页直接给出 id, 免回表

2.3 锁模型:共享锁与排他锁

SQLite 有五种锁状态,从宽松到严格:

锁状态 允许操作 冲突情况
UNLOCKED -
SHARED 多个连接并发读 与 RESERVED 冲突
RESERVED 预留写(可继续读) 只允许一个
PENDING 等待写锁 阻塞新 SHARED
EXCLUSIVE 独占写 全部阻塞

锁升级路径:UNLOCKED → SHARED → RESERVED → PENDING → EXCLUSIVE。写事务先从 SHARED 升级 RESERVED,此时其他读不受影响,直到提交前升级 EXCLUSIVE 真正落盘。

2.4 事务隔离级别

SQLite 支持两种主要模式:

模式 隔离级别 行为
默认 (rollback journal) 可串行化(读共享) 写时复制原页到 journal,读写互斥
WAL 模式 快照隔离(SNAPSHOT) 读旧版本快照,写新 WAL,读写并行

WAL 模式的核心收益:读写并发不互斥——读事务读取 WAL 中已提交但未合并的快照,写事务追加到 WAL 尾部。

2.5 WAL 预写日志机制

普通模式 (rollback journal):
  UPDATE user SET name='李四' WHERE id=1
  ① 把旧页(id=1所在页) 复制到 journal
  ② 修改数据库页
  ③ 提交: 删除 journal
  → 写操作全程持有锁, 读被阻塞

WAL 模式:
  ① 修改追加到 wal 文件尾部 (顺序IO)
  ② 返回成功 (无需 fsync 主库)
  ③ checkpoint 时机: 将 WAL 合并回主库
  ④ wal 文件达到阈值或空闲时自动 checkpoint
  → 读走主库+WAL 快照, 读写完全并行

三、源码/API 深度解析

3.1 鸿蒙 RelationalStore 开启 WAL 与索引

import { relationalStore } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

async function createOptimizedDb(context: common.Context): Promise<relationalStore.RdbStore> {
  const config: relationalStore.StoreConfig = {
    name: 'app.db',
    securityLevel: relationalStore.SecurityLevel.S1
  };
  const rdb = await relationalStore.getRdbStore(context, config);

  // 建表
  await rdb.executeSql(`CREATE TABLE IF NOT EXISTS orders (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    amount REAL NOT NULL,
    status TEXT NOT NULL DEFAULT 'pending',
    created_at INTEGER NOT NULL
  )`);

  // 关键索引
  await rdb.executeSql('CREATE INDEX IF NOT EXISTS idx_orders_user ON orders(user_id)');
  await rdb.executeSql('CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status)');
  await rdb.executeSql('CREATE INDEX IF NOT EXISTS idx_orders_created ON orders(created_at)');
  // 复合索引: 用户订单按时间倒序高频查询
  await rdb.executeSql('CREATE INDEX IF NOT EXISTS idx_orders_user_time ON orders(user_id, created_at DESC)');

  return rdb;
}

3.2 事务化批量写入

// 错误示范: 每条 insert 独立事务 → 1万条 = 1万次 fsync
async function slowBatch(rdb: relationalStore.RdbStore, n: number): Promise<void> {
  for (let i = 0; i < n; i++) {
    await rdb.insert('orders', { user_id: i, amount: 9.9, created_at: Date.now() }
      as relationalStore.ValuesBucket);
  }
}

// 正确示范: 单事务包裹批量写入
async function fastBatch(rdb: relationalStore.RdbStore, n: number): Promise<void> {
  rdb.beginTransaction();
  try {
    for (let i = 0; i < n; i++) {
      await rdb.insert('orders', { user_id: i, amount: 9.9, created_at: Date.now() }
        as relationalStore.ValuesBucket);
    }
    rdb.commit();
  } catch (e) {
    rdb.rollBack();
    throw e;
  }
}

实测对比:1 万条写入,独立事务约 8s,单事务包裹约 0.4s,提升 20 倍。

3.3 查询优化器决策

// 用 EXPLAIN 看执行计划
const rs = await rdb.querySql('EXPLAIN QUERY PLAN ' +
  'SELECT * FROM orders WHERE user_id = 100 AND created_at > 1700000000000');
// 期望输出: 走 idx_orders_user_time 索引 (SEARCH orders USING INDEX)
// 若输出 SCAN orders (全表扫描) → 索引未命中

索引命中判断:

  • WHERE 列在索引最左前缀中 → 索引扫描;
  • 前导列上有 %xx% LIKE 或函数包裹 → 索引失效;
  • ORDER BY 与索引顺序一致 → 免排序。

四、企业级实战落地

4.1 连接池与单例

RelationalStore 一个 Store 实例内部有连接管理,应用层保持单例,避免重复 open 造成连接数膨胀:

export class RdbManager {
  private static store: relationalStore.RdbStore | null = null;

  static async get(context: common.Context): Promise<relationalStore.RdbStore> {
    if (!RdbManager.store) {
      RdbManager.store = await createOptimizedDb(context);
    }
    return RdbManager.store;
  }
}

4.2 读写分离思路(应用层)

单机 SQLite 无法物理读写分离,但可以:

写路径: 直接写 RDB (事务包裹)
读路径: 高频读走内存缓存 (LruCache) → 缓存未命中再查 RDB
       + 订阅 on('dataChange') 失效缓存

4.3 分页查询优化

// 错误: LIMIT OFFSET 深翻页, OFFSET 越大越慢 (需跳过前 N 行)
SELECT * FROM orders ORDER BY id DESC LIMIT 20 OFFSET 10000;

// 正确: 游标分页 (记住上页最后 id)
SELECT * FROM orders WHERE id < :lastId ORDER BY id DESC LIMIT 20;

4.4 性能基线验证

场景 优化前 优化后 手段
单条查询(user_id) 全表 120ms 索引 0.8ms 加索引
1万条批量写入 8s 0.4s 单事务
读写并发 报错 locked 正常并行 WAL 模式
深翻页第 1000 页 1.5s 12ms 游标分页

五、问题排查与性能优化

现象 原因 解决
database is locked 并发写冲突 默认 rollback journal 读写互斥 WAL 模式 + 重试
查询越来越慢 数据量增长后劣化 无索引/索引失效 加索引 + EXPLAIN 验证
写入巨慢 批量插入卡顿 独立事务 单事务包裹
数据库膨胀 文件巨大 UPDATE/DELETE 页碎片 VACUUM
复合索引无效 查询还是全表 未遵循最左前缀 调整索引列顺序
回表开销大 索引查询仍慢 查询列不在索引 覆盖索引
死锁 并发写互相等待 锁升级冲突 串行化写 + 重试机制

5.1 写锁冲突重试

async function executeWithRetry(fn: () => Promise<void>, retries = 3): Promise<void> {
  for (let i = 0; i < retries; i++) {
    try { return await fn(); }
    catch (e) {
      const code = (e as { code?: number }).code;
      if (code === 5 || code === 6) {  // SQLITE_BUSY / SQLITE_LOCKED
        await sleep(50 * (i + 1));
        continue;
      }
      throw e;
    }
  }
  throw new Error('数据库锁冲突, 重试耗尽');
}

5.2 索引维护纪律

  • 索引不是越多越好:每个索引增加写开销(INSERT/UPDATE 都要维护索引树);
  • 高频查询列建索引,低频大表慎建;
  • 复合索引列序:等值列在前,范围列在后
  • 定期用 EXPLAIN QUERY PLAN 审查慢查询是否命中索引。

六、高阶总结与最佳实践

  1. 索引是查询性能的第一杠杆:WHERE/JOIN/ORDER BY 列建索引,用 EXPLAIN 验证命中。
  2. 事务是写性能的第一杠杆:批量写单事务包裹,避免逐条 fsync。
  3. WAL 是并发性能的第一杠杆:读写并行,解决 database is locked。
  4. 游标分页替代 OFFSET:深翻页性能差 100 倍。
  5. 连接单例 + 缓存层:应用层保持 Store 单例,高频读走缓存。

一句话记住:查询优化看索引,写入优化看事务,并发优化看 WAL,翻页优化看游标。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐