Node 内置 SQLite 实测:一万条数据写入从 232 秒到 30 毫秒

举报
yd_250310792 发表于 2026/10/25 07:24:02 2026/10/25
【摘要】 最近在改一个 CLI 工具的本地缓存:每次请求的结果攒在本地,供离线回看和历史比对,量级几千到一两万条。选型时卡在一个问题上——到底是继续用一个 JSON 文件硬扛,还是上 SQLite?直觉说「一万条用 JSON 就够了」,但 Node 22 已经内置了 node:sqlite,零依赖白给。两边都说服不了自己,就跑了一遍基准。结论比我预想的清楚,而且中间踩到一个足以决定成败的细节。一、怎么...

最近在改一个 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 分钟。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。