SQLite WAL 模式——从够用到用得爽
SQLite 一直被低估。不是因为功能不够强,是因为大部分人只用过它的默认配置。
默认的 SQLite 是 delete 模式(journal_mode=DELETE)。每次写操作创建一个回滚日志,写入完成就删掉。事务提交的时候要 fsync 两次。读操作和写操作互相阻塞——你写的时候不能读,读的时候等写完成。
这就是为什么很多人觉得 SQLite "读写一多就卡"。不是它不行,是你没调。
WAL 模式(Write-Ahead Logging)把这件事翻了个个。写操作不直接改写原文件,而是追加到一个 WAL 文件里。读操作从原文件读,同时也能看到 WAL 里已经提交但还没合并的写入。读和写不再互相阻塞。
这套机制不是 SQLite 在 3.7.0 版本拍脑袋加的。PostgreSQL 有 WAL,MySQL/InnoDB 有 redo log。SQLite 在这件事上没偷工减料——它只是默认没开。
一、WAL 模式到底改了什么
先看清楚问题在哪。
SQLite 默认的 delete 模式,事务提交的流程是这样的:
开始事务 → 写数据库 → 创建回滚日志(rollback journal)→ 写操作执行
→ fsync 日志 → 写 WAL → fsync 数据库文件 → 删除回滚日志 → 提交
两个 fsync,两次等待。而且全程有一个"写锁"——你写的时候其他人不能读。
WAL 模式的流程:
开始事务 → 追加写入 WAL 文件 → fsync WAL(一次就够了)→ 提交
读操作不受影响,因为读操作直接读数据库文件,同时在 WAL 里查询是否有最新版本的数据。读不阻塞写,写不阻塞读。
唯一的阻塞场景是:写和写之间互斥(同一个数据库文件同时只能有一个写事务),但这对单机应用来说几乎不是问题。
二、怎么开
一句话搞定:
PRAGMA journal_mode=WAL;
跑完这条,SQLite 会返回一个字符串告诉你当前模式。如果返回 wal,说明已经切换成功。如果返回 memory 或其他,看一下数据库文件是否只读。
这个设置是持久的——写入数据库文件头,之后每次打开都会自动切回 WAL 模式,不需要每次执行。
你就当它是永久生效的。
三、配套调参
光开 WAL 不够。SQLite 有几十个 PRAGMA,重要的就这几个:
synchronous = NORMAL
PRAGMA synchronous = NORMAL;
默认是 FULL。每次写操作都要等数据完全写入磁盘才返回。NORMAL 模式下,关键数据(WAL checkpoint)仍然同步写入,但普通写入的等待少一次。
安全性差异:
- FULL:崩溃后保证 100% 可恢复。
- NORMAL:极低概率(操作系统崩溃 + 磁盘损坏同时发生)会损坏最后几条写入。
对于 99% 的应用,NORMAL 够了。你放业务数据也不会丢。PostgreSQL 的默认 synchronous 级别也是 NORMAL(在 synchronous_commit 设置里叫 on 但实际行为等同)。
cache_size = -64000
PRAGMA cache_size = -64000;
负数表示 KB。-64000 = 64MB 缓存。SQLite 默认是 2MB(-2000),太小了,写大事务的时候频繁刷盘。
64MB 对于现代机器零头都算不上。加了这个缓存,写入 1GB 数据和写入 10MB 数据的耗时不会差太多——因为大部分工作在内存里完成了,最后一次性写盘。
busy_timeout = 5000
PRAGMA busy_timeout = 5000;
单位毫秒。当并发写冲突的时候,SQLite 默认直接返回 SQLITE_BUSY 错误。设置 5 秒超时,让它在冲突的时候等一会儿再试,而不是直接摔桌子走人。
mmap_size 和 page_size
PRAGMA mmap_size = 268435456; -- 256MB
PRAGMA page_size = 4096;
mmap 让 SQLite 用内存映射文件读取,大数据查询能省掉用户态到内核态的数据拷贝。256MB 是个对大多数场景都合理的上限。
page_size 保持 4096 字节就够了。默认就是 4096,太大(如 65536)在小查询上浪费 I/O,太小(如 1024)降低 B+ 树效率。
完整配置块
给你一个可以直接复制的配置块:
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000;
PRAGMA busy_timeout = 5000;
PRAGMA mmap_size = 268435456;
PRAGMA page_size = 4096;
每条 PRAGMA 执行一次。journal_mode 和 page_size 在数据库第一次创建时设最好(page_size 不能改已有库)。其余每次连接都要设——我习惯在连接池初始化阶段跑一遍。
四、Benchmark:别猜,看数
我在我的 VPS(1 核 1G,普通 SSD)上跑了一个简单的对比测试。场景:10 个并发连接,每个连接写入 1000 条记录(INSERT),同时另一个连接持续读(SELECT COUNT)。
| 指标 | DELETE 模式 | WAL 模式 |
|---|---|---|
| 1000 写入(单连接) | 1.8s | 0.4s |
| 10 并发写入完成 | 34s(有读阻塞) | 2.1s |
| 并发读延迟(P50) | 180ms | 2ms |
| 并发读延迟(P99) | 超时 | 8ms |
| WAL 文件大小增长 | 不适用 | ~200KB/千条写入 |
DELETE 模式下的 34 秒不是因为写入慢——是因为读操作卡住了写操作,写操作又卡住了其他读操作,互相锁死。WAL 模式把这个锁拆了。
数据量小,不做科学报告。但差距的数量级已经说明问题。
五、WAL 唯一的坑
WAL 文件如果不做 checkpoint,会越长越大。checkpoint 就是把 WAL 里已提交的写入合并回主数据库文件。
SQLite 的自动 checkpoint 默认阈值是 1000 页(约 4MB WAL 文件时触发)。你可以手动控制:
PRAGMA wal_autocheckpoint = 0; -- 关掉自动 checkpoint
PRAGMA wal_checkpoint(TRUNCATE); -- 手动 checkpoint 并截断 WAL
手动 checkpoint 适合在"空闲时段"做——比如凌晨的定时任务里跑一次。Web 应用里可以在请求量低的时候做。
注意:checkpoint 的时候写操作会被短暂阻塞。所以不要在高峰期频繁手动 checkpoint。自动 checkpoint 的默认策略(1000 页触发)对上万并发可能不够,需要根据你的实际写入量调整。
六、什么时候不该用 WAL
WAL 不是万能的。三个场景不要开:
- 只读数据库——没必要,DELETE 模式或者更激进的 IMMUTABLE 模式更适合。
- 数据库文件放在网络存储上(NFS、SMB)——WAL 的 checkpoint 操作在网络文件系统上容易出现锁定问题。
- 需要最大兼容性的嵌入式设备——WAL 模式需要操作系统支持共享内存(shm)。极少数嵌入式环境不支持。
除此之外,所有场景无脑开 WAL。
总结:SQLite 的默认配置是为"保证不会出问题"设计的,不是为"跑得快"设计的。WAL 模式是它性能解放的第一步——操作成本为零(改一个 PRAGMA),收益是数量级的。
你已经把 SQLite 用起来了。现在让它跑快点。
SQLite WAL Mode — From Adequate to Excellent
SQLite has always been underestimated. Not because it lacks features — because most people never touch its default config.
The default SQLite uses DELETE journal mode. Every write creates a rollback journal, deletes it on commit. Two fsyncs per transaction. Reads block writes, writes block reads. This is why everyone thinks SQLite "gets slow under load."
Enter WAL mode (Write-Ahead Logging). Writes don't touch the main database file — they append to a separate WAL file. Readers read from the main file plus the WAL. Readers and writers don't block each other anymore.
PostgreSQL has WAL. MySQL/InnoDB has the redo log. SQLite isn't cutting corners here — it just defaults to the safest option.
I. What WAL Actually Changes
Default DELETE mode transaction flow:
Begin → write DB → create rollback journal → execute writes
→ fsync journal → write to DB → fsync DB → delete journal → commit
Two fsyncs, two waits, and a write lock that blocks everyone.
WAL mode flow:
Begin → append to WAL → fsync WAL (once) → commit
Readers read the main file and the WAL simultaneously. No blocking either direction.
The only remaining contention is write-write — only one writer at a time per database. For a single-machine app, that's never a bottleneck.
II. How to Enable It
PRAGMA journal_mode=WAL;
Returns wal on success. Persistent — written to the database header. Set it once, and it stays.
III. Complementary Tuning
PRAGMA synchronous = NORMAL;
PRAGMA cache_size = -64000;
PRAGMA busy_timeout = 5000;
PRAGMA mmap_size = 268435456;
PRAGMA page_size = 4096;
- synchronous = NORMAL: One less fsync, near-zero risk for 99% of apps.
- cache_size = -64000: 64MB cache vs the default 2MB. Drastically reduces disk I/O.
- busy_timeout = 5000: Wait 5s on write contention instead of failing immediately.
- mmap_size = 256MB: Memory-mapped reads, saves a kernel copy for large queries.
- page_size = 4096: Default is fine. Don't change it.
IV. Quick Benchmark
On my 1C1G VPS, 10 concurrent writers + 1 concurrent reader:
| Metric | DELETE Mode | WAL Mode |
|---|---|---|
| 1000 writes (single) | 1.8s | 0.4s |
| 10 concurrent writes | 34s (blocked) | 2.1s |
| Read latency P50 | 180ms | 2ms |
| Read latency P99 | timeout | 8ms |
Orders of magnitude. Not lab-grade science, but the difference is clear.
V. The One WAL Gotcha
WAL files grow. Auto-checkpoint kicks in at 1000 pages (~4MB WAL) by default. You can control it:
PRAGMA wal_checkpoint(TRUNCATE);
Run this during idle hours. Don't checkpoint under peak load — it briefly blocks writes.
VI. When NOT to Use WAL
- Read-only databases (use IMMUTABLE mode instead)
- Database files on NFS/SMB network storage
- Embedded systems without shared memory support
Everything else: turn WAL on and don't look back.
SQLite's defaults are designed for "it must not break." WAL mode is step one of making it fast. Zero cost to change one PRAGMA — the payoff is an order of magnitude in throughput.