SQLite 上生产必调的 6 个 PRAGMA:WAL 模式、cache_size、synchronous 实操

· 约 6 分钟 🗄️ SQLite 在线编辑

SQLite 默认配置是 1990 年代为软盘 + 单进程优化的——保守、慢、易撞 “database is locked”。直接拿默认配置上生产,性能能差出一个数量级

这篇拆解 6 个必调 PRAGMA、WAL checkpoint 机制、以及 “database is locked” 的根因和解法。

必调的 6 个 PRAGMA

第一次连接数据库后跑:

PRAGMA journal_mode = WAL;          -- 持久化,只需一次
PRAGMA synchronous = NORMAL;        -- 每次连接
PRAGMA cache_size = -65536;         -- 64 MB,每次连接
PRAGMA mmap_size = 268435456;       -- 256 MB,每次连接
PRAGMA temp_store = MEMORY;         -- 临时表放内存
PRAGMA busy_timeout = 5000;         -- 5 秒锁等待
PRAGMA默认推荐收益
journal_modeDELETEWAL写 TPS 3-10 倍,读写并发能力提升一个量级
synchronousFULLNORMAL写 TPS 2-5 倍(SSD),断电仅丢未 checkpoint 事务
cache_size-2000(8 MB)-65536(64 MB)缓存命中查询快 10-100 倍
mmap_size0256 MB+减少 read 系统调用,OS 自动管理缓存
temp_storeDEFAULTMEMORY临时表 / 排序在内存,避免磁盘 IO
busy_timeout05000短锁冲突自动重试,少很多 SQLITE_BUSY

注意journal_mode持久化的(写到数据库文件里),其他都是连接级别——每开一个新连接都要重设。

WAL 模式的工作原理

WAL(Write-Ahead Log)是 SQLite 3.7(2010)引入的并发模型。

默认 DELETE 模式

  1. 写者拿到独占锁
  2. 把要改的页拷到 rollback journal 文件
  3. 改主库
  4. commit 时删 journal
  5. 期间读者全部阻塞

WAL 模式

  1. 写者把修改追加到 -wal 文件(顺序写,快)
  2. 读者直接读主库 + 检查 WAL 里有没有更新版本(MVCC 类似机制)
  3. 读不阻塞写、写不阻塞读
  4. 写者还是只能 1 个,但读者无数

仅有的代价

  • 多两个伴生文件 -wal-shm
  • 不能在 NFS 上用(锁机制不可靠)
  • 跨进程访问要在同一台机器

生产环境必开 WAL——除非数据库在网络共享。

synchronous 的三档权衡

模式性能安全风险
FULL(默认)最慢最安全
NORMAL(推荐)中等断电丢未 checkpoint 事务,库不损坏
OFF最快危险断电可能损坏库(DELETE 模式)

WAL + NORMAL 的安全保证

  • 主库永远一致——checkpoint 前 WAL 数据在 -wal 文件,断电只是丢未 checkpoint 的事务,主库结构完整
  • 已 commit 但未 fsync 的事务可能丢——如果 OS 还没刷盘就断电,这部分丢失,但库不损坏
  • 实际损失通常是最近几秒的事务

对比 rollback journal + NORMAL 那个组合真的会损坏库——因为 journal 可能没完整写入。所以 NORMAL 必须配 WAL。

金融账务等不可丢任何事务的场景,用 FULL,接受性能下降;其他场景一律 NORMAL。

cache_size 和 mmap_size 的关系

两个都是缓存,但层级不同:

  • cache_size:SQLite 自己的页缓存(精准但 per-connection)
  • mmap_size:让 OS 把数据库文件 mmap 到地址空间(粗放但便宜)

推荐配置

库大小cache_sizemmap_size
< 100 MB-32000(32 MB)设到库大小
100 MB - 10 GB-65536 ~ -262144(64-256 MB)1-4 GB
> 10 GB按热数据估内存的 50%

陷阱

  • cache_size 是每连接的——多连接 × 大 cache 会爆内存
  • mmap 在 32 位系统受地址空间限制
  • 不要无脑设大——测一下 hit rate,不命中再加

监控命中率:sqlite3_db_status(SQLITE_DBSTATUS_CACHE_HIT)

