有个错觉——SQLite 只适合小项目。其实很多人低估了它。理论上 SQLite 单库能到 281 TB,实际几十 GB、一亿行都能稳定跑。前提是这几个优化必须做。
先给个使用边界
| 数据量 | SQLite 适不适合 |
|---|---|
| < 10 GB | 完全没问题,甚至比 Postgres 简单 |
| 10 GB ~ 100 GB | 可以,需要认真优化 |
| > 100 GB | 谨慎,考虑分片 / 换数据库 |
| 高并发写入 | 不推荐,SQLite 写是全库锁(WAL 缓解但仍单写) |
| 多机共享文件 | 别玩,NFS 上的 SQLite 特别容易坏 |
适合 SQLite 的典型场景:本地缓存、日志、爬虫存档、IoT 边缘节点、桌面/Electron 应用、单机分析、中小型业务。
1. WAL 模式(必开)
PRAGMA journal_mode = WAL;
作用:
- 读和写可以并发(默认 journal 会锁全库)
- 崩溃恢复快
- 大量写入性能大幅提升
一次性开完之后写进库设置,不用每次连接都设。
2. 调 synchronous
PRAGMA synchronous = NORMAL; -- 折中,推荐
-- PRAGMA synchronous = OFF; -- 极限性能,接受掉电丢数据
FULL(默认)= 每次事务 fsync 磁盘、NORMAL = 检查点时 fsync、OFF = 完全信操作系统。日志/缓存场景 NORMAL 完全够用,性能差好几倍。
3. 批量事务(提升最猛的一条)
单条 insert:
for (const row of rows) {
db.prepare("INSERT INTO logs VALUES (?, ?)").run(row.a, row.b);
}
一亿行你等到明年。改成批量:
const insert = db.prepare("INSERT INTO logs VALUES (?, ?)");
const many = db.transaction((rows) => {
for (const row of rows) insert.run(row.a, row.b);
});
many(rows);
性能能差 100 ~ 1000 倍。SQLite 每个事务外面套一次 fsync,逐条 insert 就是每行都 fsync。
4. 索引不要滥建
EXPLAIN QUERY PLAN
SELECT * FROM logs WHERE user_id = ? AND ts > ?;
- 索引越多,写入越慢(每行 insert 都要维护索引)
- 只给高频过滤字段建索引
- 复合索引按 WHERE 里最常见的过滤顺序建:
(user_id, ts) - 大文本 / JSON 字段别建普通索引,需要就用 FTS5
5. 分页别用 OFFSET
-- 慢,越翻越慢:OFFSET 需要跳过前 N 行
SELECT * FROM logs ORDER BY id LIMIT 20 OFFSET 1000000;
-- 快,游标分页:直接从上一页最后一个 id 之后取
SELECT * FROM logs WHERE id > ? ORDER BY id LIMIT 20;
学名叫 Keyset Pagination,任何大表分页都该用。
6. 大字段不要塞 blob
图片、视频、大 JSON 直接扔 SQLite 会拖慢一切——包括那些跟大字段无关的查询。
正确做法:
- 文件系统 / 对象存储放实体
- SQLite 只存路径 / 元数据
或者用 SQLite 官方推荐 的经验值:< 100 KB 存库里更快,> 100 KB 存文件系统更快。
7. 加上几个 PRAGMA
PRAGMA cache_size = -100000; -- 100 MB 缓存(负数表示 KB)
PRAGMA temp_store = MEMORY; -- 临时表放内存
PRAGMA mmap_size = 268435456; -- 256 MB mmap,减少 syscall
PRAGMA busy_timeout = 5000; -- 遇到锁等 5 秒再报错
一句话总结
WAL + 批量事务 + Keyset 分页——这三条打下来,几十 GB 的 SQLite 一点都不难。别忘了大字段挪出去、别乱建索引。
