0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Node.js 24 の node:sqlite は better-sqlite3 の代わりになるか実測した

0
Posted at

この記事で分かること

  • Node.js 24 LTS の標準モジュール node:sqlite が、フラグなしでどこまで実用になるか
  • node:sqlitebetter-sqlite3 の API の違いと、移行時にハマる落とし穴
  • 1万件 INSERT / SELECT の実測値(速度を左右する本当の要因はライブラリではないという話)

対象読者: Node.js で軽量なデータ保存(ローカルツール、CLI、Electron、小規模サーバのキャッシュや設定)に SQLite を使いたい開発者。「新規プロジェクトで better-sqlite3 を入れるか、標準の node:sqlite で済ますか」を迷っている方。

結論: 基本用途(CREATE / INSERT / SELECT / プリペアドステートメント / トランザクション)なら node:sqlitebetter-sqlite3 と同等に動き、速度差も誤差レベルです。依存を増やしたくないなら node:sqlite で十分実用的です。ただし安定度は Stable 未満で、better-sqlite3 にある便利ヘルパの一部が欠けます。安定版の保証と db.transaction() などのヘルパが要るなら better-sqlite3 を選びます。

前提(動作環境)

本記事の実測はすべて次の環境で行いました。数値は「この環境での参考値」です。OS や CPU が変われば絶対値は変わります。

項目
マシン Apple M3 / メモリ 24GB
OS macOS (Darwin 25.5.0, arm64)
Node.js v24.16.0(LTS ライン)
better-sqlite3 v12.11.1(バンドル SQLite 3.53.2、prebuilt バイナリ)
node:sqlite バンドル SQLite 3.53.0

数値はすべて上記環境での実測です。公式ドキュメント(nodejs.org/api/sqlite.html)は新しいバージョン向けの内容を表示します。そのためバージョン依存の記述は、手元の v24.16.0 で実行確認した結果を正としています。

node:sqlite はフラグなしで使える

Node.js 24 では node:sqlite--experimental-sqlite フラグなしで読み込めます。手元の v24.16.0 で require('node:sqlite') を実行すると、exit code 0・stderr 空でロードできました。

// require(CommonJS)
const sqlite = require('node:sqlite');
// loaded OK, exports: DatabaseSync, StatementSync, Session, constants, backup

ESM の import も同様に警告なしでロードできます。

import { DatabaseSync } from 'node:sqlite';

process.on('warning', ...) を仕込んで基本 API を一通り叩いても、ExperimentalWarning は発火しませんでした。これはフラグが v23.4.0 / v22.13.0 で撤廃済みのためです。公式ドキュメントの History 表にも「v23.4.0, v22.13.0 — SQLite is no longer behind --experimental-sqlite but still experimental.」と記載があります。

ただし「フラグが不要」であることと「安定版」であることは別です。安定度インデックスはまだ Stable ではありません。公式ドキュメント(最新版)では「Stability: 1.2 - Release candidate」で、History 表に「v25.7.0 — SQLite is now a release candidate」とあります。v24.16.0 は RC 化(v25.7.0)より前のバージョンなので、v24 系では experimental 相当(Stability 1.1、Stable 未満)と理解するのが正確です。いずれにせよ Stability 2(Stable)ではありません。将来 API が変わる可能性を許容できるかは、導入前に判断してください。

基本操作(node:sqlite)

まずは最小のサンプルです。CREATE / INSERT / SELECT / プリペアドステートメント / トランザクションまでを 1 ファイルにまとめました。v24.16.0 で動作確認済みです。

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync('app.db'); // ファイル。メモリは ':memory:'
db.exec('PRAGMA journal_mode = WAL');   // 実運用では推奨
db.exec(`CREATE TABLE IF NOT EXISTS users(
  id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER
)`);

// プリペアド + 位置パラメータ
const insert = db.prepare('INSERT INTO users (name, age) VALUES (?, ?)');
const { lastInsertRowid } = insert.run('Alice', 30); // {changes:1, lastInsertRowid:1}

