better-sqlite3 の使い方 — Node.js最速の同期型SQLiteライブラリ
一言でいうと
better-sqlite3 は、Node.js向けの最速かつ最もシンプルなSQLiteライブラリです。非同期ではなく同期APIを採用することで、パフォーマンスとコードの可読性を両立しています。
どんな時に使う?
- Electronアプリやローカルツールのデータ永続化 — 外部DBサーバーなしで手軽にリレーショナルデータを扱いたい場合
- CLIツールやバッチ処理の中間データストア — JSONファイルでは限界があるが、PostgreSQLを立てるほどでもない場面
- テスト環境のデータベース — インメモリDBとして使い、テストごとにクリーンな状態を作れる
インストール
# npm
npm install better-sqlite3
# yarn
yarn add better-sqlite3
# pnpm
pnpm add better-sqlite3
TypeScriptで使う場合は型定義もインストールします。
npm install -D @types/better-sqlite3
注意: ネイティブモジュール(C++アドオン)のため、ビルドツール(Python、C++コンパイラ等)が必要になる場合があります。LTS版のNode.jsにはプリビルドバイナリが提供されています。
基本的な使い方
import Database from 'better-sqlite3';
// データベースを開く(ファイルが存在しなければ自動作成)
const db = new Database('myapp.db');
// パフォーマンスのためWALモードを有効化(ほぼ必須)
db.pragma('journal_mode = WAL');
// テーブル作成
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
)
`);
// INSERT(プリペアドステートメント)
const insert = db.prepare(
'INSERT INTO users (name, email) VALUES (@name, @email)'
);
const info = insert.run({ name: '田中太郎', email: 'tanaka@example.com' });
console.log(`挿入されたID: ${info.lastInsertRowid}`);
// SELECT(1行取得)
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(1) as {
id: number;
name: string;
email: string;
created_at: string;
} | undefined;
console.log(user?.name); // 田中太郎
// SELECT(複数行取得)
const allUsers = db.prepare('SELECT * FROM users').all();
console.log(allUsers);
// アプリ終了時にクローズ
db.close();
よく使うAPI
1. db.prepare() — プリペアドステートメントの作成
最も頻繁に使うメソッドです。SQLインジェクション対策としても必須です。
const stmt = db.prepare('SELECT * FROM users WHERE name = ? AND email = ?');
// 位置パラメータ
const row = stmt.get('田中太郎', 'tanaka@example.com');
// 名前付きパラメータ(@, $, : のいずれかのプレフィックス)
const stmt2 = db.prepare('SELECT * FROM users WHERE name = @name');
const row2 = stmt2.get({ name: '田中太郎' });
2. .run() / .get() / .all() / .iterate() — ステートメントの実行
// run(): INSERT/UPDATE/DELETE用。変更情報を返す
const result = db.prepare('UPDATE users SET name = ? WHERE id = ?').run('佐藤花子', 1);
console.log(result.changes); // 変更された行数
// get(): 1行だけ取得(なければundefined)
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(1);
// all(): 全行を配列で取得
const users = db.prepare('SELECT * FROM users').all();
// iterate(): イテレータで1行ずつ取得(大量データ向け)
for (const row of db.prepare('SELECT * FROM users').iterate()) {
console.log((row as { name: string }).name);
}
3. db.transaction() — トランザクション
better-sqlite3の最も強力な機能の一つです。関数をラップするだけでトランザクションになります。
interface UserInput {
name: string;
email: string;
}
const insertMany = db.transaction((users: UserInput[]) => {
const insert = db.prepare('INSERT INTO users (name, email) VALUES (@name, @email)');
for (const user of users) {
insert.run(user);
}
return users.length;
});
// 一括挿入(途中でエラーが起きれば全てロールバック)
const count = insertMany([
{ name: '鈴木一郎', email: 'suzuki@example.com' },
{ name: '山田次郎', email: 'yamada@example.com' },
{ name: '高橋三郎', email: 'takahashi@example.com' },
]);
console.log(`${count}件挿入しました`);
// ネストしたトランザクション(SAVEPOINT)も自動対応
const outerTransaction = db.transaction(() => {
// ...
const innerTransaction = db.transaction(() => {
// SAVEPOINTとして動作
});
innerTransaction();
});
4. db.function() — ユーザー定義関数
SQLの中で使えるカスタム関数を定義できます。
// スカラー関数
db.function('add_tax', (price: number) => {
return Math.round(price * 1.1);
});
const result = db.prepare('SELECT add_tax(1000) AS price_with_tax').get() as { price_with_tax: number };
console.log(result.price_with_tax); // 1100
// 集約関数
db.aggregate('median', {
start: () => [] as number[],
step: (array: number[], value: number) => {
array.push(value);
return array;
},
result: (array: number[]) => {
array.sort((a, b) => a - b);
const mid = Math.floor(array.length / 2);
return array.length % 2 !== 0
? array[mid]
: (array[mid - 1] + array[mid]) / 2;
},
});
5. db.pragma() — PRAGMAの実行
SQLiteの設定を変更・取得します。
// WALモード有効化(パフォーマンス向上の定番)
db.pragma('journal_mode = WAL');
// 外部キー制約を有効化(SQLiteはデフォルトOFF)
db.pragma('foreign_keys = ON');
// 現在の設定を取得
const journalMode = db.pragma('journal_mode', { simple: true });
console.log(journalMode); // 'wal'
// キャッシュサイズ変更
db.pragma('cache_size = -64000'); // 64MB
類似パッケージとの比較
| 特徴 | better-sqlite3 | sqlite3 (node-sqlite3) | sql.js |
|---|---|---|---|
| API方式 | 同期 | 非同期(コールバック) | 同期 |
| パフォーマンス | 最速 | 2.8〜24x遅い | 遅い(Wasm) |
| ネイティブバイナリ | 必要 | 必要 | 不要(Wasm) |
| ブラウザ対応 | ✗ | ✗ | ✓ |
| トランザクションAPI | 関数ラップ方式 | 手動BEGIN/COMMIT | 手動 |
| Worker Thread対応 | ✓ | ✗ | ✗ |
| メンテナンス状況 | 活発 | やや停滞 | 活発 |
選定の目安:
- Node.jsサーバー/CLI → better-sqlite3(迷ったらこれ)
- ブラウザで動かしたい → sql.js
- 既存プロジェクトで非同期APIが必要 → sqlite3 + sqlite(Promiseラッパー)
注意点・Tips
WALモードは必ず設定する
db.pragma('journal_mode = WAL');
これを設定しないと、読み取りと書き込みが互いにブロックし合い、パフォーマンスが大幅に低下します。ほぼ全てのユースケースで有効にすべきです。
同期APIだがブロッキングに注意
better-sqlite3は同期APIですが、SQLite自体が非常に高速なため、通常のクエリではイベントループへの影響は軽微です。ただし、数秒かかるような重いクエリがある場合は、Worker Threadの利用を検討してください。
// Worker Threadでの利用例(概念)
import { Worker } from 'worker_threads';
const worker = new Worker('./db-worker.js');
外部キー制約はデフォルトOFF
SQLiteの仕様上、外部キー制約はデフォルトで無効です。接続のたびに有効化が必要です。
const db = new Database('myapp.db');
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON'); // 毎回必要!
大量INSERTは必ずトランザクションで
トランザクションなしで1000行INSERTすると、1行ごとにディスク書き込みが発生します。db.transaction()で囲むだけで数十〜数百倍高速になります。
// ❌ 遅い
for (const item of items) {
insert.run(item);
}
// ✅ 速い
const insertAll = db.transaction((items: Item[]) => {
for (const item of items) {
insert.run(item);
}
});
insertAll(items);
64ビット整数の扱い
JavaScriptのNumberは53ビットまでしか安全に扱えません。INTEGERカラムに53ビットを超える値がある場合、BigIntが返されます。明示的に制御したい場合は以下のようにします。
// BigIntを常に返すようにする
const stmt = db.prepare('SELECT id FROM large_table');
stmt.safeIntegers(true); // BigIntで返す
インメモリDBでテストを高速化
const db = new Database(':memory:');
// テスト用のスキーマとデータを投入
ファイルI/Oが不要なため、テストが非常に高速になります。
まとめ
better-sqlite3は、Node.jsでSQLiteを使うなら第一選択となるライブラリです。同期APIによるシンプルなコード、db.transaction()による直感的なトランザクション管理、そして圧倒的なパフォーマンスが魅力です。WALモードの有効化と大量書き込み時のトランザクション利用さえ押さえておけば、多くのユースケースで快適に使えるでしょう。