モダンSQLite活用ガイド — サーバーレスからエッジまで
SQLiteの再評価
SQLiteは「おもちゃのデータベース」と思われがちですが、実は非常に強力です。
なぜ今SQLiteなのか
- シンプル: サーバー不要、単一ファイル
- 高速: ローカルI/O、ネットワークレイテンシなし
- 信頼性: ACID準拠、広く使われている(Android、iOSなど)
- モダン化: Litestream、Turso、D1などの新ツール
- エッジコンピューティング: CDNエッジでデータベースを実行
適したユースケース
- 読み取り中心のアプリ: ブログ、ドキュメント、eコマースカタログ
- エッジアプリ: Cloudflare Workers、Vercel Edge
- 組み込み: モバイルアプリ、デスクトップアプリ
- ローカルファースト: オフライン対応アプリ
Node.jsでのSQLite
better-sqlite3
最速のSQLiteバインディングです。
npm install better-sqlite3
npm install -D @types/better-sqlite3
import Database from 'better-sqlite3'
const db = new Database('app.db')
// テーブル作成
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
// 挿入
const insert = db.prepare(`
INSERT INTO users (name, email) VALUES (?, ?)
`)
insert.run('Alice', 'alice@example.com')
// トランザクション
const insertMany = db.transaction((users: Array<[string, string]>) => {
for (const [name, email] of users) {
insert.run(name, email)
}
})
insertMany([
['Bob', 'bob@example.com'],
['Charlie', 'charlie@example.com'],
])
// クエリ
const getUser = db.prepare('SELECT * FROM users WHERE id = ?')
const user = getUser.get(1)
const getAllUsers = db.prepare('SELECT * FROM users')
const users = getAllUsers.all()
// 型安全性
interface User {
id: number
name: string
email: string
created_at: string
}
const getUserTyped = db.prepare<unknown[], User>('SELECT * FROM users WHERE id = ?')
const typedUser = getUserTyped.get(1)
WALモード
Write-Ahead Logging(WAL)モードは、並行読み取りと書き込みを可能にします。
db.pragma('journal_mode = WAL')
db.pragma('synchronous = NORMAL')
db.pragma('cache_size = -64000') // 64MB
db.pragma('temp_store = MEMORY')
WALモードのメリット:
- 読み取りと書き込みがブロックしない
- パフォーマンス向上
- データ整合性
Drizzle ORMとの統合
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit
// schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
})
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content').notNull(),
authorId: integer('author_id').notNull().references(() => users.id),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
})
// db.ts
import { drizzle } from 'drizzle-orm/better-sqlite3'
import Database from 'better-sqlite3'
import * as schema from './schema'
const sqlite = new Database('app.db')
export const db = drizzle(sqlite, { schema })
// クエリ
import { db } from './db'
import { users, posts } from './schema'
import { eq } from 'drizzle-orm'
// 挿入
const user = await db.insert(users).values({
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date(),
}).returning()
// クエリ
const allUsers = await db.select().from(users)
const userWithPosts = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: true,
},
})
Litestream — バックアップとレプリケーション
Litestreamは、SQLiteをS3などにリアルタイムでレプリケーションします。
インストール
# macOS
brew install litestream
# Linux
wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz
tar -xzf litestream-v0.3.13-linux-amd64.tar.gz
sudo mv litestream /usr/local/bin/
設定
# litestream.yml
dbs:
- path: /var/lib/app.db
replicas:
- type: s3
bucket: my-app-db-backup
path: app.db
region: us-east-1
access-key-id: ${AWS_ACCESS_KEY_ID}
secret-access-key: ${AWS_SECRET_ACCESS_KEY}
実行
# レプリケーション開始
litestream replicate
# アプリと一緒に起動
litestream replicate -exec "node server.js"
# リストア
litestream restore -o /var/lib/app.db s3://my-app-db-backup/app.db
Dockerでの使用
FROM node:20-slim
RUN apt-get update && apt-get install -y wget \
&& wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz \
&& tar -xzf litestream-v0.3.13-linux-amd64.tar.gz \
&& mv litestream /usr/local/bin/ \
&& rm litestream-v0.3.13-linux-amd64.tar.gz
COPY litestream.yml /etc/litestream.yml
COPY . /app
WORKDIR /app
CMD ["litestream", "replicate", "-exec", "node server.js"]
Turso — エッジSQLite
TursoはlibSQLベースのマネージドSQLiteサービスです。
特徴
- グローバルレプリケーション: 世界中にデータを配置
- エッジコンピューティング: 低レイテンシ
- HTTP API: サーバーレス対応
- 無料枠: 500MBストレージ、10億行読み取り/月
セットアップ
# Turso CLIインストール
brew install tursodatabase/tap/turso
# ログイン
turso auth login
# データベース作成
turso db create my-app
# 接続URL取得
turso db show my-app --url
turso db tokens create my-app
Node.jsから接続
npm install @libsql/client
import { createClient } from '@libsql/client'
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
})
// クエリ
const result = await client.execute('SELECT * FROM users')
// プリペアドステートメント
const user = await client.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [1],
})
// トランザクション
const tx = await client.transaction()
try {
await tx.execute({
sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
args: ['Alice', 'alice@example.com'],
})
await tx.execute({
sql: 'INSERT INTO posts (title, author_id) VALUES (?, ?)',
args: ['Hello', 1],
})
await tx.commit()
} catch (error) {
await tx.rollback()
throw error
}
Drizzleとの統合
import { drizzle } from 'drizzle-orm/libsql'
import { createClient } from '@libsql/client'
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!,
})
export const db = drizzle(client)
Cloudflare D1
Cloudflare WorkersのネイティブSQLiteです。
データベース作成
npx wrangler d1 create my-db
# wrangler.toml
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "your-database-id"
Workerから使用
export interface Env {
DB: D1Database
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { results } = await env.DB.prepare(
'SELECT * FROM users WHERE id = ?'
).bind(1).all()
return Response.json(results)
},
}
バッチクエリ
const batch = [
env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Alice'),
env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Bob'),
env.DB.prepare('INSERT INTO users (name) VALUES (?)').bind('Charlie'),
]
await env.DB.batch(batch)
LiteFS — 分散SQLite
LiteFSはFUSEベースの分散ファイルシステムで、SQLiteをクラスターで実行できます。
特徴
- プライマリ/レプリカ: 自動フェイルオーバー
- 強い整合性: プライマリで書き込み、レプリカで読み取り
- Fly.io統合: Fly.ioで簡単にデプロイ
設定例
# litefs.yml
fuse:
dir: "/litefs"
data:
dir: "/var/lib/litefs"
lease:
type: "consul"
advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202"
candidate: ${FLY_REGION == PRIMARY_REGION}
promote: true
consul:
url: "${FLY_CONSUL_URL}"
key: "my-app/primary"
パフォーマンス最適化
インデックス
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
ANALYZE
db.exec('ANALYZE')
接続プール(複数プロセス)
import { Pool } from 'generic-pool'
import Database from 'better-sqlite3'
const pool = Pool.createPool({
create: () => new Database('app.db'),
destroy: (db) => db.close(),
}, {
max: 10,
min: 2,
})
async function query(sql: string) {
const db = await pool.acquire()
try {
return db.prepare(sql).all()
} finally {
await pool.release(db)
}
}
まとめ
SQLiteは以下のシナリオで最適です。
- 低〜中トラフィック: 月100万PVまで十分対応
- 読み取り中心: ブログ、CMS、カタログサイト
- エッジデプロイ: Cloudflare Workers、Vercel Edge
- オフライン対応: PWA、モバイルアプリ
Litestream、Turso、D1などの新ツールにより、SQLiteはPostgreSQLやMySQLの代替として十分に使える選択肢になっています。