// 名前付きパラメータ
db.prepare('INSERT INTO users (name, age) VALUES (:name, :age)')
  .run({ name: 'Bob', age: 25 });

// トランザクション(BEGIN/COMMIT)。1万件級はこれで桁違いに速くなる
const many = db.prepare('INSERT INTO users (name, age) VALUES (?, ?)');
db.exec('BEGIN');
try {
  for (let i = 0; i < 10000; i++) many.run('user' + i, i);
  db.exec('COMMIT');
} catch (e) {
  db.exec('ROLLBACK');
  throw e;
}

console.log(db.prepare('SELECT * FROM users WHERE id = ?').get(lastInsertRowid));
console.log(db.prepare('SELECT COUNT(*) AS c FROM users').get().c);
db.close();

押さえておきたい戻り値と挙動は次のとおりです(すべて v24.16.0 実測)。

  • new DatabaseSync(':memory:') でメモリDB、パス文字列を渡すとファイルDBです。
  • db.exec(sql) は複数文の DDL / DML をまとめて実行します(戻り値なし)。CREATE TABLE ... STRICT も使えます。
  • stmt.run(...) の戻り値は { changes, lastInsertRowid } です(実測: {"changes":1,"lastInsertRowid":1})。
  • stmt.get(...) は 1 行をオブジェクトで返します(実測: {"id":1,"name":"Alice","age":30})。該当なしは undefined です。
  • stmt.all(...) は全行をオブジェクトの配列で返します。
  • stmt.iterate(...) は行オブジェクトのイテレータを返し、for...of で回せます。

パラメータバインドは次のとおりです。

  • 位置指定: ?stmt.run('Alice', 30)
  • 名前付き: :name / $name / @name のいずれも使え、オブジェクトで stmt.run({ name: 'Bob', age: 25 })

トランザクション中の判定

トランザクション中かどうかは db.isTransaction プロパティで判定できます。実測では BEGIN 後に trueCOMMIT 後に false になりました。db.exec('ROLLBACK') を実行すると件数が元に戻ることも確認しています。

なお better-sqlite3 のような関数ラッパである db.transactionnode:sqlite には存在せず、undefined です。この差は後述します。

コンストラクタのオプション

new DatabaseSync(path[, options]) で使えるオプションのうち、実測で確認したものは次のとおりです。

  • readOnly: true: 読み取り専用オープン。実測で書き込みが attempt to write a readonly database で拒否され、存在しないファイルへの readOnly オープンは unable to open database file で失敗しました。
  • timeout: 5000: 受理されます(v24.0.0 で追加。History 表に「v24.0.0, v22.16.0 — Add timeout option」)。
  • ほかに enableForeignKeyConstraints(デフォルト true)、enableDoubleQuotedStringLiteralsallowExtensionopen があります。

better-sqlite3 との違い

両方を実際に動かして確認した差分をまとめます。設計思想(すべて同期 API)は共通で、run() の戻り値の形({changes, lastInsertRowid})や get() / all() の返し方も同じです。違いが出るのは以下の点です。

