Menu

SQLite PRAGMA 设置:生产环境连接调优

真正重要的几条 PRAGMA 语句:journal_mode、synchronous、foreign_keys、busy_timeout、cache_size,以及在生产环境中该用的值。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

PRAGMA 是你和引擎对话的方式

PRAGMA 是 SQLite 特有的语句,用来读取或修改引擎的行为。你可以像执行其他 SQL 一样执行它们,只不过它们操作的不是你的数据,而是数据库的配置。

当作查询执行时,PRAGMA 返回当前值;当作赋值执行时,它会修改这个值:

可以这样理解:大多数 PRAGMA 都是按连接生效的。打开一个新连接,默认值就又回来了。这就是为什么生产代码里通常会有一小段 PRAGMA 语句,在每次建立连接后立刻执行。

生产环境基线

如果你只记得住五条 PRAGMA,就记住这几条:

对几乎所有把 SQLite 当作主存储的应用来说,这都是一套合理的默认配置。每一条都值得单独弄懂,这一页剩下的部分会逐一介绍。

journal_mode = WAL

日志模式决定了 SQLite 如何让写入变得持久。默认的 DELETE 模式使用回滚日志:写操作会阻塞读操作,读操作也会阻塞写操作。对命令行工具来说没问题,对 Web 应用来说就很痛苦了。

WAL(预写日志)把这一点反了过来。读和写互不阻塞:在写操作提交期间,读操作看到的是一个一致的快照。同一时间仍然只能有一个写操作,但在高负载下读取依然很快。

有几点需要知道:

  • journal_mode 是持久生效的:一旦设置,就会一直保留在这个数据库文件上。你不需要在每个连接上都设置,不过设置了也无妨。
  • WAL 会在你的 .db 旁边创建两个额外的文件:一个 -wal 和一个 -shm。数据库打开期间不要删除它们。
  • WAL 在网络文件系统(NFS、SMB)上效果不好。请把数据库放在本地磁盘上。

另有一篇关于 WAL 模式与并发的文档讲得更深入。目前你只需要:把它打开。

synchronous = NORMAL

synchronous 控制 SQLite 把数据刷到磁盘的积极程度。这是持久性和速度之间的取舍。

  • FULL(默认):每次提交后都刷盘。持久性最高,速度较慢。
  • NORMAL:在安全的检查点刷盘。配合 WAL 时是安全的,速度更快。
  • OFF:交给操作系统决定。很快,但断电时有损坏数据库的风险。

结果中的整数(1)对应 NORMAL。在 WAL 模式下,推荐使用 NORMAL:程序崩溃时你不会丢失已提交的事务,只有在断电时才可能丢失最近的几个。对大多数应用来说,这是恰当的平衡。

除非你是在填充一个可以随时从头重建的临时数据库,否则不要用 OFF。

foreign_keys = ON

这一条经常让人栽跟头。SQLite 支持外键,但外键约束检查默认是关闭的,而且这是一个按连接生效的设置:

在 foreign_keys = ON 时,最后那条插入会失败,因为不存在 id 为 999 的作者。没有这条 PRAGMA 的话,SQLite 会若无其事地写入这条孤儿记录,几个月后你才会发现这摊烂摊子。

在每个新连接上,把 PRAGMA foreign_keys = ON; 作为第一条语句执行。大多数 ORM 会自动这么做;如果你直接用底层驱动,这件事就得你自己负责。

busy_timeout = 5000

SQLite 同一时间只允许一个写操作。默认情况下,如果第二个连接在第一个连接的事务进行到一半时尝试写入,它会收到 SQLITE_BUSY 并立即放弃。

busy_timeout 会让 SQLite 改为等待并重试:

单位是毫秒。5000 的意思是“最多等 5 秒拿到锁,然后才放弃”。配合 WAL,这能消除并发应用中大多数莫名其妙的 database is locked 错误。

如果你发现自己要把它调到 30 秒以上,真正的解决办法可能是缩短事务,而不是延长超时时间。

cache_size

cache_size 设置 SQLite 在内存中保留多少个数据库页。缓存越大,磁盘读取就越少,热点数据的查询也就越快。

这个值有两种形式:

  • 正数:表示页数。默认页大小为 4 KB 时,2000 就是 8 MB。
  • 负数:表示 KiB。不管页大小是多少,-20000 都是 20 MB。

负数形式更容易理解:你说的是“给我 20 MB 缓存”,而不用拿页大小去换算。对小型应用来说,20 到 50 MB 就足够了。对于数据库较大、读多写少的场景,可以设得更高。和 synchronous 一样,cache_size 也是按连接生效的。

