第3章:SQLite数据库优化

数据库优化,说白了就是让App读写数据更快、更稳。我见过太多应用因为数据库卡顿被用户骂,其实很多问题都是可以避免的。今天咱们就聊聊SQLite优化的三个核心方向:事务与WAL模式、索引与查询计划、ORM框架选型。

3.1 事务与WAL模式

先说说事务。很多人觉得事务就是「beginTransaction + commit」这么简单,其实不然。我遇到过一个小伙伴,在循环里逐条插入1000条数据,每条都单独提交事务。结果呢?插入耗时从几十毫秒变成了十几秒。

为什么会这样?因为每次提交事务,SQLite都要做一次磁盘同步操作。这个操作很慢,大概要花10-50毫秒。1000次就是10-50秒,你说慢不慢?

核心原则:批量操作一定要用事务包裹起来。把多次插入合并成一次提交,性能提升立竿见影。

// 错误做法:逐条提交
db.beginTransaction();
try {
    for (Data data : dataList) {
        db.execSQL("INSERT INTO table VALUES (?)", new Object[]{data.value});
        db.setTransactionSuccessful(); // 每次循环都提交
    }
} finally {
    db.endTransaction();
}

// 正确做法:批量提交
db.beginTransaction();
try {
    for (Data data : dataList) {
        db.execSQL("INSERT INTO table VALUES (?)", new Object[]{data.value});
    }
    db.setTransactionSuccessful(); // 只在循环结束后提交一次
} finally {
    db.endTransaction();
}

接下来聊聊WAL模式。默认情况下,SQLite使用回滚日志模式(DELETE模式)。这种模式下,读操作和写操作会互相阻塞。你想想看,用户正在浏览列表,后台突然来了一条写入,界面就卡住了——这体验能好吗?

WAL模式(Write-Ahead Logging)解决了这个问题。它把写入操作先记在日志文件里,读操作直接从原数据库文件读取。读写互不干扰,并发性能大幅提升。

我的建议:新项目直接启用WAL模式。老项目迁移时注意,WAL模式下数据库文件会多一个-wal和-shm文件,备份时别忘了。

// 启用WAL模式
db.execSQL("PRAGMA journal_mode=WAL;");

// 或者通过SQLiteOpenHelper
public class MyDatabaseHelper extends SQLiteOpenHelper {
    @Override
    public void onConfigure(SQLiteDatabase db) {
        super.onConfigure(db);
        db.setJournalMode(SQLiteDatabase.JOURNAL_MODE_WAL);
    }
}

WAL模式还有一个好处:写入性能更好。因为写入操作是追加到日志文件末尾,不需要修改原文件。我做过测试,在WAL模式下,批量插入性能提升了30%-50%。

注意:WAL模式在Android 9及以上版本默认开启。但如果你要兼容低版本,建议手动设置。另外,WAL模式下的数据库文件不能直接复制使用,需要先执行checkpoint。

3.2 索引优化与查询计划分析

索引这东西,用好了是神器,用不好就是累赘。我见过有人给每个字段都建了索引,结果插入一条数据要等半天。为什么?因为索引本身也要维护,每次写入都要更新索引树。

那什么时候该建索引?记住三个场景:

  • WHERE条件字段:经常出现在查询条件里的字段
  • JOIN关联字段:多表关联时用到的字段
  • ORDER BY字段:排序操作频繁的字段

但要注意,索引不是越多越好。我建议一个表最多建5-6个索引,再多就得不偿失了。

怎么判断索引有没有生效?用EXPLAIN QUERY PLAN。这个命令能告诉你SQLite是怎么执行你的查询的。

-- 查看查询计划
EXPLAIN QUERY PLAN SELECT * FROM users WHERE age > 18;

-- 输出示例
-- 0|0|0|SCAN TABLE users
-- 如果走了索引,会显示:
-- 0|0|0|SEARCH TABLE users USING INDEX idx_age (age>?)

看到「SCAN」说明是全表扫描,性能堪忧。看到「SEARCH」说明走了索引,性能不错。

我在项目中遇到过一个问题:明明建了索引,查询还是慢。查了半天才发现,是因为在WHERE条件里对索引字段做了函数操作。比如 WHERE DATE(create_time) = '2024-01-01',这样索引就失效了。正确的做法是写成范围查询:WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'

避坑指南:我曾经因为复合索引的字段顺序搞反,导致索引完全没起作用。记住一个原则:最左前缀匹配。复合索引(a, b, c)能匹配(a)、(a,b)、(a,b,c),但不能匹配(b)或(c)。

3.3 ORM框架选型:Room vs GreenDAO

ORM框架的选择,说白了就是「省事」和「性能」之间的权衡。我两个都用过,说说我的真实感受。

对比维度 Room GreenDAO
官方支持 Google官方,Jetpack组件 第三方,维护频率较低
编译期检查 支持(SQL语法检查) 不支持
性能 优秀(接近原生SQLite) 极优(号称最快ORM)
学习成本 中等(需要学注解和DAO) 较低(API简单直观)
数据库版本迁移 支持(Migration机制) 支持(但较复杂)
Kotlin协程支持 原生支持 需要手动适配

我个人习惯用Room。原因有三:

  • 编译期检查:写错SQL语句直接编译报错,不用等到运行时崩溃。这个特性救过我很多次。
  • 与LiveData/Flow无缝集成:数据变化自动通知UI更新,省去了手动刷新逻辑。
  • 官方维护:跟着Android版本走,不用担心兼容性问题。

但GreenDAO也有它的优势。如果你对性能有极致要求,比如每秒要写入上万条数据,GreenDAO确实更快。我做过对比测试,在批量插入场景下,GreenDAO比Room快10%-20%。

选型建议:新项目首选Room。如果项目已经用了GreenDAO且运行良好,没必要强行迁移。性能瓶颈通常不在ORM本身,而在SQL语句和索引设计上。

// Room示例
@Entity
data class User(
    @PrimaryKey val id: Long,
    val name: String,
    val age: Int
)

@Dao
interface UserDao {
    @Query("SELECT * FROM user WHERE age > :minAge")
    fun getUsersOlderThan(minAge: Int): List<User>
    
    @Insert
    fun insert(user: User)
}

// GreenDAO示例
// 需要先定义实体类,然后通过GreenDAO生成代码
@Entity
public class User {
    @Id
    private Long id;
    private String name;
    private int age;
}

// 使用方式
DaoSession daoSession = ((App) getApplication()).getDaoSession();
UserDao userDao = daoSession.getUserDao();
List<User> users = userDao.queryBuilder()
    .where(UserDao.Properties.Age.gt(18))
    .list();

嗯,这里要注意一点:不管用哪个ORM,都不要完全依赖它。遇到复杂查询,直接写原生SQL反而更高效。我见过有人用ORM的链式调用拼了一个20行的查询,性能差到离谱。换成原生SQL后,3行搞定,速度提升了10倍。

最后说一句:数据库优化没有银弹。事务、WAL模式、索引、ORM选型,每个环节都要根据实际场景来权衡。多跑跑测试,多看看查询计划,你也能成为SQLite优化高手。