SQLite 存几十 GB 数据的六个优化点

有个错觉——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 一点都不难。别忘了大字段挪出去、别乱建索引。