鸿蒙关系型数据库高级优化:SQLite索引策略/事务隔离级别/共享锁与排他锁/WAL预写日志底层机制
·



一、前置思考
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)索引可加速a、a,b、a,b,c查询,但加速不了b或c单独查询。
无索引: 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审查慢查询是否命中索引。
六、高阶总结与最佳实践
- 索引是查询性能的第一杠杆:WHERE/JOIN/ORDER BY 列建索引,用 EXPLAIN 验证命中。
- 事务是写性能的第一杠杆:批量写单事务包裹,避免逐条 fsync。
- WAL 是并发性能的第一杠杆:读写并行,解决 database is locked。
- 游标分页替代 OFFSET:深翻页性能差 100 倍。
- 连接单例 + 缓存层:应用层保持 Store 单例,高频读走缓存。
一句话记住:查询优化看索引,写入优化看事务,并发优化看 WAL,翻页优化看游标。
更多推荐




所有评论(0)