はじめに
Better-Sqlite3は、Node.js向けの「最速かつシンプル」なSQLiteライブラリですね。
GitHubで6,700スター以上、月間900万ダウンロードという数字を見ると、Node.jsでSQLiteを使うなら事実上の標準になっているのがわかります。個人的には、ローカルDBが必要なElectronアプリやCLIツールを作るとき、真っ先に検討するライブラリです。
正直なところ、最初は「sqlite3で十分じゃん」と思っていたんですよ。でも実際にBetter-Sqlite3を使ってみたら、同期APIの書きやすさと、ベンチマークで「11.7倍速い」という圧倒的なパフォーマンスに驚きました。
ライセンスはMITで、安心して使えます。
Better-Sqlite3とは
Better-Sqlite3は、その名の通り「より良いsqlite3」を目指したライブラリです。従来のsqlite3パッケージが非同期APIを採用していたのに対し、Better-Sqlite3は同期APIを採用しています。
「え、同期って遅くない?」と思うかもしれませんが、これが意外と逆なんですよ。SQLiteはインメモリまたはローカルファイルへのアクセスなので、非同期のオーバーヘッドの方がむしろ無駄になる。
特徴・メリット
1. 圧倒的なパフォーマンス
Better-Sqlite3の最大の特徴は、その速さですね。
公式ベンチマークによると、1行のSELECTで他ライブラリの約11.7倍、100行のSELECTでも約2.9倍高速。これ、意外と体感できるレベルの差があります。
30代になって思うのは、パフォーマンスチューニングより「最初から速いツールを選ぶ」方が時短になるということ。
2. 同期APIのシンプルさ
コールバック地獄やPromiseチェーンとはおさらばです。
// Better-Sqlite3(同期)
const row = db.prepare('SELECT * FROM users WHERE id = ?').get(userId);
console.log(row.name);
// 従来のsqlite3(非同期)
db.get('SELECT * FROM users WHERE id = ?', [userId], (err, row) => {
if (err) throw err;
console.log(row.name);
});
同期APIの方が圧倒的に読みやすい。エラーハンドリングもtry-catchで統一できる。
3. フルトランザクションサポート
データベース操作の一貫性を保証するトランザクションに完全対応しています。
const insertMany = db.transaction((users) => {
for (const user of users) {
insert.run(user);
}
});
insertMany([
{ name: '山田', age: 30 },
{ name: '田中', age: 25 },
{ name: '佐藤', age: 35 },
]);
transaction関数でラップするだけ。失敗したら自動ロールバック。
4. ユーザー定義関数
SQLiteにカスタム関数を追加できます。
db.function('addNumbers', (a, b) => a + b);
const result = db.prepare('SELECT addNumbers(10, 20)').pluck().get();
// result: 30
SQLだけでは難しい処理をJavaScriptで書けるのは便利。
5. 64ビット整数対応
JavaScriptのNumber型は大きな整数で精度が落ちる問題がありますが、Better-Sqlite3はBigIntをサポートしています。
db.pragma('journal_mode = WAL');
db.defaultSafeIntegers(true); // BigIntを使用
金額計算やIDの取り扱いで安心。
6. WALモード対応
Write-Ahead Logging(WAL)モードに対応しており、読み取りと書き込みの並行処理が可能です。
db.pragma('journal_mode = WAL');
複数プロセスからの同時アクセスがある場合に威力を発揮する。
インストール方法
前提条件
Node.js v14.21.1以上が必要です。
基本インストール
# npm
npm install better-sqlite3
# pnpm
pnpm add better-sqlite3
# yarn
yarn add better-sqlite3
TypeScript対応
npm install better-sqlite3
npm install -D @types/better-sqlite3
型定義が別パッケージなのは少し面倒ですが、型サポートは充実しています。
ビルドに関する注意
Better-Sqlite3はネイティブモジュールなので、ビルド環境が必要です。
# Ubuntu/Debian
sudo apt-get install build-essential python3
# macOS
xcode-select --install
# Windows
npm install --global windows-build-tools
Electronで使う場合はrebuildが必要になることも。
基本的な使い方
データベース接続
const Database = require('better-sqlite3');
// ファイルベース
const db = new Database('myapp.db');
// インメモリ
const memDb = new Database(':memory:');
// オプション付き
const db = new Database('myapp.db', {
readonly: false,
fileMustExist: false,
timeout: 5000,
verbose: console.log
});
テーブル作成
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
age INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
execは複数のSQL文を一度に実行できる。
CRUD操作
// INSERT
const insert = db.prepare('INSERT INTO users (name, email, age) VALUES (?, ?, ?)');
const info = insert.run('山田太郎', 'taro@example.com', 30);
console.log(info.lastInsertRowid); // 挿入されたID
// SELECT(単一行)
const getUser = db.prepare('SELECT * FROM users WHERE id = ?');
const user = getUser.get(1);
console.log(user);
// SELECT(複数行)
const getAllUsers = db.prepare('SELECT * FROM users');
const users = getAllUsers.all();
// SELECT(イテレータ)
for (const user of getAllUsers.iterate()) {
console.log(user.name);
}
// UPDATE
const update = db.prepare('UPDATE users SET age = ? WHERE id = ?');
update.run(31, 1);
// DELETE
const remove = db.prepare('DELETE FROM users WHERE id = ?');
remove.run(1);
prepare → run/get/all/iterateの流れは直感的。
名前付きパラメータ
const insert = db.prepare(`
INSERT INTO users (name, email, age)
VALUES (@name, @email, @age)
`);
insert.run({
name: '田中花子',
email: 'hanako@example.com',
age: 25
});
プレースホルダーより可読性が高い。
トランザクション
const insertUser = db.prepare(`
INSERT INTO users (name, email, age) VALUES (@name, @email, @age)
`);
const insertMany = db.transaction((users) => {
for (const user of users) {
insertUser.run(user);
}
});
// 一括挿入(全成功 or 全失敗)
insertMany([
{ name: 'ユーザー1', email: 'user1@example.com', age: 20 },
{ name: 'ユーザー2', email: 'user2@example.com', age: 25 },
{ name: 'ユーザー3', email: 'user3@example.com', age: 30 },
]);
大量データの挿入はトランザクションでまとめると100倍以上速くなることも。
実践的なユースケース
Electronアプリでのローカルストレージ
const Database = require('better-sqlite3');
const path = require('path');
const { app } = require('electron');
// ユーザーデータディレクトリにDB作成
const dbPath = path.join(app.getPath('userData'), 'app.db');
const db = new Database(dbPath);
// 設定テーブル
db.exec(`
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT
)
`);
// 設定の保存・取得
const setSetting = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)');
const getSetting = db.prepare('SELECT value FROM settings WHERE key = ?').pluck();
setSetting.run('theme', 'dark');
const theme = getSetting.get('theme'); // 'dark'
Electronとの相性は抜群。IndexedDBより直感的に使える。
CLIツールのデータ永続化
const Database = require('better-sqlite3');
const os = require('os');
const path = require('path');
// ホームディレクトリに設定DB作成
const configDir = path.join(os.homedir(), '.myapp');
const db = new Database(path.join(configDir, 'data.db'));
// キャッシュテーブル
db.exec(`
CREATE TABLE IF NOT EXISTS cache (
key TEXT PRIMARY KEY,
value TEXT,
expires_at INTEGER
)
`);
// キャッシュ操作
function setCache(key, value, ttlSeconds) {
const expiresAt = Date.now() + ttlSeconds * 1000;
db.prepare('INSERT OR REPLACE INTO cache (key, value, expires_at) VALUES (?, ?, ?)')
.run(key, JSON.stringify(value), expiresAt);
}
function getCache(key) {
const row = db.prepare('SELECT value, expires_at FROM cache WHERE key = ?').get(key);
if (!row || row.expires_at < Date.now()) return null;
return JSON.parse(row.value);
}
ファイルベースのキャッシュより検索が速い。
TypeScriptでの型安全な使用
import Database from 'better-sqlite3';
interface User {
id: number;
name: string;
email: string;
age: number | null;
created_at: string;
}
const db = new Database('app.db');
// 型付きのprepare
const getUser = db.prepare<[number], User>('SELECT * FROM users WHERE id = ?');
const user = getUser.get(1);
// user の型は User | undefined
const getAllUsers = db.prepare<[], User>('SELECT * FROM users');
const users = getAllUsers.all();
// users の型は User[]
// 挿入
type InsertUser = Omit<User, 'id' | 'created_at'>;
const insertUser = db.prepare<[InsertUser]>(`
INSERT INTO users (name, email, age) VALUES (@name, @email, @age)
`);
insertUser.run({ name: '山田', email: 'yamada@example.com', age: 30 });
Drizzle ORMとの連携
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
const sqlite = new Database('app.db');
const db = drizzle(sqlite);
// スキーマ定義
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').unique(),
});
// 型安全なクエリ
const allUsers = await db.select().from(users);
Drizzle ORMと組み合わせると、SQLiteでも型安全なクエリが書ける。個人的にはこの組み合わせがおすすめ。
sqlite3との比較
正直な比較をしておきます。
| 観点 | Better-Sqlite3 | sqlite3 |
|---|---|---|
| API | 同期 | 非同期 |
| パフォーマンス | 高速(2.9〜11.7倍) | 標準 |
| 学習コスト | 低い | コールバック理解が必要 |
| エラーハンドリング | try-catch | コールバック内 |
| トランザクション | 簡潔 | やや複雑 |
| ユーザー定義関数 | 対応 | 対応 |
| 64ビット整数 | BigInt対応 | 限定的 |
| メンテナンス | 活発 | 活発 |
個人的には、新規プロジェクトならBetter-Sqlite3一択ですね。
sqlite3を選ぶ理由があるとすれば、既存コードとの互換性くらい。
まとめ
Better-Sqlite3を導入して感じた変化:
- パフォーマンス: 他ライブラリの数倍〜10倍以上速い
- コードの可読性: 同期APIでシンプルに書ける
- エラーハンドリング: try-catchで統一できる
- トランザクション: 簡潔に書ける
- 型安全性: TypeScriptとの相性が良い
- QOL: 確実に上がった
Node.jsでSQLiteを使うなら、Better-Sqlite3は間違いなく最有力候補ですね。
特に「Electronアプリでローカルデータを保存したい」「CLIツールで設定やキャッシュを永続化したい」という人には、Better-Sqlite3がハマると思います。
これ、意外とsqlite3から乗り換えた人が多い印象です。「非同期は必要ないのに、なんでPromiseで書かなきゃいけないんだ」というモヤモヤを解消してくれる。