Node 内置 SQLite 实测:一万条数据写入从 232 秒到 30 毫秒
最近在改一个 CLI 工具的本地缓存:每次请求的结果攒在本地,供离线回看和历史比对,量级几千到一两万条。选型时卡在一个问题上——到底是继续用一个 JSON 文件硬扛,还是上 SQLite?
直觉说「一万条用 JSON 就够了」,但 Node 22 已经内置了 node:sqlite,零依赖白给。两边都说服不了自己,就跑了一遍基准。结论比我预想的清楚,而且中间踩到一个足以决定成败的细节。
一、怎么测的
环境:Windows Server 2022(build 20348),Node v22.22.2,直接用内置的 node:sqlite,不装任何三方包。数据设定为每条 5 个字段、payload 180 字节,代表偏小的记录。
对比的四个场景是这类缓存最典型的操作:追加 1 条、批量写入 1 万条、按主键查 1 条、改 1 条,外加冷启动读首值和落盘体积。JSON 侧按最常见写法实现——JSON.parse 读全量、改完 JSON.stringify 写回全量。
需要说明的是:每组只跑了一次,没有多轮取平均,磁盘调度和缓存会有抖动。但后面你会看到,量级差异大到不依赖那点抖动。
二、结果
追加 1 条(数据量递增,看增长趋势):
· 已有 1000 条:JSON 23.4 ms
· 已有 5000 条:JSON 20.1 ms
· 已有 10000 条:JSON 48.6 ms,SQLite 24.5 ms
批量写入 10000 条(同样的代码,只改事务):
· SQLite 自动提交:231709.7 ms(约 3 分 52 秒)
· SQLite 单个事务 BEGIN/COMMIT:83.1 ms
· SQLite WAL + synchronous=NORMAL:29.8 ms
按主键查 1 条:JSON 48.2 ms(含全量 parse),SQLite 0.043 ms(100 次共 4.3 ms)。
改 1 条:JSON 57.5 ms,SQLite 19.1 ms。
冷启动读首值:JSON 31.6 ms,SQLite 3.2 ms。
落盘体积(10000 条):JSON 2.48 MB,SQLite 2.07 MB。
先看横向对比:SQLite 在查询上有压倒性优势(48.2 ms 对 0.043 ms,差三个数量级),改单条快 3 倍,冷启动快 10 倍,落盘还更小——因为 JSON 每一行都要重复存一遍字段名。
单条追加这条反而没那么夸张:10k 条时 JSON 48.6 ms,SQLite 24.5 ms,只差 1 倍。但注意 JSON 的这条曲线是随数据量线性增长的(1000 条 23.4 ms → 10000 条 48.6 ms),SQLite 基本是平的,因为它只写改动的那一个页。
三、真正的坑在事务
上面最刺眼的一行是 231709.7 ms——快 4 分钟,和同一次测试的 83.1 ms 差了约 2800 倍。代码完全一样,唯一区别是外面包没包 BEGIN/COMMIT。
原因不复杂:SQLite 默认每条写语句都是一个独立事务,而每个事务提交都要等一次 fsync。在 Windows 上这次 fsync 大概 23 ms,一万次就是 230 秒。包进一个事务后,一万条共用一次 fsync。
所以顺序是:批量写必须显式包事务,这是唯一重要的一条;WAL + synchronous=NORMAL 是锦上添花,再快 2.8 倍。只调 WAL 不包事务,照样慢。
四、可复用的写法
const { DatabaseSync } = require('node:sqlite');
function openCache(file) {
const db = new DatabaseSync(file);
db.exec('PRAGMA journal_mode = WAL');
db.exec('PRAGMA synchronous = NORMAL');
db.exec(`CREATE TABLE IF NOT EXISTS cache (
id INTEGER PRIMARY KEY, name TEXT, ts INTEGER, payload TEXT, tags TEXT
)`);
return db;
}
function bulkInsert(db, rows) {
const stmt = db.prepare(
'INSERT OR REPLACE INTO cache (id,name,ts,payload,tags) VALUES (?,?,?,?,?)'
);
db.exec('BEGIN');
try {
for (const r of rows) stmt.run(r.id, r.name, r.ts, r.payload, JSON.stringify(r.tags));
db.exec('COMMIT');
} catch (e) {
db.exec('ROLLBACK'); // 不回滚,写锁会一直挂着
throw e;
}
}
两个小提醒。一是 db.prepare() 出来的语句要复用,别在循环里反复 prepare,上面的 0.043 ms 是复用之后的结果。二是查询返回的行是 null 原型对象(打印出来长这样:[Object: null prototype] { c: 1 }),它没有 hasOwnProperty,直接丢进通用工具函数里可能踩空。
另外,node:sqlite 在 22.x 仍是实验特性。实测 22.22.2 不加任何 flag 就能用,只会在 stderr 打一条 ExperimentalWarning;这个 CLI 工具的无害场景可以接受,要上生产建议先评估 API 稳定性,或者退到 better-sqlite3。
五、什么时候不值得换
如果你的数据只有几百条、而且是一次性整体读写(比如一份配置文件),JSON 完全够用,换了只是徒增一个依赖和一张表。真正的分界线是「有没有增量写和按条件查」:只要出现「改一条要重写整个文件」或者「每次查都要全量 parse」,SQLite 的收益就开始压过成本。
按这次的数据,我愿把它记成一条经验:数据量超过几千条、且有增量更新时,直接上 SQLite;但上之前先确认批量写包了事务,否则你会先遇到那个 4 分钟。
- 点赞
- 收藏
- 关注作者
评论(0)