在SQLite的众多PRAGMA配置项里,synchronous是与数据安全和写入速度关系最直接的一个。它决定了事务提交时数据库引擎以怎样的强度把修改内容刷回磁盘,取值不同,掉电时数据库损坏的概率和日常写入的性能表现会有天壤之别。不少应用把SQLite当作嵌入式存储,却从未关注过这个参数,结果要么白白牺牲了性能,要么为了速度埋下数据丢失甚至文件损坏的隐患。

synchronous参数到底控制什么
要弄明白这个参数,得先了解SQLite的写入流程。一个事务提交时,引擎并不是把数据写进主数据库文件就结束,而是先借助日志文件兜底:回滚日志模式下先把原始页面写入journal文件,WAL模式下则把修改追加到wal文件。而应用程序对文件的写入,通常只是进了操作系统的页缓存,真正写到物理磁盘需要调用fsync这类强制同步操作,让内核把缓存刷下去。
synchronous控制的就是SQLite执行这些强制同步的时机与次数。设为FULL时,引擎会在关键节点反复调用fsync,保证哪怕突然断电,日志和数据库文件也处于一致状态;设为OFF时,引擎把数据交给操作系统就算提交完成,什么时候真正落盘完全由系统自行调度。NORMAL则介于两者之间,只在最关键的节点同步,牺牲极小概率的一致性风险换取明显更快的提交速度。
有一点必须分清:这个参数防的是掉电、内核崩溃这类硬件或系统级故障,而不是进程崩溃。进程被杀掉后,操作系统仍会保证页缓存里的数据最终写入磁盘,所以OFF模式下进程崩溃一般不会损坏数据库;真正危险的是断电,此时OFF模式下数据库文件有可能损坏到无法打开的地步。理解了这一点,才谈得上如何选择档位。
四档同步级别的具体差异
SQLite为synchronous定义了四个取值,用数字或关键字都可以指定。先用一张表看清它们的核心区别:
| 取值 | 数字 | 同步行为 | 断电后果 |
|---|---|---|---|
| OFF | 0 | 完全不主动fsync | 可能损坏数据库文件 |
| NORMAL | 1 | 只在最关键节点同步 | 极小概率损坏,WAL模式下仅丢失最近事务 |
| FULL | 2 | 关键节点多次同步 | 不损坏,已提交事务不丢失 |
| EXTRA | 3 | 比FULL更激进,删除journal后还同步目录 | 不损坏,对部分文件系统更保险 |
OFF是速度的极限。官方文档明确说过,某些操作在OFF下比FULL快50倍以上,因为省掉了几乎所有的fsync等待。它适合的场景很窄:可以随时重建的缓存库、中间结果库,或者批量导入中途可以重来的一次性任务。凡是承载用户数据的库,都不该用OFF。
NORMAL是多数场景下推荐的折中档。具体来说,回滚日志模式下它仍会在写数据库文件前同步journal,但省掉了提交后、删除journal前对数据库文件的同步,这个窗口期就是极小概率损坏的来源;而在WAL模式下,它只在checkpoint时同步WAL文件和数据库文件,断电最多导致最近几个已提交事务回滚,数据库本身不会损坏。这也是WAL加NORMAL成为高性能组合的原因。
FULL是默认值,也是最保守的选择。它在每次提交的关键路径上做完整同步:journal写入后同步、数据库文件写完后同步、删除journal前同步,单条小事务的提交延迟会被fsync主导,在机械盘或繁忙的SSD上尤其明显。EXTRA是3.22.0版本之后加入的档位,比FULL还多一次目录同步,主要用于journal文件删除后防止目录项本身没落盘的极端情况,日常很少用到。
如何查看和修改同步级别
查看与设置都很简单,直接发PRAGMA语句即可。不带参数是查询,带参数是设置:
-- 查看当前同步级别,返回0到3的整数 PRAGMA synchronous; -- 用关键字设置 PRAGMA synchronous = NORMAL; -- 也可以用数字,等价于NORMAL PRAGMA synchronous = 1; -- attach了多个库时可以只对主库设置 PRAGMA main.synchronous = OFF;
在应用程序里,建议在拿到连接后、执行业务SQL之前完成设置。以Python为例:
import sqlite3
conn = sqlite3.connect("app.db")
cur = conn.cursor()
# 先切WAL再调同步级别,两条语句都在事务外执行
cur.execute("PRAGMA journal_mode=WAL")
cur.execute("PRAGMA synchronous=NORMAL")
# 之后正常执行业务SQL
cur.execute("CREATE TABLE IF NOT EXISTS log(id INTEGER PRIMARY KEY, msg TEXT)")
cur.execute("INSERT INTO log(msg) VALUES('hello')")
conn.commit()
用C接口时同样是在打开数据库后立刻执行:
#include <stdio.h>
#include "sqlite3.h"
int main(void) {
sqlite3 *db;
if (sqlite3_open("app.db", &db) != SQLITE_OK) {
fprintf(stderr, "open failed: %s\n", sqlite3_errmsg(db));
return 1;
}
// 打开后马上设置,注意不能在事务进行中修改
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", 0, 0, 0);
sqlite3_exec(db, "PRAGMA synchronous=NORMAL;", 0, 0, 0);
sqlite3_close(db);
return 0;
}
这里有几个容易踩的细节。第一,synchronous是连接级别的设置,不会持久化到数据库文件里,每个新连接都会回到默认的FULL,所以必须在每次建立连接后重新设置,漏掉一次性能就掉回去了。第二,事务进行中不允许修改这个参数,引擎会直接报错,务必在事务外配置。第三,如果想让默认值不再是FULL,只能在编译SQLite时通过SQLITE_DEFAULT_SYNCHRONOUS宏指定,运行时没有全局开关,每个连接都得自己设置。
WAL模式下为什么推荐NORMAL
journal_mode和synchronous是相互配合的两个维度。WAL模式下写入只追加到wal文件,读和写可以并发,这本身就大幅提升了吞吐;再把synchronous设为NORMAL,每次事务提交就不再强制同步wal文件,只有checkpoint时才做真正的落盘动作,提交路径变得非常轻。
这个组合的代价是可控的:断电后,最后一次checkpoint之后的已提交事务可能回滚消失,但数据库文件一定完好,打开就能继续用。换句话说,你损失的是最后几秒的写入,换来的是提交延迟的大幅下降。对绝大多数业务来说,这个交换是划算的,比如聊天记录、操作日志、监控指标这类允许极少量丢失的场景。
如果业务要求已提交事务一条都不能丢,比如交易流水,那就在WAL模式下保留FULL,让每次提交都同步wal文件,或者干脆接受回滚日志模式加FULL的组合,用性能换严格持久性。没有万能配置,只有按业务分级取舍。
实际项目中的选择建议与常见坑
按场景给几条可以直接落地的建议。嵌入式设备或手机端,存储介质通常是eMMC,fsync很贵,WAL加NORMAL是首选;批量导入大量数据时,先把整批操作包在一个事务里,比调synchronous有效得多,因为一个事务只付出一次同步成本,必要时导入期间可以临时用OFF;高频小事务写入,比如每秒成百上千次提交,同步级别的选择直接决定吞吐量上限,建议在真实硬件上压测后再定。
常见的坑有三个。一是忘了synchronous不持久化,测试环境配置了NORMAL,线上代码没设,性能问题排查半天才发现是默认FULL;二是误以为OFF只是丢最近数据,实际上回滚日志模式加OFF在断电时可能直接把库文件写坏,连打开都打不开,恢复成本远高于丢几条记录;三是使用连接池时长连接被复用,池子里的连接状态混乱,建议统一在获取连接的封装函数里设置PRAGMA,保证每个出池的连接都是预期状态。
最后给一个简单的判断思路:先问自己能不能接受断电丢最近几秒的数据,能接受就用WAL加NORMAL,不能接受就用FULL;OFF只留给可以随时重建的临时库。想验证各档位的实际差距,可以用每秒提交数做指标对比,务必在真实硬件上跑,不要在容器或网络盘上测,否则fsync的行为会失真,得出的结论没有参考价值。
PRAGMA synchronousSQLite同步级别数据库写入性能修改时间:2026-09-26 23:06:53