7.1 页缓存与 PRAGMA 算术


7.1 页缓存与 PRAGMA:cache_size 的算术

本节摘要:页缓存(page cache)是 SQLite 读路径的最后一站缓存,默认配置小到几乎等于没有。本节讲清它的工作位置与淘汰规则,算清 cache_size 正负两种单位的换算账,给出一组按场景分档的配置基线,并用数字说明命中率如何决定点查是微秒还是毫秒。

页缓存住在哪里,怎么淘汰

第 1 章的五层图里,页缓存在 pager 层——所有 B-Tree 的页访问都要过它的手。读一页时先查缓存:命中则直接返回内存地址(纳秒级);未命中则发起一次 VFS 读(冷缓存时是真实的磁盘 IO,毫秒级)。写一页时页变"脏",留在缓存里,事务提交时按日志模式决定刷盘时机(第 4 章的账本故事)。

淘汰规则朴素:缓存满时按 LRU 近似回收干净页优先——脏页要先过日志流程才能让位,所以脏页比例高的库淘汰成本更高。每个连接的缓存彼此独立(这是进程内引擎的自然结果),一百个连接就是一百份缓存——这一点与 MySQL 共享缓冲池、PostgreSQL 共享缓冲区的"全局一份"结构截然相反,是 7.2 节的核心对照点。

cache_size 的两种单位

这是 SQLite 配置里最容易算错的一笔账:

PRAGMA cache_size = 2000; -- 正数:单位是"页"。2000 页 = 2000 × 4096 = 8.2MB PRAGMA cache_size = -8000; -- 负数:单位是 KB。等价于 8000KB ≈ 7.8MB PRAGMA cache_size; -- 查询当前值

正数按页、负数按 KB。写成 cache_size = 8000 以为给了 8MB 实际给了 32MB——页大小 4096 时正数 8000 页是 32MB。建议统一用负数写法,语义与直觉一致。另一个常见误区:cache_size 是每连接的预算,五十个连接各 10MB 就是 500MB——移动应用里多线程各开连接时,这笔账要乘上并发数再核。

图:页缓存命中路径与预算分档

图:页缓存命中路径与预算分档

一组可直接抄的配置基线

把本章与第 4 章的旋钮合并成三行初始化语句,覆盖绝大多数场景:

PRAGMA journal_mode = WAL; -- 读写并行(第 4 章) PRAGMA synchronous = NORMAL; -- WAL 下的安全快档(第 4 章) PRAGMA cache_size = -65536; -- 每连接 64MB,按并发数乘出总预算 PRAGMA busy_timeout = 5000; -- 撞锁等待(第 4 章) PRAGMA foreign_keys = ON; -- 外键约束默认是关的,按需开 PRAGMA temp_store = MEMORY; -- 排序与临时表进内存(内存充裕时)

判断缓存够不够,别拍脑袋,用 PRAGMA cache_stats(3.4x 版本的部分构建提供)或观察 sqlite3_status 的页缓存命中计数。更朴素的办法:把库文件大小与缓存预算比一比——库 2GB、每连接 64MB、4 个连接,热区覆盖率本就有限,此时该优化的是索引让热区变小,而不是无限加缓存。

与服务端配置哲学的对照

InnoDB 的 innodb_buffer_pool_size 与 PostgreSQL 的 shared_buffers 都是全局一份、服务独占的内存池,配置时可以放心给到机器内存的一半以上(还要留文件系统缓存的分额)。SQLite 没有这份奢侈:每个连接一份缓存、与应用共享堆、没有后台线程替你预热。所以服务端调优的口诀是"给够",SQLite 调优的口诀是"算准"——预算 = 单连接缓存 × 连接数 + mmap 共享映射 + 应用自身开销,三者加起来不能超过设备给这次任务划的线。这也是为什么移动端团队会把数据库内存写进应用的内存水位表统一管理,而服务端 DBA 习惯把数据库进程当成内存的唯一主人。

一个完整的预算实例

把本章的算术走一遍全流程。场景:内容管理应用,库 1.2GB,峰值并发四个读连接加一个写连接,设备内存 8GB、应用自身占用约 1.5GB。

第一步 列预算项: 写连接 cache_size -65536 = 64MB 读连接 ×4 各 -32768 = 128MB temp_store 峰值(最大排序约) = 64MB 语句与 schema 缓存 ≈ 8MB 合计堆占用 ≈ 264MB 第二步 mmap 划线: 热区是全部文章正文索引约 400MB mmap_size = 536870912(512MB)覆盖热区,占地址空间不占堆 第三步 验证: 压测下 sqlite3_status 的内存峰值 ≈ 280MB,远低于给数据层 划的 512MB 预算 → 配置安全,还有余量

这个例子的方法比数字重要:先列项、再划线、后验证。服务端 DBA 习惯的"给 buffer pool 一半内存"在这里行不通,因为 SQLite 的内存账是乘法(缓存乘连接数)加加法(临时峰值),一笔笔算出来才敢签字。

常见问题速答

**内存翻倍,数据库就快一倍吗?**不是线性关系。缓存的收益集中在"热区能否装下"这个临界点上:热区 300MB,缓存从 200MB 加到 320MB,命中率跳升、延迟骤降;从 320MB 加到 640MB,几乎没有任何变化。先量热区(dbstat 的表加索引页数),再让缓存预算略大于热区,多余的一分钱都是浪费——这与缓冲池"越大越好"的服务端直觉正好相反。

**cache_size 改完立刻生效吗?**生效,但作用是"上限"不是"预分配"——SQLite 不会一次性抓住预算内存,而是按需增长到上限。调小则即时生效,超过新上限的缓存页逐步淘汰。所以调优实验可以大胆做:改小不会立刻导致内存骤降毛刺,改大也不会预支内存,观察命中率变化即可。

本节要点回顾

  • 页缓存在 pager 层、每连接独立;脏页淘汰成本高于干净页。
  • cache_size 正数按页、负数按 KB;总预算要乘连接数,这是 SQLite 与服务端配置哲学的分水岭。
  • 三档基线:移动端 2MB、桌面 64MB、重载 1GB 低并发;命中率决定点查在微秒与毫秒之间的落位。
  • 缓存不够时先想索引(缩小热区),再想加缓存——顺序别反。

**WAL 模式会额外吃多少内存?**有一份额外账目值得知道:shm 文件映射与写事务的 WAL 缓冲,通常固定在百 KB 到几 MB 级,远小于页缓存预算,但极小内存设备(几百 KB 级可用内存的 MCU 场景)上也要计入。常规应用不必为 WAL 单列预算——它改变的是日志形态,不是内存量级。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U