章と記事の一覧
動かすストレージ
D1
SQLite。SQL、索引、課金される「行数」
サーバーレスアーキテクチャでリレーショナルデータベースを扱おうとすると、長らく二つの壁に阻まれてきた。長距離ネットワークによる通信遅延と、接続プール(コネクションプール)の枯渇である。
D1 は SQLite の SQL を使うサーバーレスのデータベースである。既定では 1 か所のプライマリがすべてのクエリを処理するため、遠いロケーションからは通信遅延が残る。リードレプリカを有効にして Sessions API(withSession())を使うと、読み取りクエリを世界各地のレプリカへ分散できる。書き込みは常にプライマリで実行される。
クエリ実行の 3 原則
// テーブル定義やマイグレーションの実行
await env.DB.prepare("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)").run();
// プレースホルダーを用いた安全なパラメータ注入
await env.DB.prepare("INSERT INTO notes (body) VALUES (?)").bind("hello").run();
// 検索結果の取得
const { results } = await env.DB.prepare("SELECT * FROM notes").all();- 文字列連結による SQL の組み立ては厳禁である。
prepare(...).bind(...)を用いてエスケープ処理をドライバーに任せ、SQL インジェクションの脆弱性を排除する。 exec()は改行文字で文を分割して実行するという癖を持つ。複数行にわたる複雑な DDL を渡すと予期しない箇所で文がちぎれて構文エラーを引き起こす。DDL もprepare(...).run()で単一文として渡すのが堅牢である。- 複数行の取得には
all()、先頭 1 行だけの抽出にはfirst()を呼び分ける。
課金モデルの勘所
D1 の料金体系で最も注意すべき指標は、クエリの発行回数ではなく、ディスクから読み出した行数と書き込んだ行数である。
WHERE 句の対象カラムにインデックスが張られていない場合、データベースはテーブル全体の全行スキャンを実行する。10 万行のテーブルに対するたった 1 回の無邪気な検索が、10 万行分の読み取り課金を発生させる。CREATE INDEX の設計を怠ってはならない。
| 操作種別 | 無料枠 | 有料プラン |
|---|---|---|
| 行読み取り | 500 万行/日 | 250 億行/月まで基本料込み |
| 行書き込み | 10 万行/日 | 5,000 万行/月まで基本料込み |
エディタ上の env.DB は、実際の D1 を RPC で中継したもので、同じ SQLite のクエリエンジンが動作する。使えるのは prepare()(bind() / run() / all() / raw() / first())と exec() だけで、batch() や withSession() はない。データベースはこのサイトの全員で共有している。
export default {
async fetch(request, env) {
// DDL は prepare().run() で実行(exec() は改行で文を分割するため注意)
// Execute DDL with prepare().run() (exec() splits statements on newlines)
await env.DB.prepare(
"CREATE TABLE IF NOT EXISTS playground_notes (id INTEGER PRIMARY KEY AUTOINCREMENT, body TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')))",
).run();
// プリペアドステートメントで値を安全にバインド(SQLインジェクション防止)
// Bind parameters safely using prepared statements (prevents SQL injection)
await env.DB.prepare("INSERT INTO playground_notes (body) VALUES (?)")
.bind("note " + new Date().toISOString())
.run();
const count = await env.DB.prepare("SELECT COUNT(*) AS n FROM playground_notes").first();
console.log("rows =", count.n);
// 注意: このインデックスは下の ORDER BY id では使われない。WHERE created_at … のためのもの
// Note: this index is not used by the ORDER BY id query below; it serves WHERE created_at … queries
await env.DB.prepare(
"CREATE INDEX IF NOT EXISTS idx_notes_created ON playground_notes (created_at)",
).run();
const { results } = await env.DB.prepare(
"SELECT id, body, created_at FROM playground_notes ORDER BY id DESC LIMIT 5",
).all();
return Response.json({ total: count.n, latest: results });
},
};
実行結果
実行するとここに結果が出ます。ネットワークは塞がれていますが、D1・KV・R2・Durable Object の状態は訪問者全員で共有されます。