“database is locked” 根因和解决

这是 SQLite 最常见的报错。根因永远是写竞争

  1. 多写者同时 BEGIN——只有 1 个拿到锁,其余 SQLITE_BUSY
  2. 长读事务持有共享锁阻塞写
  3. WAL checkpoint 和写者冲突
  4. 跨进程 + busy_timeout=0 立刻报错

解决步骤

  1. PRAGMA busy_timeout = 5000——拿不到锁时等 5 秒再报错,多数短冲突自动消化
  2. 缩短事务——BEGIN 到 COMMIT 之间不要做网络请求 / 长计算
  3. 批量写入用单事务——BEGIN; INSERT; INSERT; ...; COMMIT; 比逐条快 10-100 倍且锁次数少
  4. WAL 模式必开——默认 DELETE 读写互斥,冲突频繁
  5. 应用层重试——大多数 sqlite 模块不会自动重试,要包一层 retry
  6. 避免 BEGIN EXCLUSIVE——除非真需要排他锁,DEFERRED(默认)够用

典型 bug

  • Python sqlite3 默认 isolation_level="" 自动 BEGIN/COMMIT,多线程下容易互锁;建议 isolation_level=None 自己控制事务
  • Web 框架 请求处理函数里嵌套写事务,一个请求长占锁阻塞其他
  • with conn: 嵌套——内层 commit 时外层还持锁

WAL checkpoint:合并回主库

WAL 不会无限增长——SQLite 定期把 -wal 文件内容合并回主库,叫 checkpoint。

自动 checkpoint:默认 WAL 累积到 1000 页(约 4 MB)时,下一个写者执行完触发 PASSIVE checkpoint,正常情况下 -wal 文件稳定在 4-50 MB。

四种手动模式

模式作用
PASSIVE不阻塞,能 checkpoint 多少算多少
FULL等所有读完成后完整 checkpoint
RESTARTFULL + 重置 WAL 指针
TRUNCATEFULL + 把 WAL 文件截到 0 字节(释放磁盘)

什么时候手动

  • WAL 涨到几百 MB / 几 GB(长事务或 PASSIVE 没机会做完)→ TRUNCATE
  • 夜间维护窗口定时跑 → TRUNCATE
  • 备份前 → 把 WAL 合并回主库再复制
  • 关闭应用前 → 清理 WAL 让下次启动更快

自动限制PRAGMA journal_size_limit = 67108864(64 MB)超过自动 truncate。

