我的博客

用 Node.js 内置的 SQLite,给小项目省掉一个数据库

以前在 Node 里用 SQLite,基本绕不开 better-sqlite3 或 sqlite3 这两个包。它们都依赖原生模块,换个 Node 版本、换台机器就可能要重新编译,在 Windows 和一些精简的 Linux 镜像上尤其麻烦。

从 Node.js 22.5 开始,官方内置了 node:sqlite 模块;22.13 / 23.4 之后不再需要加启动参数。对于个人项目、内部工具、小型网站来说,这意味着不用装任何东西就有一个可用的数据库。

基本用法

const { DatabaseSync } = require('node:sqlite');

const db = new DatabaseSync('data/app.db');

db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    created_at INTEGER NOT NULL
  )
`);

const insert = db.prepare('INSERT INTO notes (title, created_at) VALUES (?, ?)');
const r = insert.run('第一条笔记', Date.now());
console.log(r.lastInsertRowid);

const rows = db.prepare('SELECT * FROM notes ORDER BY id DESC LIMIT ?').all(10);

API 和 better-sqlite3 很像:prepare 得到语句对象,然后用 run / get / all 执行。

建议打开的几个选项

db.exec(`
  PRAGMA journal_mode = WAL;
  PRAGMA foreign_keys = ON;
  PRAGMA busy_timeout = 5000;
`);
  • WAL 模式:读写可以并发,写入性能也更好,几乎是必开的;
  • foreign_keys:SQLite 默认不检查外键,需要手动打开;
  • busy_timeout:遇到锁时等待一会儿而不是立刻报错。

事务

node:sqlite 没有提供事务的封装,自己写一个也很简单:

function tx(fn) {
  db.exec('BEGIN IMMEDIATE');
  try {
    const result = fn();
    db.exec('COMMIT');
    return result;
  } catch (e) {
    db.exec('ROLLBACK');
    throw e;
  }
}

用 BEGIN IMMEDIATE 而不是默认的 BEGIN,可以在事务开始时就拿到写锁,避免"先读后写"时出现锁升级失败。

什么时候不适合

  • 它是同步 API,一条慢查询会阻塞整个进程。小数据量下这不是问题,但不要在里面跑几秒钟的报表;
  • 同一时刻只能有一个写入者,写入非常密集的场景不合适;
  • 需要多台服务器共享同一个数据库时,应该换成 MySQL 或 PostgreSQL。

对日活几百、几千的个人网站来说,SQLite 的性能绰绰有余。真正需要注意的反而是备份——整个数据库就是一个文件,丢了就全没了。