mmap_size

内存映射 I/O 让 SQLite 直接从操作系统的页缓存中读取数据库文件的部分内容,省掉一次复制。它可以加快大型数据库的读取速度:

这是 256 MB。只要有空间,SQLite 最多会把数据库的这么多内容映射到内存中。分页由操作系统负责,所以你并不是一开始就分配了 256 MB,而只是允许它最多映射这么多。

mmap_size 在读多写少的场景下效果最好。对小数据库也没有坏处。默认值比较保守,所以调大它通常都有收益。

PRAGMA optimize

查询规划器根据统计信息来选择索引。统计信息过时,就会得到糟糕的查询计划。PRAGMA optimize 能以很小的代价更新这些统计信息:

推荐的做法是在关闭长时间存活的连接之前执行它,比如应用关闭时,或者在一个长时间持有连接的请求处理结束时。它很快(通常只要几毫秒),而且只在确实有东西需要更新时才会干活。

它和 ANALYZE 不一样,后者会完整重建统计信息。optimize 是它的轻量版亲戚,可以经常执行。

查看所有设置

如果你想知道一个连接当前的配置,就不带赋值地查询这些 PRAGMA:

调试时很有用:从另一个驱动连接后发现行为变了,原因几乎总是 PRAGMA 不同。

还有一条 PRAGMA pragma_list;,可以列出当前构建支持的所有 PRAGMA:

PRAGMA pragma_list;

不需要背下来,但需要时很方便。

应该在建库时设置、而不是运行时设置的选项

有几条 PRAGMA 配置的是数据库文件本身,只有在创建任何表之前设置才会生效:

  • PRAGMA page_size = 8192;:磁盘上的页大小。默认是 4096,对大多数场景都合适。更大的页对较大的行有帮助。
  • PRAGMA encoding = 'UTF-8';:文本编码。
PRAGMA page_size = 8192;
PRAGMA encoding = 'UTF-8';
CREATE TABLE ...

如果你在已有的数据库上修改 page_size,需要执行 VACUUM 才会生效。在建库时设置一次,然后就可以忘掉它们了。

一段真实的连接初始化代码

在应用代码里,这段代码通常放在负责打开连接的地方。概念上是这样的:

-- Run once on every new connection:
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA cache_size = -20000;
PRAGMA temp_store = MEMORY;

-- Run periodically, or before close:
PRAGMA optimize;

temp_store = MEMORY 会把临时表和临时索引放在内存中,能加快那些需要在没有索引的情况下排序或聚合的查询。

这就是完整的生产环境检查清单。短短六行,SQLite 就从“开发时够用”变成了“能扛住真实负载”。

下一篇:常见错误

即使 PRAGMA 设置得当,你还是会碰到 SQLite 那几个常见的错误:database is locked、disk I/O error、constraint failed。下一页会逐一讲解它们的真正含义以及如何修复。

常见问题

SQLite 中的 PRAGMA 语句是什么?

PRAGMA 是 SQLite 特有的命令,用来读取或修改数据库引擎的行为。你可以像执行 SQL 一样执行它们:PRAGMA journal_mode = WAL; 切换日志模式,PRAGMA foreign_keys; 读取当前值。大多数 PRAGMA 只对当前连接生效,所以通常在打开数据库后立刻执行。

生产环境中应该用哪些 PRAGMA 设置?

对大多数应用来说,一个稳妥的基线是:journal_mode = WAL、synchronous = NORMAL、foreign_keys = ON、busy_timeout = 5000,再加上一个充足的 cache_size。在关闭长时间存活的连接之前执行 PRAGMA optimize。这些设置能让你轻松获得并发读取、持久的写入和参照完整性。

为什么 PRAGMA foreign_keys 默认是关闭的?

为了向后兼容。SQLite 在 3.6.19 版本加入了外键约束检查,并默认保持关闭,以免旧数据库突然开始拒绝写入。你必须在每个新连接上用 PRAGMA foreign_keys = ON; 打开它:这不是数据库级别的设置,而是连接级别的。

PRAGMA optimize 有什么作用?

PRAGMA optimize 执行轻量的维护工作,主要是更新查询规划器选择索引时使用的统计信息。它开销很小,定期执行也很安全。推荐的做法是在关闭长时间存活的连接之前调用它,这样下次应用启动时,规划器就有新的统计信息可用。

Coddy 编程语言插图

和 Coddy 一起学编程

开始