観点 node:sqlite (v24.16.0) better-sqlite3 (v12.11.1)
導入 Node 標準(npm install 不要、依存ゼロ) npm install better-sqlite3。prebuilt バイナリで今回は約1秒・node-gyp ビルド不要
import import { DatabaseSync } from 'node:sqlite' import Database from 'better-sqlite3'
DBクラス new DatabaseSync(path, opts) new Database(path, opts)
トランザクション中判定 db.isTransaction db.inTransaction(プロパティ名が違う)
トランザクションヘルパ なし(BEGIN/COMMIT を exec) db.transaction(fn) あり。関数化でき、例外時に自動 ROLLBACK
スカラ取得 setReturnArrays(true) で配列化。.pluck() 相当はなし .pluck()(単一列をスカラで)、.raw()(行を配列で)あり
名前付き param のキー { v: ... }(bare)も { ':v': ... }(prefix付き)も両方OK { v: ... }(bare)のみ。{ ':v': ... }Missing named parameter "v" でエラー
拡張ロード loadExtension() あり。ただし allowExtension: true を明示しないと extension loading is not allowed で拒否 db.loadExtension() を標準提供
boolean 束縛 不可。Provided value cannot be bound to SQLite parameter で例外 不可。SQLite3 can only bind numbers, strings, bigints, buffers, and null で例外
BigInt setReadBigInts(true) で INTEGER を BigInt 読み出し defaultSafeIntegers() 等で対応
ユーザー定義関数/集約 db.function() / db.aggregate() あり あり
Worker thread 標準ドキュメントに明示なし 公式に Worker thread サポートを明記

node:sqlite のトップレベル export は DatabaseSync, StatementSync, Session, backup, constants でした(v24.16.0 実測)。StatementSyncall, columns, get, iterate, run, setAllowBareNamedParameters, setAllowUnknownNamedParameters, setReadBigInts, setReturnArrays を持ちます。DatabaseSyncaggregate, applyChangeset, close, createSession, createTagStore, deserialize, enableDefensive, enableLoadExtension, exec, function, loadExtension, location, open, prepare, serialize, setAuthorizer を持ちます。createTagStore(プリペアド文の LRU キャッシュ)や Session / changeset、serialize / deserializebackup() は Node 独自です。

移行時の落とし穴: 名前付きパラメータのキー

移行で特にハマりやすいのが名前付きパラメータのキー命名です。node:sqlite は bare({ v: ... })でも prefix 付き({ ':v': ... })でも通ります。一方 better-sqlite3 は bare のみで、prefix 付きは Missing named parameter "v" でエラーになります。

// node:sqlite: どちらも通る
stmt.run({ name: 'Bob' });    // OK
stmt.run({ ':name': 'Bob' }); // OK

// better-sqlite3: prefix 付きはエラー
stmt.run({ name: 'Bob' });    // OK
stmt.run({ ':name': 'Bob' }); // Missing named parameter "name"

node:sqlite 向けに prefix 付きキーで書いたコードを better-sqlite3 に移すと、この違いがバグの原因になります。

better-sqlite3 の transaction() ヘルパ

better-sqlite3 には db.transaction(fn) があり、処理を関数化できます。例外が飛べば自動で ROLLBACK されます(実測で throw により件数が巻き戻ることを確認)。

import Database from 'better-sqlite3';
const db = new Database('app.db');
const insertMany = db.transaction((rows) => {
  const stmt = db.prepare('INSERT INTO users (name, age) VALUES (?, ?)');
  for (const [n, a] of rows) stmt.run(n, a);
});
insertMany([['Dave', 50], ['Eve', 22]]); // 例外が飛べば自動でROLLBACK

node:sqlite には同等のヘルパがないため、同じ「自動ロールバック」が欲しい場合は自前でラッパを書きます(動作確認済みのパターン)。

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

パフォーマンス実測

1万件 INSERT と SELECT を両ライブラリで計測しました。

測定条件:

  • 1万件(N=10000)INSERT を「非トランザクション(各 run() が自動コミット)」と「単一トランザクションで一括」で比較
  • SELECT は「単一行 get() を5000回ループ」と「全件 all()(1万件)」
  • ファイルベースDB(:memory: ではなく一時ファイル)。PRAGMA synchronous = NORMAL 固定
  • ジャーナルモードは WAL と DELETE(デフォルトのロールバックジャーナル)の両方を計測
  • ウォームアップ1回を捨て、計測3回の中央値を採用。SELECT の検証用チェックサムが全構成で一致し、結果の正当性を確認

環境は前掲のとおり Node.js v24.16.0 / Apple M3 / macOS arm64 です。中央値(ミリ秒。小さいほど速い)は次のとおりです。

