SQLite在默认情况下通过read()系统调用读取数据库文件,每一次读页面都要经历从内核缓冲区到用户空间的拷贝,还要承担系统调用本身的开销。而当设置了PRAGMA mmap_size之后,SQLite会把数据库文件的一部分甚至全部映射到进程的虚拟地址空间,读取数据就像访问普通内存数组一样直接,省去了拷贝和系统调用。这个特性从SQLite 3.7.17版本开始引入,是官方明确推荐的性能优化手段之一,但很多使用者对它的工作机制和副作用了解不深,导致要么不敢用,要么用出问题。本文把这件事从头到尾讲清楚。

mmap_size的工作原理与基本语法
先看语法,这条指令的形式非常简单:
-- 查询当前配置 PRAGMA mmap_size; -- 设置最大映射大小为 1GB PRAGMA mmap_size = 1073741824; -- 关闭内存映射,回到传统 read 模式 PRAGMA mmap_size = 0;
返回值就是当前生效的最大映射字节数。要注意的是,PRAGMA mmap_SIZE设置的是一个上限,不是实际映射量。SQLite采用按需增长策略:随着读取的页面越来越远,映射窗口会逐步扩大,直到达到这个上限或者进程地址空间不足以再扩张为止。设置成0表示完全禁用mmap,这也是绝大多数平台上的默认值,只有少数嵌入式平台默认开启。
它背后的原理是操作系统提供的mmap()系统调用。进程拿到一段映射地址后,第一次访问某个页面会触发缺页中断,内核把对应的文件块加载进页缓存并建立页表映射,之后所有读取都变成普通的内存访问。这带来两个直接收益:一是省掉了内核态到用户态的数据拷贝,二是省掉了每次读页面时的系统调用开销。对于随机读密集型的查询,比如大量使用索引的点查,收益相当明显。
还有一个容易被忽略的点:mmap_size是每个数据库连接独立的设置,不是数据库文件本身的属性。同一个文件,A连接开了映射,B连接不开,互不影响。所以如果你的应用用了连接池,要保证每条连接建立后都执行一次这条PRAGMA,否则部分连接享受不到优化。另外,SQLITE_MAX_MMAP_SIZE编译期选项决定了运行期可设置的最大值,默认是1GB,如果你想设得更大,需要重新编译SQLite。
性能收益实测与读写场景差异
内存映射主要优化的是读操作。写操作走的仍然是WAL日志和页写入路径,mmap对写几乎没帮助。所以判断该不该开,第一步是看你的负载类型。典型的受益场景包括:只读或读多写少的配置库、嵌入式设备上的本地缓存库、以查询为主的报表类应用。官方文档给出的参考数据是,在大表全表扫描的场景下,读速度提升约百分之十,而在大量随机索引查询中提升更明显。
可以自己动手验证。写一段测试代码,先用mmap_size=0跑一遍查询,再开映射跑一遍:
#include <stdio.h>
#include "sqlite3.h"
int main(void) {
sqlite3 *db;
sqlite3_open("test.db", &db);
// 关闭映射基线测试
sqlite3_exec(db, "PRAGMA mmap_size=0;", 0, 0, 0);
run_benchmark(db);
// 开启 256MB 映射
sqlite3_exec(db, "PRAGMA mmap_size=268435456;", 0, 0, 0);
run_benchmark(db);
sqlite3_close(db);
return 0;
}实测中你会发现数据库文件大小对效果影响很大。文件比上限小得多时,整个文件都进了映射,收益最大;文件远大于上限时,SQLite需要在映射窗口和传统read之间来回切换,反而可能出现轻微的性能抖动。一个经验性的设置是让mmap_size略大于或等于数据库文件的典型大小,如果文件会持续增长,就设一个你能接受的内存占用上限。
还要区分物理内存占用和虚拟地址占用这两个概念。mmap分配的是虚拟地址空间,实际物理内存由操作系统的页缓存统一管理,内核可以在内存紧张时回收这些页面。所以设置一个远大于文件大小的mmap_size并不会立即吃掉那么多物理内存,但在32位进程上会迅速耗尽宝贵的虚拟地址空间(通常只有2GB到3GB可用),导致映射失败回退或直接报错。64位系统则基本不用担心地址空间问题。
潜在风险与多进程并发注意事项
mmap最大的坑来自信号处理。当数据库文件被另一个进程截断(比如有人执行了VACUUM重建文件,或运维脚本误用truncate)时,访问已映射但已被截断的区域会触发SIGBUS信号,默认行为是进程直接崩溃。SQLite内部对这类情况做了一定防护,但应用层如果在映射期间持有文件操作,风险仍然存在。规范的做法是保证只有SQLite自己管理这个文件,禁止外部工具在库打开期间做截断类操作。
第二个注意点是WAL模式下的行为。WAL模式下mmap_size只作用于主数据库文件,WAL日志文件本身仍然用传统方式读写,而且当WAL需要重启(checkpoint后reset)时,映射关系可能失效,SQLite会自动处理,但在极端高并发写入下偶尔能看到性能毛刺。如果你的场景是写多读少,开mmap的意义不大,还平添复杂度。
第三个是稳定性和平台差异。某些文件系统比如网络文件系统NFS、部分FUSE实现,对mmap的支持有各种问题,官方明确建议在NFS上禁用mmap。老版本的一些Android系统内核在并发mmap时也有已知的bug。稳妥的方案是把设置放在可配置项里,出问题时可以快速回退为0。下面是一段Python示例,展示在应用启动时的安全配置方式:
import sqlite3
def open_db(path):
conn = sqlite3.connect(path)
# 读取为主,文件约几百MB,开启 512MB 映射
conn.execute("PRAGMA mmap_size=536870912;")
conn.execute("PRAGMA journal_mode=WAL;")
conn.execute("PRAGMA cache_size=-8000;")
return conn
conn = open_db("app.db")
print(conn.execute("PRAGMA mmap_size;").fetchone())
conn.close()最后给一组配置建议。只读配置库或字典库:直接设为文件大小加一些余量,比如64MB到256MB。读多写少的业务库:设为256MB到1GB,配合WAL模式使用。写多读少或文件在网络存储上:保持默认值0,不折腾。32位程序:保守起见不超过512MB,或者干脆不开。无论哪种场景,上线前务必在目标平台上做一轮完整的功能与稳定性回归测试,确认没有信号异常和句柄泄漏,再让它进入生产环境。
PRAGMA mmap_sizeSQLite内存映射数据库性能优化修改时间:2026-09-10 01:46:41