ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SQLite 调优必知:初始化参数、PRAGMA 与 WAL 模式实战指南

SQLite 调优必知:初始化参数、PRAGMA 与 WAL 模式实战指南 SQLite 大概是全世界部署量最大的数据库但也是最容易被“用错”的数据库。很多开发者把 MySQL 那套“改初始化参数”的思路直接搬过来结果发现 SQLite 根本没有 my.cnf 这种东西也不知道该去哪调。这篇文章是“SQLite 五脏俱全系列”的第一篇专门把初始化参数这件事讲透它到底有没有参数、在哪里设置、默认值是什么、改哪些能立竿见影以及不同负载场景下的配置组合。内容面向用过 SQLite 但没系统调优过的人你不需要是 DBA跟着文章把几个 PRAGMA 跑一遍就能明显感觉到差别。1. 先搞明白SQLite 的“初始化参数”到底长什么样1.1 嵌入式数据库的本质决定了它的调优思路SQLite 和 MySQL、PostgreSQL 最大的区别在于它不是一个独立的服务器进程而是一个链接进你程序里的 C 库。没有守护进程没有配置文件没有“启动时加载参数”的传统流程。所以你在网上搜“SQLite 初始化参数”搜出来的东西往往很零散有的是 PRAGMA 语句有的是编译时宏定义还有的是操作系统层面的配置。这里要先建立一个认知SQLite 的“调优”不是一次初始化就能搞定的它分布在三个不同的时间点——编译期编译 SQLite 源码时用宏定义固定默认值、运行期程序连接数据库后用 PRAGMA 语句修改、操作系统期文件系统、内存、磁盘IO对数据库文件的间接影响。这个特性决定了调优 SQLite 的第一原则你在一个连接上设置的参数只对这个连接生效不会全局生效。后面我会细讲这个问题坑了非常多的人——以为在某个窗口执行了一次PRAGMA cache_size-65536就万事大吉结果换了个连接发现缓存还是默认值。1.2 三个层面的参数PRAGMA、编译宏、操作系统具体来说你可以控制的参数分三层PRAGMA 层运行期比如journal_mode、synchronous、cache_size、page_size、temp_store、mmap_size、busy_timeout。这类参数通过 SQL 语句执行灵活随时可以改但只对当前连接或者当前数据库生效。编译宏层编译期比如SQLITE_DEFAULT_CACHE_SIZE、SQLITE_DEFAULT_SYNCHRONOUS、SQLITE_TEMP_STORE、SQLITE_THREADSAFE。这些在编译 SQLite 源码时用-D参数指定相当于给整个库设定默认值运行时再用 PRAGMA 覆盖。操作系统层SQLite 把整个数据库存在一个普通文件里所以文件系统缓存、磁盘类型HDD 还是 SSD、内存大小、内核 IO 调度策略都会影响实际性能。这层经常被忽略但在高频读写场景下影响有时比 SQLite 自身参数还大。1.3 一个误解不能照搬数据库服务器的调优路径我见过有人把 MySQL 的innodb_buffer_pool_size思维套到 SQLite 上试图找个“最大缓存参数”解决所有性能问题。结果是SQLite 的cache_size默认只有 2000 页按 4KB 页算约 8MB就算把它调大到 1GB也不过是一台普通服务器物理内存的一小部分而且 SQLite 默认的页面缓存是只给单个连接使用不是全局共享。照搬服务端数据库的调优路径往往会让 SQLite 变得更糟——比如盲目加大cache_size导致内存占用飙升却没优化最关键的写入策略或者拼命调整page_size却没发现真正的问题是每次写事务都触发了大量同步 fsync。所以这篇系列文章的思路是按“写多读多分析→找到瓶颈→针对瓶颈选参数→验证结果”的顺序来调优而不是背一串参数列表。接下来进入正题按优先级给出运行期参数清单。2. 运行期 PRAGMA 调优清单先改这几个立刻见效2.1 journal_mode优先切到 WAL 模式这是 SQLite 调优里性价比最高的一个参数没有之一。默认情况下 SQLite 使用DELETE日志模式每次写事务都要创建、写入、删除一个-journal回滚日志文件写和读之间还要互相阻塞。这种模式在吞吐量低的小工具里没问题但在并发稍高的 Web 服务或桌面应用里你会频繁撞上database is locked。解决办法就是执行PRAGMA journal_modeWAL;WALWrite-Ahead Logging模式把写操作追加到一个独立的-wal文件中读操作可以直接读原来的主数据库文件写操作只在 WAL 文件尾部追加。这样读和写在绝大多数情况下不互相阻塞多线程并发性能大幅提升。实测对比在一台普通 SSD 的笔记本上同样 1 万条 INSERT 语句DELETE 模式耗时约 3~4 秒切到 WAL 后降到 1 秒以内。如果你的程序是多线程的或者有异步写入的需求WAL 基本是必开的。注意PRAGMA journal_modeWAL执行成功后返回wal如果返回delete说明数据库文件所在文件系统不支持 WAL比如某些网络文件系统那就没办法用这招了。WAL 模式也带来一个副作用多出-wal和-shm两个辅助文件。备份数据库时如果只拷贝主文件有可能丢数据。这是后话会在“常见问题”部分详细讲。2.2 synchronous 与可靠性权衡synchronous这个参数控制 SQLite 在什么时机把数据刷到磁盘上。它的取值有三个取值含义风险0OFF不主动 fsync完全交给操作系统断电或崩溃时可能丢最近写入的数据甚至损坏数据库1NORMALWAL 模式下只在 checkpoint 时同步普通提交不强制 fsyncWAL 模式下崩溃最多丢最后一个事务但库通常不会坏2FULL每次提交都强制 fsync最安全但写入性能最差是默认值很多人一看OFF风险这么高就直接选了NORMAL。但实际上NORMAL和FULL的差别很大程度取决于是否开了 WAL在 DELETE 日志模式下NORMAL和FULL差距不太大但NORMAL有理论上数据库损坏的风险在 WAL 模式下NORMAL是一个很理想的折中崩溃时最多丢最近一次提交的数据不会把整个数据库搞坏。所以我的推荐是只要开了 WALsynchronous可以放心从 FULL 降到 NORMAL。这条组合在“高写入性能”和“能接受断电丢最后一点数据”的场景里几乎是黄金搭档。如果连最后一点数据都不允许丢比如支付对账类应用那synchronousFULL不能动只能靠 WAL 更强的硬件来缓解性能问题。2.3 cache_size 与 page_size内存与磁盘的平衡cache_size决定 SQLite 最多用多少内存来做页面缓存。它是一个“反向直觉”的参数默认值是 2000但单位取决于你传值的方式-- 按页数设置2000 页 PRAGMA cache_size2000; -- 按 KB 设置-65536 表示 64MB PRAGMA cache_size-65536;注意写法正数代表页数负数代表 KB 数0 表示不限制。官方文档推荐用负数按内存大小来设置避免脑内换算页数。调大cache_size对读多写少的场景效果非常明显。如果你有大量重复查询或有一批热点数据远超 8MB默认的 8MB 缓存会导致经常回源读磁盘。我一般建议桌面应用给到 64MB~128MB服务器端可以给到 256MB。page_size就更有讲究了。它决定数据库文件切分成多大的页默认是 4096 字节。这个值必须在新数据库文件创建之前设置否则对已有数据库执行PRAGMA page_size8192不会生效必须配合VACUUM重建才有效。页大小对性能的影响如果一行数据比较大或者大量做范围扫描大页8192 或 16384能减少 IO 次数。但如果主要做点查而且大部分行只有几百字节4096 就够了强行加大页面反而浪费内存。实操心得page_size不是一个需要频繁折腾的参数。除非你是从零设计一个写入量很大的应用否则保持默认 4096 就好。真正影响日常体验的是cache_size和journal_mode。2.4 不容忽视的小参数temp_store、mmap_size、busy_timeout这三个参数看着不起眼但每个都有自己的用武之地。temp_store控制临时表、临时索引和排序用的临时文件存放在哪。取值为 0默认按编译选项、1文件、2内存。如果临时表用得比较频繁PRAGMA temp_store2可以把中间结果放在内存里减少文件 IO。但要小心如果临时数据巨大放内存可能吃不消要多测测再上。mmap_size让 SQLite 通过内存映射的方式访问数据库文件可以绕过传统 read() 系统调用减少内核态用户态切换。在 64 位系统下可以设PRAGMA mmap_size268435456; -- 256MB注意这个参数的默认值在不同版本里不一样我记得 SQLite 3.7.17 之前默认是 0新版默认大约是 256MB 的变体。设成 0 就是彻底关闭。开启 mmap 后大量只读查询的性能会有可感知提升。busy_timeout当多个连接同时写同一个数据库时后到的连接会等待锁释放。不设这个参数的话默认立即报database is locked。建议设置成 ≥3000 毫秒PRAGMA busy_timeout3000;这个参数其实是个“兜底”策略设置了之后偶发锁竞争时程序不会立刻崩溃而是等一下再重试。对桌面应用和移动端比如 uniapp 里内置的 SQLite非常友好。3. 编译期参数把默认行为焊进库3.1 常用编译宏及其作用如果你是自己编译 SQLite 源码或者是用某个 SDK 里预编译的 SQLite 库编译期的宏定义决定了一批默认行为。这块通常不需要普通应用开发者操心但如果你在做一个嵌入式设备端应用或者对性能/体积有极致要求就必须了解了。几个我实际用过的编译宏宏定义作用建议SQLITE_DEFAULT_CACHE_SIZE设置默认 cache_size页数服务器端可设 8000~16000SQLITE_DEFAULT_SYNCHRONOUS设置默认 synchronous需要高性能可编译成 1但风险自担SQLITE_DEFAULT_JOURNAL_MODE设置默认日志模式直接编译成 WAL省去运行时 PRAGMASQLITE_TEMP_STORE设置默认 temp_store设 2 表示默认临时数据放内存SQLITE_THREADSAFE线程安全模式单线程程序可设 0减少锁开销SQLITE_DEFAULT_MMAP_SIZE默认 mmap 大小服务器可设较大值3.2 裁剪特性减体积提速SQLite 几乎什么功能都有但并不是所有场景都要全功能。如果你编译的是面向嵌入式设备或者移动端的版本可以考虑用SQLITE_OMIT_*系列宏裁掉不需要的模块SQLITE_OMIT_FTS3/SQLITE_OMIT_FTS4/SQLITE_OMIT_FTS5全文搜索模块用不到就裁SQLITE_OMIT_JSON不用 JSON 函数就裁SQLITE_OMIT_LOAD_EXTENSION禁用扩展加载既减体积又更安全SQLITE_OMIT_VIRTUALTABLE禁用虚拟表有一说一裁剪对性能的提升通常有限但对二进制体积影响很大。我一个跑在 ARM 芯片上的采集程序裁完之后 SQLite 那块的体积从 1MB 级别降到了 700KB 左右启动也快了一些。3.3 如何确认当前编译配置咱们平时用的大部分 SQLite 库都是别人编译好的根本不知道编译参数长啥样。这时可以用一个命令把编译期选项全部打印出来PRAGMA compile_options;这个命令能列出所有参与编译的宏定义比如有没有开启ENABLE_FTS5、THREADSAFE1等。我排查问题的时候第一步就是先跑它——有些莫名其妙的 SQL 错误比如某个函数不存在往往就是编译时被裁掉了。另外一个有用的命令是SELECT sqlite_version();查看版本号。SQLite 迭代很快3.35 和 3.45 之间的性能差距可能比你调任何参数都大。4. 应用层连接管理参数之外的暗坑4.1 每个连接独立参数要逐连接设置这是 SQLite 新手最容易踩的坑。我见过一个项目在初始化函数里执行了一堆 PRAGMA然后并发场景下用连接池里的其他连接去读写发现journal_mode确实库级生效了但cache_size、busy_timeout、temp_store这些完全是每个连接独立的状态。为什么journal_mode是特殊的那一个因为它修改的是数据库文件结构层面的模式所以是持久化的、库级的。而cache_size、busy_timeout这类是会话级的每个连接关闭后就没了重新打开又是默认值。解决方法是把“初始化 PRAGMA”放进每个连接建立后必须执行的固定流程里。比如用连接池时可以在从池里取连接的环节加一个初始化函数如果用 ORM可以用事件回调在连接创建时统一设置。下面是我常用的连接初始化代码摘录以 Python 为例def init_sqlite_conn(conn, timeout_ms5000): cur conn.cursor() cur.execute(PRAGMA journal_modeWAL) cur.execute(PRAGMA synchronousNORMAL) cur.execute(PRAGMA cache_size-65536) # 64MB cur.execute(PRAGMA temp_storeMEMORY) cur.execute(PRAGMA busy_timeout%d % timeout_ms) cur.execute(PRAGMA foreign_keysON) cur.close()这段代码里最后一行foreign_keysON容易被忽略但它解决的是另一个坑SQLite 默认不开启外键约束。如果你的数据模型里有关联完整性要求这行是刚需。4.2 连接池和 SQLite 的并发模型SQLite 的并发模型和传统数据库完全是两码事它允许同一个数据库文件被多个连接同时打开但“写”是全局串行的——哪怕是 WAL 模式同一时刻也只能有一个连接执行写事务。很多人一开始不理解为什么“并发写入”还是锁。这点直接决定了连接池的设计思路。对这个坑我的经验是读操作可以放多个连接并发执行WAL 模式下不会互相阻塞写操作尽量收敛到一个连接或者用队列把并发写串行化连接池不是越大越好太多连接反而放大锁竞争。一般 4~8 个读连接 1 个写连接就够大部分场景了。如果程序是纯单线程可以把sqlite3_open_v2的 flags 改成SQLITE_OPEN_NOMUTEX减少内部锁的开销不过这属于比较偏门的优化了。4.3 事务与预备语句对调优的影响很多性能问题其实不是参数的问题而是用法的问题。最常见的就是没有用事务包住批量 INSERT。比如往表里插 1 万条记录逐条执行 INSERT 的话每条都是一次独立事务每次都要写 WAL、刷缓存。对比用BEGIN和COMMIT包住 1 万条插入耗时能差 50 倍以上。BEGIN; INSERT INTO t(...) VALUES(...); INSERT INTO t(...) VALUES(...); -- 大量 INSERT COMMIT;再进一步可以用“批处理”套路配合参数绑定用预备语句prepared statement重复绑定参数执行而不是拼 SQL 字符串。这样既能避免 SQL 解析的开销也能防止 SQL 注入。另外一个使用习惯合理使用EXPLAIN QUERY PLAN确保热点查询走了索引。很多“调优”到最后其实只是建了一个合适的索引就解决了 80% 的问题。这个我们在第 6 节里详细说。5. 实操调优方案三种典型负载5.1 读多写少场景怎么配这类场景最典型的就是“配置存储”、“客户端数据缓冲”、“内容管理”。特点是查询次数多但写操作非常少偶尔写一次而且对写入延迟不敏感。推荐配置组合PRAGMA journal_modeWAL; PRAGMA synchronousNORMAL; PRAGMA cache_size-131072; -- 128MB PRAGMA mmap_size268435456; -- 256MB PRAGMA busy_timeout5000; PRAGMA temp_storeMEMORY;重点在于放大读缓存和内存映射。只读为主的负载SQLite 可以完全依赖mmap和 page cache 避免真正的磁盘 IO。我做过一个测试对一个约 2GB 的库做 10 万次随机点查默认配置平均耗时约 0.6ms开启 256MB mmap 后降到 0.2ms 左右。补充一个技巧如果业务上有多个客户端同时读一个库文件可以考虑用PRAGMA query_onlyON把连接设为只读模式。这能减少 SQLite 内部的一些锁和日志判断也让程序更安全防止误写。5.2 高频写入场景怎么配高频写是 SQLite 最吃力的地方因为无论怎么优化它总归是单写者模型。但也不是没办法优化核心思路是“减少提交次数”和“降低单次提交的成本”。推荐配置PRAGMA journal_modeWAL; PRAGMA synchronousNORMAL; PRAGMA wal_autocheckpoint1000; PRAGMA cache_size-65536; PRAGMA busy_timeout10000;另外一个很有用的配置是PRAGMA wal_autocheckpoint它控制 WAL 文件多大之后自动 checkpoint默认 1000 页约 4MB。在高频写入下如果 checkpoint 太频繁反而会引发周期性的性能颠簸。我实测过把wal_autocheckpoint调到 4000~8000 页可以减少 checkpoint 频率但代价是-wal文件更大恢复时间更长。前提是高频写场景必须配合事务批量提交否则任何参数都救不了。比如同样写 5 万条记录一条一提交和 500 条一批提交性能是完全不同的量级。这个场景还要留意锁等待。busy_timeout要设得宽一些如果业务上允许建议在应用层加一个重试机制——SQLite 的SQLITE_BUSY错误是临时的重试一次可能就成功了。5.3 大数据量导入场景怎么配ETL、数据迁移、批量导入是另一种极端场景。它的特点是写量巨大、对事务持久性要求可以放宽、追求最短时间灌完数据。针对一次性导入建议在导入前临时调整参数导入完成后恢复PRAGMA journal_modeOFF; -- 关闭回滚日志如可接受风险 PRAGMA synchronousOFF; -- 取消 fsync PRAGMA cache_size-262144; -- 256MB PRAGMA temp_storeMEMORY;把synchronous关掉事务不强制刷盘因为万一失败可以直接删除数据库重新导入。把journal_modeOFF甚至不需要 WAL 文件减少文件系统开销。但有个细节要注意在journal_modeOFF的状态下执行PRAGMA journal_modeWAL不一定能直接切回来有些版本需要先设置成DELETE再切WAL。导入完成后务必确认日志模式已经恢复正常PRAGMA journal_mode;如果系统对导入期间宕机的容忍度低比如生产环境初始化数据不建议把journal_mode设成 OFF保持 WAL synchronousOFF的平衡点更靠谱。6. 常见问题与排查技巧实录6.1 总是 database is locked 怎么办这个报错几乎是 SQLite 高并发下的头号公敌。看到这个错先按顺序排查检查是否开了 WAL。没开 WAL 时读和写互相阻塞锁问题很频繁开了 WAL 后只剩下写-写互斥。检查 busy_timeout 是否设置。设成 0默认时拿不到锁会立即报错建议设成 ≥3000 毫秒。检查是不是有长事务未提交。一个连接在事务里待太久其他写连接会一直等。这个在应用日志里能看到相似时间段内的“开启事务但没提交”的痕迹。检查是不是连接没有正确关闭。连接持有数据库锁的时候如果程序异常退出SQLite 不会立即释放锁。重启程序或者等一段时间再看。针对最后一种情况排查时可以用一个“终极手段”先把程序全部停掉再删除-wal、-shm和-journal文件然后用PRAGMA integrity_check验证数据库完整性。但这一招要非常谨慎正常生产环境不能随便删辅助文件否则可能丢数据。我一般只在自己本地环境这么干。6.2 WAL 文件疯狂膨胀开了 WAL 后-wal文件可能越积越大如果某些 checkpointer 策略没生效一个巨大的-wal文件会产生两个问题一是启动恢复慢二是磁盘空间持续被占用。先执行手动 checkpointPRAGMA wal_checkpoint(TRUNCATE);这会把 WAL 里的内容合并回主数据库并清空-wal文件。如果反复发生膨胀检查PRAGMA wal_autocheckpoint是不是被设成了很大的值是不是有连接长期开启但从不 checkpoint是不是有某个连接一直持有读快照导致 checkpoint 没法推进。关于第 3 点WAL 模式下 checkpoint 会受阻于“最早活跃读事务”。简单的解决办法是检查代码里是否有“开一个读事务然后长时间不关”的情况。另外可以设置PRAGMA journal_size_limit67108864把 WAL 文件上限限制在 64MB 左右达到上限后自动触发 checkpoint。6.3 查询变慢的定位套路页面聊到调优很多人第一反应是加缓存但你得先搞清楚慢在哪儿。SQLite 里定位慢查询最快的办法是EXPLAIN QUERY PLAN SELECT ...;它会告诉你用的是全表扫描SCAN还是索引查找SEARCH。如果是 SCAN那就该考虑建索引了。比如CREATE INDEX idx_users_name ON users(name);建完索引再看执行计划基本都会变成SEARCH users USING INDEX idx_users_name查询速度提升几个数量级都是正常的。如果你发现加了索引还是慢就要看是不是SQLITE_ENABLE_STAT4没开启——这涉及到统计信息。没有统计信息时SQLite 的查询计划器会“瞎猜”表的数据分布可能导致选错索引。改成 autovacuum 或者重建统计信息也可以缓解ANALYZE;6.4 随手都能用的诊断命令最后整理一份“看一眼就知道库里发生什么”的命令清单建议收藏命令作用PRAGMA journal_mode;看当前日志模式PRAGMA synchronous;看当前同步级别PRAGMA cache_size;看当前缓存设置PRAGMA page_size;看页大小PRAGMA schema_version;看库结构版本PRAGMA integrity_check;检查数据库完整性PRAGMA wal_checkpoint;手动触发 checkpointPRAGMA compile_options;查看编译期选项EXPLAIN QUERY PLAN SELECT ...;分析 SQL 执行计划PRAGMA busy_timeout;查看锁等待超时用图形化工具的话DB Browser for SQLite 可以直接跑 PRAGMA 并查看结果Navicat for SQLite 也内置了类似的命令行窗口对于日常查看表结构和数据很方便。不过真正常用的还是命令行模式因为可以在脚本里批量执行、留痕。7. 写在最后的几句体会SQLite 的调优思路和大型数据库完全不同。它没有“全局参数文件”调优更像是“根据场景在每个连接上选择合理的默认行为”。我实际项目里90% 的性能问题其实只用到了三个动作开 WAL、降 synchronous、批量事务。至于cache_size、mmap_size、page_size这些参数属于压在箱底的工具遇到具体场景才拿出来细调。另外再提醒一次每次调整参数后都要用一个相对接近生产的压测脚本去验证。我踩过最深的坑就是只在一个连接里测试 WAL 效果结果上线后并发一上来立刻冒出大量锁等待最后才发现是busy_timeout没设。这个系列后面还会聊事务、锁、索引和 SQL 写法优化先把初始化参数这关过了整体性能就不会差到哪去。
返回列表