構成 INSERT 1万件・非TX INSERT 1万件・単一TX SELECT get()×5000 SELECT all() 1万件
node:sqlite / WAL 65.9 3.1 6.9 3.3
node:sqlite / DELETE 1391.9 3.3 19.1 3.5
better-sqlite3 / WAL 68.5 4.2 5.5 2.5
better-sqlite3 / DELETE 1421.0 4.5 17.7 2.4

この表から読み取れることは 3 点です。

  1. トランザクションの効果が支配的です。 node:sqlite / DELETE で見ると、非TX 1391.9ms に対し単一TX 3.3ms で、約422倍の差です。WAL でも 65.9ms 対 3.1ms で約21倍です。ライブラリの違いより、こちらが桁違いに効きます。
  2. node:sqlite と better-sqlite3 の差は誤差レベルです。 TX 付き INSERT では node:sqlite がわずかに速く(約3.1〜3.3ms 対 約4.2〜4.5ms)、SELECT get() は better-sqlite3 がわずかに速い(約5.5ms 対 約6.9ms)です。全件 all() も better-sqlite3 がわずかに速い(2.4〜2.5ms 対 3.3〜3.5ms)です。いずれも「どちらを選んでも実用上変わらない」レベルです。
  3. WAL は非トランザクションの大量 INSERT と単発 SELECT を速くします。 DELETE の 1400ms 級が WAL では 66ms 級になります。

これらは Apple M3 / macOS arm64 での参考値です。Linux x64 や Windows、ストレージの種類で絶対値は変わるため、数値そのものの一般化はしません。ただし「トランザクションが効く」「両者はほぼ同等」という傾向は、この計測から読み取れます。

現時点の制約・注意点

node:sqlite を新規採用する前に、次の点を確認してください(v24.16.0 時点)。

  • 安定度は Stable 未満です(v24 系は experimental 相当、v25.7.0 で RC)。API 変更の可能性を許容できるか判断が要ります。
  • better-sqlite3 にあって node:sqlite に無い / 弱いものがあります: db.transaction(fn) の自動ロールバック・ネスト管理ヘルパ、.pluck().raw()setReturnArrays で代替できますが文単位のトグル)、公式に謳う Worker thread サポート、unsafe mode などの上級機能。
  • boolean を直接バインドできません(両者共通の制約)。true / false は自分で 0 / 1 に変換します。
  • 拡張ロードは allowExtension: true の明示が必須です(安全側デフォルト)。
  • 名前付きパラメータのキー命名が better-sqlite3 と非互換になり得ます。移行時のバグ源です。

まとめ

  • Node.js 24 LTS(v24.16.0 で確認)の node:sqlite は、フラグ不要・警告なしで import / require でき、基本用途は better-sqlite3 と同等に動きます。
  • 実測(Apple M3 / macOS、1万件 INSERT / SELECT)では両者の速度差は誤差レベルです。支配的なのはライブラリではなくトランザクションで、DELETE ジャーナルでは非TX比 約422倍、WAL でも約21倍 INSERT が速くなりました。
  • 性能を上げたいなら、まず「トランザクションで囲む」「WAL を使う」を先に検討します。ライブラリ選択より効きます。
  • 依存を増やしたくない・軽量に始めたいなら node:sqlite で十分実用的です。
  • 安定版の保証と db.transaction() / .pluck() / .raw() などのヘルパ、Worker thread の明示サポートが要るなら better-sqlite3 を選びます。

参考リンク

  1. Node.js 公式 node:sqlite ドキュメント: https://nodejs.org/api/sqlite.html (表示は新しいバージョン向け。v24 固有はローカル v24.16.0 で実行確認)
  2. Node.js 24.0.0 リリースブログ(2025-05-06): https://nodejs.org/en/blog/release/v24.0.0
  3. better-sqlite3 リポジトリ(README・API・ベンチ比較表): https://github.com/WiseLibs/better-sqlite3
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?