ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SQLite调优实战:初始化参数与PRAGMA配置从入门到精通

2026/9/9 22:13:34 拓冰建站 浏览量
SQLite调优实战:初始化参数与PRAGMA配置从入门到精通 聊到 SQLite很多人第一反应是“轻量”“嵌入式”“零配置”然后反手一句“这玩意儿还需要调优初始化参数在哪”。说实话我第一次接触 SQLite 的时候也是这个想法。后来在移动端、桌面工具、甚至服务端边缘计算场景里把它往死里压过一段时间才慢慢意识到——SQLite 并不是“没有参数”而是它的参数体系跟 MySQL、PostgreSQL 那套完全不一样。它把很多配置藏在 PRAGMA 里用不用得起、调不调得好直接决定你的应用并发高了之后是丝般顺滑还是整天 “database is locked”。这篇是“SQLite 五脏俱全”系列的第一篇我先从初始化参数和调优这个最常被误解的入口讲起。适合谁看主要三类人在 Android/iOS 或 uni-app 里用 SQLite 做本地存储、用 C/Qt 把数据落地的桌面开发以及把 SQLite 当生产数据库单机来用的后端开发。你会在这篇里搞清楚 SQLite 到底有哪些真正值得调的参数怎么验证调整效果以及我在实际项目中踩过哪些坑。1. 先搞明白SQLite 的“初始化参数”到底藏在哪里1.1 没有配置文件不代表没有参数用过 MySQL 的朋友对 my.cnf 一定不陌生改完重启就生效。SQLite 作为嵌入式库走的是完全另一套思路它没有一个集中式的服务进程也没有全局配置文件。按理说库都没了参数往哪放答案是参数分三个层面存在。第一层是编译期参数。也就是你在编译 SQLite 源码时通过 C 预处理宏指定的比如是否启用 WAL、默认 page size、是否开启 FTS5 全文检索、是否支持 JSON1 扩展函数。这类参数一旦编译成库运行期无法修改。拿我常用的一个例子默认的 SQLITE_DEFAULT_PAGE_SIZE 在旧版本里是 1024在较新版本里已经跑到 4096。如果你的表行宽比较大编译期没调后期只能靠 PRAGMA page_size 在建库前设置一旦建库完成再改就必须做 VACUUM麻烦不少。第二层是连接期 PRAGMA。这一层就是大家最有感知的“初始化参数”。只要你打开一个数据库连接就能通过PRAGMA 名称 值这样的语句去设置。比如最出名的PRAGMA journal_mode WAL;就是在这里执行。它有几个很有意思的特性部分 PRAGMA 是持久化的会写进数据库文件头比如 journal_mode、page_size改一次之后就算换一个连接也依旧生效而像 cache_size、mmap_size、synchronous 这类属于连接级状态默认只对当前连接有效连接关闭就丢。第三层是运行期查询。用PRAGMA不带参数地执行一遍能读到当前连接各种状态的实时值比如PRAGMA cache_size;、PRAGMA journal_mode;。这层对诊断问题特别重要很多“我明明改了没生效”的困惑都是因为没先查一遍当前值。所以如果有人问“SQLite 需要初始化参数吗”我的标准回答是不需要传统意义上的初始化但如果你想拿到高性能必须在打开连接之后立刻做一套 PRAGMA 调优。1.2 为什么说“调 SQLite 就是调 PRAGMA”SQLite 之所以把调优手段做成 PRAGMA是因为它背后的设计哲学是“程序嵌入而非服务部署”。在嵌入式场景里没有 DBA 去维护配置数据库和宿主程序共享同一份资源所以参数必须足够简洁、足够局部、足够见缝插针。比如最常见的并发场景。默认情况下一个 SQLite 库同一时间只允许一个连接写数据所有写操作串行排队。如果你的应用不需要高并发那默认配置确实够用但只要你的程序一开多线程或者多个进程共享同一个库文件默认参数就会瞬间变成瓶颈。这时候你第一个要调的就是 journal_mode 和 busy_timeout。再比如频繁的执行大量 INSERT。如果每次写一条数据都把事务提交一次SQLite 会频繁刷日志和写主数据库文件磁盘 I/O 直接拉满。但如果你把事务包成一个大事务配合合适的 synchronous 级别性能差距可能达到几十倍。这些优化不是靠数据库自动完成的得你在代码里和 PRAGMA 里一起控制。所以把 SQLite 调优理解为“PRAGMA 组合拳”一点不过分。2. 核心参数逐个拆解每个参数改的是什么东西2.1 journal_mode决定你的并发上限journal_mode 是 SQLite 最重要的一个参数没有之一。它的取值包括 DELETE、TRUNCATE、PERSIST、MEMORY、WAL、OFF其中生产环境里最常用的就是默认的 DELETE也叫 rollback journal 模式和 WALWrite-Ahead Logging预写日志。DELETE 模式的工作方式可以这样理解每次写事务开始前SQLite 把要修改的原始页面内容拷贝到一个独立回滚日志文件里事务提交后回滚日志立刻删除。这个模式的缺陷在于任何时刻数据库文件本身只能被一个写连接持有读操作虽然可以并发但如果读发生在写事务中间也可能被阻塞或返回 SQLITE_BUSY。WAL 模式解决的就是这个问题。它不再把改动直接写到主数据库文件而是追加写到一个独立的-wal日志文件里。写连接只管顺序追加日志读连接照常读主文件读和写真正做到了互不阻塞。这里有个直观的数据在 WAL 下一个进程内的多线程读写的并发度远高于默认模式把多个进程指向同一个库文件也比 DELETE 模式稳得多。我一般会在建库之后立刻执行PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL;值得注意journal_mode 是持久化参数设置后产出的 WAL 状态会写进数据库头后续连接默认就是 WAL不需要每次重设。如果选 MEMORY 或 OFF对写入性能提升更夸张但代价是数据库可能损坏或丢数据我强烈不建议在生产环境碰。2.2 synchronous用安全换性能的关键开关synchronous 控制的是 SQLite 何时把 WAL 内容或回滚日志 fsync 到磁盘。它有三个级别FULL、NORMAL、OFF。默认 FULL 是最安全的。在 DELETE 模式下FULL 意味着每一步关键写入都要求数据落到磁盘掉电不会丢事务。但代价是大量磁盘同步操作写入性能会明显打折。在 WAL 模式下NORMAL 和 FULL 的行为又不一样。WAL NORMAL 是一个非常有名的组合它会保证 WAL 文件完整同步但不会在每次事务提交时都对主数据库文件做 fsync。这样做的实际效果是如果系统崩溃或断电最多损失最近一小部分已经提交的事务通常可以恢复但数据库整体结构不太会损坏读取永远能拿到一个一致快照。这也是我推荐生产环境用WAL NORMAL的原因。OFF 就是完全交给操作系统极致性能但风险极高一旦进程崩溃或掉电数据库文件很可能直接损坏且无法恢复。自己临时写个小工具、存一些可再生的缓存数据你可以考虑 OFF正经业务数据千万别用。2.3 cache_size让热数据留在内存里cache_size 控制的是 SQLite 每个连接用来缓存数据库页面的内存上限单位默认是页不是字节。算一下很容易如果 page_size 是 4096PRAGMA cache_size -64000;这种写法里的负数表示以 KiB 为单位也就是设置 64MB 缓存。那么问题来了为什么默认缓存只有 2MB 左右因为 SQLite 是嵌入式库默认目标是不能在别人手机上反手吃掉几百 MB 内存。但如果你的设备内存足够调大 cache_size 往往比加索引还立竿见影。你可以在连接初始化时执行PRAGMA cache_size -65536;注意这里的负号是有意义的。正数表示“页数”负数表示“KiB”。例如 -65536 就是 64MiB写起来比PRAGMA cache_size 16384;页数如果页大小 4KB 也是 64MiB更直观不容易受 page_size 影响。cache_size 是连接级参数不会持久化。所以如果期望每个连接都有大缓存就得在每个连接打开时都执行一遍。这里有个常见的坑在连接池场景下如果你只在初始化第一个连接时设了 cache_size其他连接可能还是默认值。2.4 mmap_size让读操作少拷贝一次mmap_size 是很多人忽略的调优点。它允许 SQLite 把数据库文件映射到进程地址空间读数据的时候直接访问内存映射区域减少一次从内核态到用户态的拷贝对大量只读查询很有帮助。设置方式PRAGMA mmap_size 268435456;这个值是字节数上面这句设置约 256MB。并不是所有环境都适合开很大因为 mmap 区域会占用进程虚拟内存空间。32 位进程要特别谨慎映射太大可能撑爆地址空间64 位环境基本可以放开一点。有个小经验如果数据库文件非常小mmap 的好处并不明显当单库文件超过 100MB并且你的热点读很密集mmap_size 带来的提升才比较容易感知到。2.5 temp_store临时表去哪儿temp_store 控制临时表比如 ORDER BY、DISTINCT、子查询中间结果是放在内存还是磁盘。取值 FILE、MEMORY、DEFAULT。瓶颈在于用 FILE临时数据写到磁盘不占内存但慢用 MEMORY占用应用内存但快。如果你查询经常产生大量中间结果又不想动 SQL 逻辑可以先试PRAGMA temp_store MEMORY;。但千万注意过大的排序结果全塞内存可能会直接把内存吃满触发 OOM尤其是移动端。我自己在桌面工具里开 MEMORY移动端保持默认就是一种取舍场景不同不能一刀切。2.6 busy_timeout 与 locking_mode并发问题的两个侧面SQLite 严格来说不擅长高并发写入但通过 busy_timeout 可以极大改善多连接竞争时的体验。它表示一个连接在获取到数据库锁失败后愿意等待的毫秒数。如果不设置默认可能是 0也就是说一遇到锁直接返回 SQLITE_BUSY应用层不加处理就会报错。设置方式很简单PRAGMA busy_timeout 5000;这个参数是连接级的建议在每个连接初始化的时候立刻设置。它不能真正解决并发写冲突但能让锁竞争变得有“弹性”而不是动不动就抛异常。locking_mode 通常在涉及到跨进程共享同一个数据库文件时使用。默认 NORMAL 下如果两个进程同时访问会依赖操作系统文件锁如果需要长期持有锁可以试试PRAGMA locking_mode EXCLUSIVE;。代价是别的进程基本读不到数据所以一般只在专用场景下用。2.7 其他值得知道的参数page_size、wal_autocheckpoint、foreign_keyspage_size 决定数据库页大小建库前设置一次之后不能直接改。BTree 的节点大小就是 page_size如果你的单行数据较大、索引较多较大的页可以让一次磁盘 I/O 读到更多有效数据。默认 4096 其实已经适合大多数场景除非你有大量宽表可以建库前设成 8192。wal_autocheckpoint 控制 WAL 文件自动 checkpoint 的阈值默认 1000 页。也就是说 WAL 文件积累到约 4MB1000 页 * 4KB时SQLite 自动把 WAL 内容合并回主库。checkpoint 本身也会占用 I/O如果写非常密集可以适当调大。例如在批量导数据的时候我会设成较大的值甚至 0关闭自动 checkpoint导入完成再手动执行PRAGMA wal_checkpoint(TRUNCATE);foreign_keys 默认是关闭的也就是说外键约束并不会自动生效。这不是性能参数但属于“初始化参数”里非常重要的一个。如果你依赖外键级联删除或完整性校验记得在每个连接上执行PRAGMA foreign_keys ON;为什么是“每个连接”因为 foreign_keys 也是连接级参数不持久化。很多新手在 SQLite 里发现外键“没生效”十有八九是忘了这一条。下面把常用的参数整理成一个对照表方便快速查阅参数名核心作用是否持久化推荐值一般场景journal_mode日志模式决定读写并发是WALsynchronous同步级别影响安全与性能否WAL NORMALcache_size页缓存大小否-6553664MB左右mmap_size内存映射大小否268435456256MBtemp_store临时表存储位置否MEMORY / FILE 按需busy_timeout锁等待毫秒数否5000page_size数据库页大小是建库前4096 / 8192wal_autocheckpointWAL 自动合并阈值否1000可调大foreign_keys外键约束是否启用否ON3. 动手设置参数初始化流程与验证方法3.1 用 sqlite3 CLI 完成建库和参数设置命令行是最直接的验证方式。假设我要新建一个名为 app.db 的数据库并做全套初始化sqlite3 app.db进入交互环境后先设 journal_mode。这里有个细节如果你在库还没建表的时候设置 WALSQLite 会先创建一个空库然后返回当前 journal_mode 值PRAGMA journal_mode WAL; -- 期望输出wal接着设置其他运行时参数PRAGMA synchronous NORMAL; PRAGMA busy_timeout 5000; PRAGMA cache_size -65536; PRAGMA temp_store MEMORY; PRAGMA foreign_keys ON;然后创建表、写入测试数据、查询参数确认生效CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, score INTEGER DEFAULT 0 ); PRAGMA journal_mode; -- wal PRAGMA synchronous; -- 1对应 NORMAL PRAGMA cache_size; -- -65536这里你会注意到PRAGMA synchronous 查询出来的是 1而不是字符串 NORMAL。因为 SQLite 内部就是用 0、1、2 表示 OFF、NORMAL、FULL。同理journal_mode 查询出来是小写的文本因为它是持久化属性需要以字符串存进文件头。验证完可以执行一个简单的插入测试感受一下参数调整前后的差别。比如先建一张只有一列的表然后循环插入 10000 条数据分别测“不开事务、默认同步级别”和“开事务、WALNORMAL”的耗时。不用精确到毫秒级差不多量级的区别就能让人印象深刻。3.2 在应用代码里怎么“初始化参数”CLI 验证结束应用代码里就是一套“连接即初始化”的固定流程。我用一个 C 的例子说明因为 C 是我们后端工具链里很常用的语言#include sqlite3.h sqlite3* db nullptr; int rc sqlite3_open(app.db, db); if (rc ! SQLITE_OK) { // 处理错误 } // 设置 busy_timeout避免并发时立刻报错 sqlite3_busy_timeout(db, 5000); // 执行 PRAGMA 初始化 const char* pragmas[] { PRAGMA journal_mode WAL;, PRAGMA synchronous NORMAL;, PRAGMA cache_size -65536;, PRAGMA temp_store MEMORY;, PRAGMA foreign_keys ON; }; for (const char* sql : pragmas) { char* errMsg nullptr; rc sqlite3_exec(db, sql, nullptr, nullptr, errMsg); if (rc ! SQLITE_OK) { // 记录 errMsg并释放 sqlite3_free(errMsg); } }这套写法的核心思路是把 PRAGMA 初始化固定成一个函数在每次打开数据库后调用。尤其是用了连接池的项目每个连接都必须执行一遍不能只初始化第一个连接。在 C 里也可以用 C20 的 jthread 做连接级线程检查但为了控制篇幅这里不展开。如果你在 Python 里用 sqlite3 模块同样也是在拿到 connection 对象后立刻 execute 一串 PRAGMAimport sqlite3 conn sqlite3.connect(app.db, timeout5) conn.execute(PRAGMA journal_modeWAL) conn.execute(PRAGMA synchronousNORMAL) conn.execute(PRAGMA cache_size-65536) conn.execute(PRAGMA foreign_keysON)其实几乎所有语言封装的 SQLite 驱动都保留了对原始 SQL 语句的透传能力所以 PRAGMA 的设置逻辑可以完全复用。3.3 用 DB Browser for SQLite 怎么可视化调参很多朋友喜欢用 DB Browser for SQLite 或者 Navicat for SQLite 来操作库不太习惯写命令行。这件事也能做。DB Browser for SQLite 自带一个“Execute SQL”标签页你可以在里面直接输入上面那些 PRAGMA 语句并执行效果和命令行一致。尤其适合快速验证某个参数设置后再去看“Database Structure”或“Browse Data”是否正常。比如你在界面上看到某个表的外键约束没有生效直接执行一句PRAGMA foreign_keys ON;就能当场验证。Navicat for SQLite 相对更图形化一些同样可以在查询窗口执行 PRAGMA。注意一点这些图形化工具自带的连接不一定替你执行过初始化参数所以你在工具里测试的性能数据未必代表你应用代码连接后的真实表现。4. 调优案例从默认配置到百万行写入4.1 场景描述与性能基线我拿一个前阵子做的桌面端数据采集工具举例。程序每秒钟要接收若干条传感器数据落库到本地 SQLite。单次采集的数据量不大但一天累积下来可能就是上百万行。最初程序用的是最原始的写法每来一条数据单独执行一个 INSERT连接保持默认配置事务全靠自动提交。跑了一个小时后采集端开始出现明显的延迟堆积数据库文件对应的目录里经常能看到临时日志文件并且偶尔弹出 “database table is locked” 错误。我做的第一件事是查基线数据。用一个简单的测试脚本模拟程序行为循环执行 10000 条单条 INSERT记录耗时。当时的结果是在默认配置下10000 条单条 INSERT 大约耗时 1.8 秒这还没有算上应用层其他逻辑。如果设备持续跑一天这个性能完全撑不住。4.2 参数调整与实测对比我先做了第一轮调整开启 WAL并把 synchronous 降到 NORMAL。PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL;同一个脚本10000 条单条 INSERT 的耗时降到约 1.1 秒。有提升但还不够明显。原因很简单单条自动提交模式下每次 INSERT 仍然要做一次事务提交WAL 日志文件被反复写fsync 虽然被 NORMAL 削弱了但文件写入的开销还在。第二轮我在代码层面把 INSERT 包进显式事务conn.execute(BEGIN) for item in data_list: conn.execute(INSERT INTO sensor VALUES (?, ?), item) conn.commit()同时把 cache_size 调大一点PRAGMA cache_size -65536;结果非常夸张10000 条插入的总耗时降到约 0.05 秒也就是 50 毫秒左右比第一轮快了一个数量级以上。这里面的关键不是 cache_size而是“显式事务”把成千上万次磁盘同步压成了一两次。第三轮我又试着把 temp_store 设为 MEMORY并稍微调整了 wal_autocheckpointPRAGMA temp_store MEMORY; PRAGMA wal_autocheckpoint 2000;排序类查询的速度有一定提升但写入这块基本没有变化因为全表扫和单条插入很少用到临时表。这也能体现一个思路调优不是把所有参数都拉满而是针对瓶颈去调。4.3 批量写入的通用调优套路从上面这个案例可以提炼出一套非常通用的批量调优步骤第一能用事务就用事务。这是所有 SQLite 写入优化的第一原则。单条提交意味着每次都要落盘一次把 10000 条合并到一个事务里性能提升是数量级的。第二把 journal_mode 设成 WALsynchronous 设成 NORMAL。WAL 模式降低了读写锁的竞争NORMAL 则减少了数据落盘的频率。这两个组合在一起对绝大多数读写混合场景都是安全又高效的平衡点。第三cache_size 按设备内存预算来。如果桌面端内存 16GB给 SQLite 划分 64MB 缓存完全不影响移动端如果内存紧张默认值也不是不能用只是大量范围查询会比较明显。第四如果数据是一次性灌入的比如导旧数据、跑初始化脚本甚至可以临时把 synchronous 设为 OFF等导入完成后再改回 NORMAL。这一点非常适合那种“一次性把 1000 万行历史数据灌进去”的场景性能差异可以达到两三倍。4.4 只读场景的调优侧重如果你主要是在做只读查询比如把 SQLite 当作一个本地只读数据库分发给用户调优思路又不一样。这时候写入优化几乎不重要重点应该放在 mmap_size、cache_size 和合适的索引上。只读场景下我会建议把 mmap_size 调得比较大比如 512MB 甚至更高让热数据尽量走内存映射通道。cache_size 也可以放开因为只读连接不会产生脏页缓存的换页成本很低。再配合 WAL 模式即使有一个独立的写进程在更新数据只读查询也能顺畅读取。另外一个容易被忽略的是PRAGMA query_only ON;这个参数设成 ON 之后连接彻底拒绝写操作能在架构上防止误写。它不算严格意义上的调优参数但在只读分发场景里非常实用。5. 常见问题与排查技巧实录5.1 “PRAGMA journal_mode WAL”返回了 delete但没有报错这个问题出现频率最高。原因是你设置 WAL 的时候数据库可能已经存在并且有其他连接占用。SQLite 只有在没有任何连接持有共享锁或排他锁时才能切换日志模式如果当前会话或其他会话已经锁定了数据库这次修改会静默失败返回的还是原来的模式。排查方法很简单执行前先确认没有并发连接或者直接在一个干净连接里执行。设置完成后立刻用PRAGMA journal_mode;查返回值必须是 wal 才算成功。很多坑都是因为“以为成功了其实没有”造成的。还有一种情况数据库文件存放在文件系统不支持共享内存的目录里比如某些网络磁盘、FUSE 挂载WAL 也需要共享内存支持失败时也会表现出类似问题。解决办法是把库文件挪到本地盘或者干脆在初始化逻辑里探测式设置如果失败就降级。5.2 database is locked 到底是什么原因“database is locked”是 SQLite 群里每天都会出现的问题。原因分类大致如下现象可能原因优先排查高频写事务没合并提交太频繁检查代码里是否每个写操作都自动提交多进程并发没有设置 busy_timeout连接初始化时加PRAGMA busy_timeout5000;长事务写事务长时间不提交阻塞其他连接查看代码里是否有忘记 commit 的分支死锁AB-BA 模式跨连接加锁规范事务顺序尽量只用一个写连接归档/备份外部程序读取时持锁避免直接用文件复制方式备份 WAL 数据库如果用的是 uniapp 或移动端还会遇到一个特殊场景应用前后台切换时之前的事务没有提交系统把进程挂起然后新的连接一写就读不到锁报错看起来就是“database is locked”。5.3 synchronous 设成 NORMAL 之后会不会丢数据这也是被问得最多的问题之一。坦白说在 WAL 模式下NORMAL 并不等于危险。它保证每个事务提交时 WAL 文件被可靠写入只是可能延迟把 WAL 合并回主库文件的时机。如果系统崩溃最多出现的是“最近提交的一些事务丢失”但这种丢失通常仅限于最后一次 checkpoint 之后的数据而且数据库文件本身不会因此损坏。如果你的业务要求严格不丢数据那就保持 FULL如果对“万一断电丢最近几毫秒数据”可以接受那 NORMAL 换来的性能收益非常可观。这是典型的可用性和性能权衡没有绝对标准取决于业务。5.4 为什么设置了 cache_size 但感觉没效果可能原因有三种一是你在连接 A 设置了 cache_size实际跑查询用的是连接 B二是你设置过PRAGMA cache_size 20000;以为是 20000KB但正数单位其实是页按 4KB 页大小算是 78MB表现可能超过预期三是你的查询是串行扫描级别的缓存命中率本身就不高缓存再大也救不了全表扫描。所以更好的做法是先明确业务 SQL 属于“点查”“小范围查”还是“遍历型统计”。点查和小范围查对 cache 敏感遍历型统计更吃 I/O 顺序读取速度一般可以结合 mmap 或 SSD 本身的速度来缓解。5.5 C 里把 JSON 保存进 SQLite 的坑有些朋友会在 C 程序里把整个 JSON 对象作为字符串存进 SQLite 某个 TEXT 列。这个做法本身没问题但有两个隐患。第一如果你的 JSON 很大单个字段超过几 MBSQLite 默认参数下执行性能会明显下降。建议执行一次PRAGMA max_page_count;同时关注是否有大量 UPDATE 重写整条大字段。更推荐的做法是将 JSON 拆成多个结构化字段或者只在 SQLite 里存 JSON 的索引信息而把原始 JSON 文件直接放磁盘SQLite 里存路径或哈希。第二如果 JSON 里包含特殊字符拼 SQL 时千万不要用简单的字符串拼接否则会出现转义问题和 SQL 注入风险。用 sqlite3_bind_text 绑定参数才是正确姿势。C 下可以配合 json 库先序列化成 std::string再绑定到预编译语句。这两个点其实不完全是 PRAGMA 调优但和整库初始化配合起来能让 SQLite 的整体表现上一大截。6. 一些实操总结6.1 我建议的“标配初始化模板”不管是什么项目我几乎每次打开 SQLite 数据库都会套下面这套模板有特殊场景再改PRAGMA journal_mode WAL; PRAGMA synchronous NORMAL; PRAGMA busy_timeout 5000; PRAGMA foreign_keys ON; PRAGMA cache_size -65536;如果程序跑在 64 位桌面/服务端额外加一条PRAGMA mmap_size 268435456;如果要对一个存量库做检查也可以先执行PRAGMA integrity_check;确认库没坏再执行后续 PRAGMA 优化。这个检查虽不是参数但排错时能省下不少时间。6.2 调优思路比参数更关键很多参数都是“项目不同最佳值不同”所以只背参数没有意义。我个人的经验是调优前先明确三件事当前瓶颈是 I/O 还是 CPU应用的读写比例大致多少并发连接是单进程多线程还是多进程。判断方式也很直接如果 CPU 利用率不高但程序卡基本是 I/O 问题优先调 journal_mode、synchronous、cache_size如果数据库文件不大但查询很慢优先看索引和是否在全表扫描如果并发一上来就报 locked优先检查每个连接的初始化 PRAGMA。SQLite 真正强大之处在于它把所有关键控制器都浓缩成了几条 PRAGMA上手快、可验证、也能压榨出很高的性能。这一期先把“初始化参数”的坑填上后续系列里我会继续拆解索引优化、事务设计、以及 WAL 模式在跨平台使用时的底层细节。希望这一篇能帮你少走一点弯路。