索引设计的 SQLite 特有坑

  1. type affinity —— 列是动态类型,存进去的 “3” 和 3 在没有明确类型时可能不匹配索引;建表时指定 INTEGER / TEXT / REAL
  2. WITHOUT ROWID —— 默认每表隐藏 rowid + 主键单独索引;主键就是查询路径时加 WITHOUT ROWID,主键变聚簇,省一份索引
  3. 复合索引顺序 —— (a, b, c) 能加速 a=?a=? AND b=?a=? AND b=? AND c=?加速不了 b=?c=?;高基数列放前面、等值列放前面、范围列放后面
  4. partial index —— CREATE INDEX idx ON t(a) WHERE b=1 只索引满足条件的行,省空间又快;适合”软删除”(WHERE deleted=0
  5. expression index —— CREATE INDEX idx ON t(LOWER(email)) 配合 WHERE LOWER(email)=?
  6. EXPLAIN QUERY PLAN —— 必看工具,USING INDEX 表示命中,SCAN TABLE 表示全表扫
  7. ANALYZE —— 周期性跑 ANALYZE 更新统计信息

容易踩的查询坑

  • WHERE a=? OR b=? 即使 a、b 都有索引也可能 SCAN;改写成 UNION
  • LIKE '%xxx%' 走不了索引;前缀匹配 LIKE 'xxx%' 才能
  • WHERE a=NULL 永远 false;要用 IS NULL(能命中索引)
  • JSON 字段:3.38+ 支持 CREATE INDEX idx ON t(json_extract(data, '$.key'))

调优顺序速查

按这个顺序调,多数性能问题能解:

优先级调什么预期收益
1journal_mode = WAL写 TPS 3-10 倍 + 锁冲突大幅减少
2synchronous = NORMAL写 TPS 2-5 倍
3busy_timeout = 5000消除大部分 SQLITE_BUSY
4批量写入用单事务写 TPS 10-100 倍
5加合适的索引 + EXPLAIN 校验查询 10-100 倍
6cache_size / mmap_size 加大命中率上去后再快 1-3 倍
7还不行 → 检查是不是真超 SQLite 能力迁 Postgres

上生产前的检查清单

  • PRAGMA journal_mode=WAL 已写入文件(一次性持久化)
  • 每次连接设 synchronous=NORMAL + cache_size + mmap_size + busy_timeout
  • 关键查询用 EXPLAIN QUERY PLAN 验证走索引
  • 批量写入用 BEGIN; ... COMMIT; 包,不要逐条 commit
  • 监控 -wal 文件大小(< 100 MB 健康)
  • 监控 SQLITE_BUSY 错误率,频繁出现意味着写并发到瓶颈
  • 配 Litestream 或定期 .backup,不要直接 cp 文件
  • 定期 ANALYZE 更新统计 + PRAGMA integrity_check 验证完整性
  • 32 位系统不要设 mmap_size > 1 GB
  • 不在 NFS 上用 WAL 模式

默认 SQLite 是给”个人本地存储”优化的;生产场景必须显式配置 WAL 和缓存。这 6 个 PRAGMA 是分水岭,过了之后 SQLite 能撑住中等并发的真实业务。

❓ 常见问题

WAL 模式比默认快多少?为什么生产必开?

写性能 3-10 倍、读写并发能力提升一个量级默认 journal_mode=DELETE 的问题:(1) 每次写事务先把要修改的页拷到 rollback journal,再改主库,commit 时删 journal;(2) 读和写互斥——任一时刻整库只能一种操作;(3) 磁盘 IO 翻倍——每次写都是"写 journal + 写主库"两次。WAL 模式(journal_mode=WAL):(1) 写入只追加到 -wal 文件(顺序写,速度快);(2) 读直接读主库 + 检查 WAL 里的新版本;(3) 读不阻塞写、写不阻塞读——写者只锁 WAL 末尾,读者各自读不同时点的快照(MVCC 类似机制);(4) checkpoint 把 WAL 内容批量合并回主库(默认 1000 页或手动触发)。实测:在 SSD 上中等事务负载,WAL 写入 TPS 5000-50000(默认模式 500-5000);混合读写下吞吐量提升更明显。只需一次设置PRAGMA journal_mode=WAL持久化的,会写到数据库文件里,下次打开自动 WAL;不需要每次连接都设。仅有的代价:(1) 多两个文件 -wal 和 -shm;(2) 不能在网络文件系统(NFS)上用——锁机制不可靠;(3) 跨进程访问要在同机器。结论:除非数据库在 NFS / 跨机共享,生产环境必开 WAL

synchronous=NORMAL 真的安全吗?会不会丢数据?

WAL + synchronous=NORMAL 是生产推荐组合,断电最坏丢"最近未 checkpoint 的事务",主库永远一致三档对比:(1) FULL(默认)——每次 commit 都 fsync,最安全但最慢;(2) NORMAL——commit 时不强制 fsync,依赖 OS 刷盘,但 checkpoint 时一定 fsync;(3) OFF——完全不 fsync,最快但断电可能损坏数据库。WAL + NORMAL 的安全保证:(1) 主库永远一致——checkpoint 前 WAL 内容在 -wal 文件里,断电只是丢未 checkpoint 的事务,主库结构完整;(2) 已 commit 的事务可能丢——如果 OS 还没把 WAL 数据刷到磁盘就断电,这部分事务丢失但不会损坏库;(3) 实际损失通常是最近几秒的事务对比 rollback journal + NORMAL:那个组合真的会损坏库——因为 journal 可能没完整写入,崩溃恢复时无法判断哪些是已 commit 的。性能差异:FULL → NORMAL 在 SSD 上提升 2-5 倍,HDD 上 5-20 倍;OFF 比 NORMAL 再快 10-50%(差距不大但风险倍增)。实务:(1) Web 应用 / 桌面 App → WAL + NORMAL 是黄金组合;(2) 金融账务 / 不可丢任何事务 → 用 FULL(接受性能下降);(3) 一次性导入 / 临时缓存 → 可用 OFF(导入完后切回 NORMAL);(4) 永远不要 OFF + DELETE——崩溃必坏库。

cache_size 和 mmap_size 该设多大?区别在哪?

cache_size 是 SQLite 内部页缓存,mmap_size 让 OS 帮你管缓存——优先调 cache_sizecache_size:(1) SQLite 自己维护的内存缓存,按页(page)算;(2) 默认 -2000 表示 2000 页 × 4096 字节 ≈ 8 MB——太小了;(3) 推荐设置 PRAGMA cache_size = -64000(64 MB,负数表示按 KB 算 KB 数);(4) 大库可设到几百 MB 甚至几 GB;(5) 命中缓存的查询比未命中快 10-100 倍。mmap_size:(1) 把数据库文件 mmap 到进程地址空间,让 OS 内核管理缓存;(2) 默认 0(不启用);(3) 推荐 PRAGMA mmap_size = 268435456(256 MB)或更大;(4) 优势:减少 read() 系统调用、和 OS 文件缓存共享;(5) 劣势:增加进程虚拟内存占用、32 位系统受地址空间限制、某些 OS 上 fork 后效率下降。两者关系:(1) cache_size 是 SQLite 的私有缓存(更精准);(2) mmap_size 是 OS 级别的兜底(更便宜但粗放);(3) 两个都开效果叠加,但 cache_size 优先级更高(命中后不需要 mmap)。推荐配置:(1) 小库(< 100 MB)——cache_size = -32000(32 MB),mmap_size 设到库大小;(2) 中库(100 MB - 10 GB)——cache_size = -65536 到 -262144(64-256 MB),mmap_size = 1-4 GB;(3) 大库(> 10 GB)——cache_size 按热数据估,mmap_size 设到内存的 50%。陷阱:(1) cache_size 是每连接的——多连接 × 大 cache 会爆内存;(2) mmap 在 32 位系统会报 SQLITE_FULL 因为地址空间不够;(3) 不要无脑设大 —— 测一下 hit rate,不命中再加;(4) 用 sqlite3_db_status(SQLITE_DBSTATUS_CACHE_HIT) 监控命中率。

"database is locked" 错误是怎么来的?怎么解决?

这是 SQLite 里最常见的报错,根因是写竞争触发场景:(1) 多个写者同时尝试 BEGIN——只有 1 个能拿到锁,其余报 SQLITE_BUSY;(2) 长读事务持有共享锁阻塞写;(3) WAL checkpoint 操作和写者冲突;(4) 跨进程访问 + 默认 busy_timeout=0 立刻报错。解决方案:(1) PRAGMA busy_timeout = 5000——告诉 SQLite 拿不到锁时等 5 秒再报错,多数短锁冲突自动消化;(2) 缩短事务——BEGIN 到 COMMIT 之间不要做网络请求 / 长计算,否则锁持有时间长;(3) 批量写入用单事务——BEGIN; INSERT...; INSERT...; ...; COMMIT; 比逐条 INSERT 快 10-100 倍且锁次数少;(4) WAL 模式必开——默认 DELETE 模式读写互斥,冲突频繁;(5) 应用层重试——sqlite3 模块本身不重试,要包一层 retry with backoff;(6) 避免显式 BEGIN EXCLUSIVE——除非真的需要排他锁,DEFERRED(默认)够用。典型 bug:(1) Python sqlite3 默认 isolation_level 是 ""(自动 BEGIN/COMMIT)——多线程下容易互锁,建议 isolation_level=None 自己控制事务;(2) Web 框架的请求处理函数里嵌套写事务——一个请求长占锁阻塞其他请求;(3) 用 with conn: 上下文管理器嵌套——内层 commit 时外层还持锁。调优顺序:(1) 先开 WAL;(2) 设 busy_timeout=5000;(3) 检查长事务、缩短锁持有时间;(4) 还不行就考虑写并发是不是真超出 SQLite 能力,迁 Postgres。

WAL checkpoint 是什么?什么时候手动触发?

checkpoint 把 -wal 文件里累积的修改合并回主库,避免 WAL 无限增长自动 checkpoint:(1) WAL 累积到 1000 页(约 4 MB,可调)时,下一个写者执行完会触发 PASSIVE checkpoint;(2) PASSIVE 模式不阻塞读和写,能 checkpoint 多少算多少;(3) 默认配置下 -wal 文件通常稳定在 4-50 MB。手动 checkpoint:(1) PRAGMA wal_checkpoint(PASSIVE)——温和模式,不阻塞,能 checkpoint 多少算多少;(2) PRAGMA wal_checkpoint(FULL)——等所有读完成后 checkpoint 完整;(3) PRAGMA wal_checkpoint(RESTART)——FULL + 重置 WAL 文件指针,下次写从头开始;(4) PRAGMA wal_checkpoint(TRUNCATE)——FULL + 把 WAL 文件截断到 0 字节(释放磁盘空间)。什么时候手动 checkpoint:(1) WAL 文件涨到几百 MB / 几 GB——长事务或 PASSIVE 一直没机会做完,手动 TRUNCATE;(2) 写入低谷期定时维护——夜间 cron 跑一次 wal_checkpoint(TRUNCATE);(3) 备份前——把 WAL 合并回主库再复制,避免备份丢东西;(4) 关闭应用前——清理 WAL 让下次启动更快。陷阱:(1) WAL 文件 = 0 字节才表示完全 checkpoint,几 MB 是正常的;(2) 长读事务持有 WAL 内的某个版本,会阻塞 checkpoint 到这之后的内容;(3) PRAGMA journal_size_limit = 67108864(64 MB)能限制 WAL 文件大小,超过会自动 truncate;(4) checkpoint 中如果有写者,会让写者等一会——但 PASSIVE 模式很短。监控PRAGMA wal_checkpoint 不带参数也能跑(默认 PASSIVE),返回 (busy, log_pages, checkpointed_pages),三个数字告诉你 WAL 状态。

索引设计在 SQLite 里有什么坑?

几个 SQLite 特有的索引行为。(1) type affinity 影响索引选择——SQLite 的列是动态类型,存进去的"3"和 3 在没有强类型转换时可能匹配不到索引;建表时指定明确类型(INTEGER / TEXT / REAL),SQLite 会自动转换。(2) WITHOUT ROWID 表更紧凑——默认每个表有隐藏 rowid 列 + 主键单独索引;如果主键就是查询路径,建表加 WITHOUT ROWID,主键变成聚簇索引,省一份索引。(3) 复合索引顺序很重要——CREATE INDEX idx ON t(a, b, c) 能加速 WHERE a=?WHERE a=? AND b=?WHERE a=? AND b=? AND c=?,但加速不了 WHERE b=?WHERE c=?;高基数列放前面,等值查询列放前面,范围查询列放后面。(4) partial index——CREATE INDEX idx ON t(a) WHERE b=1 只索引满足条件的行,能省一半空间且查询更快;适合"软删除"场景(WHERE deleted=0)。(5) expression index——CREATE INDEX idx ON t(LOWER(email)) 索引函数结果,配合 WHERE LOWER(email)=? 用。(6) EXPLAIN QUERY PLAN——必看工具——EXPLAIN QUERY PLAN SELECT ... 输出查询计划,确认索引被命中(USING INDEX 而不是 SCAN TABLE)。(7) ANALYZE 更新统计信息——周期性跑 ANALYZE 让查询规划器有最新统计,索引选择更准。陷阱:(1) OR 条件可能走全表扫——WHERE a=? OR b=? 即使 a、b 都有索引,也可能走 SCAN;改写成 UNION 两个查询;(2) LIKE '%xxx%' 走不了索引——前缀匹配 LIKE 'xxx%' 才能;全文搜索用 FTS5;(3) NULL 比较——WHERE a=NULL 永远 false,要用 IS NULL;索引能命中 IS NULL;(4) JSON 字段索引——SQLite 3.38+ 用 CREATE INDEX idx ON t(json_extract(data, '$.key')) 索引 JSON 内字段。

🗄️ 打开 SQLite 在线编辑 拖拽 .db/.sqlite 浏览·SQL 查询/增删改·导出 CSV/JSON/.db·本地不上传

📖 同一工具的其他教程