- D1 の位置づけ — SQLite セマンティクスを持つサーバーレス SQL データベースの特性と、リードレプリカの仕組み
wrangler d1 createによるデータベース作成と、migrations によるスキーマのバージョン管理- Worker からの
prepare/bind/run/first/batchを使った安全なクエリ発行 - ローカル開発用データベースと本番データベースの関係(
--local/--remote) - 制限と料金モデル(rows read / rows written)を踏まえたインデックス設計
D1 の位置づけ — SQLite セマンティクスのサーバーレス SQL
D1 は Cloudflare のマネージドなサーバーレス SQL データベースです。クエリエンジンには SQLite を採用しており、CREATE TABLE や JOIN、外部キー制約といった標準的な SQL に加え、FTS5(全文検索)・JSON 関数・数学関数などの SQLite 拡張が使えます。Free プランでも利用でき、Worker からはバインディング経由で直接クエリを発行します。第 5 章の KV が「結果整合性の key-value」だったのに対し、D1 はリレーショナルなスキーマとインデックスを持つ「本物の SQL データベース」です。
アーキテクチャ上の重要な特性が 2 つあります。
- 1 データベース = 1 プライマリインスタンス。各 D1 データベースは内部的に単一の Durable Object に支えられており、シングルスレッドでクエリを 1 つずつ処理します。スループットはクエリの実行時間に直結し、平均 1 ms のクエリなら毎秒約 1,000 クエリ、100 ms なら毎秒 10 クエリが目安です。
- 10 GB × 多数への水平分割が前提の設計。1 データベースの上限は 10 GB(拡張不可)ですが、データベース自体はテナント単位・ユーザー単位で数万個作れます(データベース数自体には追加課金なし)。「巨大な 1 つの DB」ではなく「小さな DB を大量に」が D1 流のスケール戦略です。
リードレプリカと Sessions API
既定では読み書きの全クエリが世界のどこか 1 箇所にあるプライマリインスタンスへ届くため、遠いユーザーほどレイテンシが伸びます。グローバルリードレプリカを有効にすると(ダッシュボードの Settings から)、D1 は対応全リージョン(ENAM / WNAM / WEUR / EEUR / APAC / OC。2026-08 時点)に読み取り専用のコピーを自動作成し、読み取りクエリを近いレプリカへ振り分けます。
| インスタンス種別 | 書き込みクエリ | 読み取りクエリ |
|---|---|---|
| プライマリ | 処理できる | 処理できる |
| リードレプリカ | プライマリへ転送する | 自身のコピーで処理できる |
レプリカへの複製は非同期のため、レプリカは常に少し古い可能性があります(レプリカラグ)。この一貫性問題を解決するのが Sessions API です。env.DB.withSession() でセッションを開始すると、セッション内の全クエリに逐次一貫性(sequential consistency)が保証されます。逆に言えば、Sessions API を使わない限りリードレプリカは使われず、全クエリがプライマリで実行されます。開始パラメータは "first-primary"(最初のクエリを必ずプライマリへ)、"first-unconstrained"(既定。どのインスタンスでもよい)、または前回セッションの bookmark 文字列の 3 通りです。
もう 1 つ運用面で心強いのが Time Travel です。常時有効・追加コストなしで、過去 30 日(Free プランは 7 日。2026-08 時点)の任意の 1 分単位の状態へ npx wrangler d1 time-travel restore で復元できます。WHERE 句を忘れた DELETE をやらかしても戻れます。
Worker Binding API — prepare / bind / run / batch
Worker から D1 を使うには、wrangler.jsonc に d1_databases バインディングを定義します。binding に指定した名前が env オブジェクトのプロパティ名になります。
{
"d1_databases": [
{
"binding": "DB", // Worker からは env.DB でアクセス
"database_name": "bookshelf-db",
"database_id": "<YOUR_DATABASE_ID>" // wrangler d1 create の出力の ID に置き換える
}
]
}
クエリは「①prepare() で SQL 文を準備 → ②bind() でプレースホルダ ? に値を束縛 → ③実行メソッドで結果を受け取る」の 3 段階で発行します。値を SQL 文字列に直接埋め込まず bind() を通すことで、SQL インジェクションを構造的に防げます。実行メソッドは用途別に 4 つあります。
| メソッド | 返り値 | 用途 |
|---|---|---|
run() / all() | D1Result(results 配列 + meta) | 基本形。all() は run() の別名 |
first() | 先頭行のオブジェクト。行がなければ null | 1 件取得。列名を渡すとその値だけ返す |
raw() | 配列の配列(メタデータなし) | 大量行を軽量に受け取る |
batch() | D1Result の配列 | 複数文を 1 往復で実行(後述) |
D1Result の meta には rows_read(スキャンした行数)、rows_written、last_row_id(直近の INSERT の ROWID)、changes(変更された行数)、どのインスタンスが処理したかを示す served_by_region / served_by_primary(リモート実行では常に返り、ローカル開発では undefined)などが入ります。料金は rows read / written に基づくため、meta はコストの実測装置でもあります。
batch() は複数の prepared statement を 1 回のネットワーク往復でまとめて実行します。各文は順番に・並行せずに実行され、全体が SQL トランザクションとして扱われます。途中の文が失敗するとその文のエラーが返り、シーケンス全体がロールバックされます。D1 は明示的な BEGIN TRANSACTION を書かない自動コミット動作のため、複数文の原子性が必要な場面では batch() が第一の選択肢です。
// 単発クエリ: prepare → bind → run(TypeScript は型パラメータで行の型を付けられる)
const { results, meta } = await env.DB
.prepare("SELECT * FROM books WHERE author = ?")
.bind("夏目漱石")
.run<BookRow>();
console.log(meta.rows_read); // このクエリがスキャンした行数
// batch: 同じ prepared statement を使い回して 1 往復で実行(暗黙のトランザクション)
const stmt = env.DB.prepare("INSERT INTO books (title, author) VALUES (?, ?)");
const batchResults = await env.DB.batch([
stmt.bind("こころ", "夏目漱石"),
stmt.bind("羅生門", "芥川龍之介"),
]);
このほかにパラメータ束縛なしで文字列をそのまま実行する exec() もありますが、prepared statement より安全性・性能とも劣るため、公式ドキュメントもメンテナンスや一回限りの作業専用としています。アプリケーションコードでは使いません。
migrations とローカル開発・本番の関係
スキーマ変更をダッシュボードから手で流すと、環境間の差分がすぐ追えなくなります。D1 には Wrangler 組み込みのマイグレーション機構があり、migrations/ フォルダに連番付き .sql ファイルとしてスキーマ変更を積み上げます。適用済みのマイグレーションはデータベース内の d1_migrations テーブルに記録されるため、apply は未適用分だけを順に実行します。適用中にエラーが起きるとそのマイグレーションはロールバックされ、直前までの適用状態が保たれます。
npx wrangler d1 migrations create <DB名> <説明>— 空のマイグレーションファイルを生成npx wrangler d1 migrations list <DB名>— 未適用のマイグレーションを一覧npx wrangler d1 migrations apply <DB名>— 未適用分を順に適用
そして D1 のローカル開発は「本番の複製に触る」のではなく、完全に別物のローカル DB を使います。wrangler dev は既定でローカルモードで起動し、D1 の実体は .wrangler/state/v3/d1 配下の SQLite ファイルです(Wrangler v3 以降は実行をまたいで永続化されます)。d1 execute や migrations apply には --local / --remote フラグがあり、向き先を明示できます。開発フローは次の順序が基本です。
migrations apply <DB名> --localでローカル DB にスキーマを適用するwrangler devでローカル DB 相手に実装・動作確認するmigrations apply <DB名> --remoteで本番 DB にスキーマを適用するwrangler deployで Worker をデプロイする
本番データ相手にローカル開発したい場合は、バインディング設定に "remote": true を付ける remote bindings という仕組みもありますが、本番への変更は取り消せない点に注意してください。
制限と料金
主要な制限は次のとおりです(2026-08 時点)。
| 項目 | Free | Workers Paid |
|---|---|---|
| データベース数 / アカウント | 10 | 50,000(申請で増枠可) |
| 1 データベースの最大サイズ | 500 MB | 10 GB(増枠不可) |
| アカウント合計ストレージ | 5 GB | 1 TB(申請で増枠可) |
| Worker 呼び出しあたりのクエリ数 | 50 | 1,000 |
| Time Travel の復元可能期間 | 7 日 | 30 日 |
プランに依存しない制限として、SQL 文の最大長 100 KB、1 クエリのバインド変数は最大 100 個、クエリの最大実行時間 30 秒、1 テーブル最大 100 列、行の最大サイズ 2 MB があります。batch() ではこれらの個別制限が内側の各文に適用され、さらに 30 秒制限はバッチ呼び出し全体にもかかります。
料金は「読んだ行数・書いた行数・ストレージ」の 3 軸だけで、待機コストはゼロ(scale-to-zero)、エグレス課金もありません(2026-08 時点)。
| 指標 | Free | Workers Paid |
|---|---|---|
| Rows read | 500 万行 / 日 | 月 250 億行込み + 100 万行あたり 0.001 USD |
| Rows written | 10 万行 / 日 | 月 5,000 万行込み + 100 万行あたり 1.00 USD |
| ストレージ | 合計 5 GB | 5 GB 込み + 1 GB・月あたり 0.75 USD |
インデックスで rows_read を減らす
ここで重要なのは、rows read が「返した行数」ではなく「スキャンした行数」で数えられることです。インデックスのない列を WHERE で絞ると、5,000 行のテーブルなら 1 行返すだけでも 5,000 行分の read が課金されます。頻繁に検索条件・JOIN 条件に使う列には CREATE INDEX を張りましょう(INTEGER PRIMARY KEY には自動で索引が効くため不要です)。インデックスが効いているかは EXPLAIN QUERY PLAN で確認できます。全表スキャンなら SCAN、インデックス利用なら SEARCH ... USING INDEX と表示されます。
EXPLAIN QUERY PLAN SELECT * FROM books WHERE author = '夏目漱石';
QUERY PLAN
`--SEARCH books USING INDEX idx_books_author (author=?)
インデックス列への書き込みは本体 + インデックスで rows written が 1 行分増えますが、読み取り削減の利益がほぼ常に上回ります。複数列インデックスは「左端の列を含む検索」にだけ効く点((a, b) のインデックスは a 単独には効くが b 単独には効かない)、インデックス作成後に PRAGMA optimize を実行するとクエリプランナの統計が更新される点も覚えておきましょう。
第 1 章の環境(Cloudflare アカウント + Node.js + wrangler ログイン済み)があれば、この章のハンズオンは Free プランで完走できます。所要時間はあわせて 40〜60 分です。
蔵書管理 API「bookshelf」の土台を作ります。Worker プロジェクトを作成し、D1 データベースを作ってバインドするまでで約 10 分です。
- Worker プロジェクトを作成する。テンプレートは Hello World example → Worker only → TypeScript を選ぶ
npm create cloudflare@latest -- bookshelf-api cd bookshelf-api - D1 データベースを作成する
npx wrangler d1 create bookshelf-db✅ Successfully created DB 'bookshelf-db' in region APAC Created your new D1 database. { "d1_databases": [ { "binding": "bookshelf_db", "database_name": "bookshelf-db", "database_id": "<unique-ID-for-your-database>" } ] } - 「Would you like Wrangler to add it on your behalf?」と聞かれたら Yes と答える。wrangler.jsonc に
d1_databasesブロックが自動追記される - 追記されたブロックの
bindingを"DB"に変更する(本章のコードはenv.DB前提)。database_idはそのまま使う
npx wrangler d1 list に bookshelf-db が表示されれば成功です。データベース名は後から変えられないため、用途と環境がわかる名前(例: staging-db-web)を付けるのが公式推奨です。
books テーブルとインデックスをマイグレーションとして作成し、まずローカル DB に適用します。約 10 分です。
- マイグレーションファイルを生成する
npx wrangler d1 migrations create bookshelf-db create_booksmigrations/フォルダに0000_create_books.sqlのような連番付きファイルが生成される - 生成されたファイルに次のスキーマを書く
-- migrations/0000_create_books.sql CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, author TEXT NOT NULL, read INTEGER NOT NULL DEFAULT 0 -- 0: 未読, 1: 読了 ); CREATE INDEX IF NOT EXISTS idx_books_author ON books(author); - ローカル DB に適用する。適用予定の一覧が表示されるので確認して進める
npx wrangler d1 migrations apply bookshelf-db --local
npx wrangler d1 execute bookshelf-db --local --command="PRAGMA table_list" を実行し、books と適用記録用の d1_migrations テーブルが見えれば成功です。
一覧・取得・登録・更新・削除の 5 エンドポイントを実装し、ローカルで動作確認します。約 20 分です。
src/index.tsを次の内容に置き換える(前半: 読み取りと登録)export interface Env { DB: D1Database; } type BookRow = { id: number; title: string; author: string; read: number }; export default { async fetch(request, env): Promise<Response> { const url = new URL(request.url); const m = url.pathname.match(/^\/api\/books\/(\d+)$/); // 一覧: GET /api/books if (request.method === "GET" && url.pathname === "/api/books") { const { results } = await env.DB .prepare("SELECT * FROM books ORDER BY id") .run<BookRow>(); return Response.json(results); } // 1 件取得: GET /api/books/:id if (request.method === "GET" && m) { const book = await env.DB .prepare("SELECT * FROM books WHERE id = ? LIMIT 1") .bind(Number(m[1])) .first<BookRow>(); return book ? Response.json(book) : Response.json({ error: "not found" }, { status: 404 }); } // 登録: POST /api/books if (request.method === "POST" && url.pathname === "/api/books") { const { title, author } = await request.json<{ title: string; author: string }>(); const { meta } = await env.DB .prepare("INSERT INTO books (title, author) VALUES (?, ?)") .bind(title, author) .run(); return Response.json({ id: meta.last_row_id }, { status: 201 }); } return handleWrite(request, env, m); // 更新・削除は下の関数へ }, } satisfies ExportedHandler<Env>;- 続けて同じファイルの末尾に更新・削除を追加する
async function handleWrite( request: Request, env: Env, m: RegExpMatchArray | null, ): Promise<Response> { // 更新: PUT /api/books/:id if (request.method === "PUT" && m) { const body = await request.json<{ title: string; author: string; read: boolean }>(); const { meta } = await env.DB .prepare("UPDATE books SET title = ?, author = ?, read = ? WHERE id = ?") .bind(body.title, body.author, body.read ? 1 : 0, Number(m[1])) .run(); return meta.changes === 0 ? Response.json({ error: "not found" }, { status: 404 }) : Response.json({ updated: meta.changes }); } // 削除: DELETE /api/books/:id if (request.method === "DELETE" && m) { const { meta } = await env.DB .prepare("DELETE FROM books WHERE id = ?") .bind(Number(m[1])) .run(); return meta.changes === 0 ? Response.json({ error: "not found" }, { status: 404 }) : new Response(null, { status: 204 }); } return Response.json({ error: "not found" }, { status: 404 }); } npx wrangler devで起動し、別ターミナルから一連の CRUD を叩くcurl -X POST http://localhost:8787/api/books \ -H "Content-Type: application/json" \ -d '{"title": "吾輩は猫である", "author": "夏目漱石"}' # → {"id":1} curl http://localhost:8787/api/books # → [{"id":1,"title":"吾輩は猫である","author":"夏目漱石","read":0}] curl -X PUT http://localhost:8787/api/books/1 \ -H "Content-Type: application/json" \ -d '{"title": "吾輩は猫である", "author": "夏目漱石", "read": true}' # → {"updated":1} curl -X DELETE http://localhost:8787/api/books/1 # →(204 No Content)
ポイントは 3 つ。ユーザー入力は必ず bind() 経由で渡すこと、1 件取得は first()(0 件なら null)を使うこと、更新・削除の成否は meta.changes で判定することです。read: true で送った値が読み出しでは 1 になるのは、D1 が Boolean を INTEGER に変換して格納するためです。
ここまでの変更はすべてローカル DB 上の話です。同じマイグレーションを本番 DB に適用し、Worker をデプロイします。約 5 分です。
- マイグレーションを本番(リモート)DB に適用する
npx wrangler d1 migrations apply bookshelf-db --remote - Worker をデプロイし、表示された URL に対して 7-3 と同じ curl を実行する
npx wrangler deploy curl -X POST https://bookshelf-api.<YOUR_SUBDOMAIN>.workers.dev/api/books \ -H "Content-Type: application/json" \ -d '{"title": "三四郎", "author": "夏目漱石"}' # → {"id":1}<YOUR_SUBDOMAIN>は deploy 出力に表示される自分の workers.dev サブドメインに置き換える - ダッシュボードの D1 ページで bookshelf-db を開き、Tables から books の中身が入っていることを確認する
登録 API のレスポンスが返り、ダッシュボードにも行が見えれば、リレーショナルな API が世界中から使える状態になっています。
つまずきポイント
ローカル DB(.wrangler/state 配下)と本番 DB は完全に別物です。migrations apply --local だけ実行して --remote を忘れると、デプロイ後の Worker はテーブルのない本番 DB を叩いて失敗します。デプロイ前に「--remote で apply したか」を必ず確認しましょう。
D1_TYPE_ERROR — undefined を bind した
リクエスト JSON にキーが欠けていると bind(undefined) になり、クエリは D1_TYPE_ERROR で落ちます(null は合法、undefined は非対応)。外部入力は検証するか body.title ?? null のように正規化してから束縛します。また型変換は一方通行で、Boolean は INTEGER(0 / 1)として格納・読み出しされます。
クエリの実行時間は最大 30 秒(2026-08 時点)で、数十万行を 1 文で書き換えるデータ移行はまず失敗します。1,000 行ずつなどのチャンクに分割して繰り返し実行してください。batch() にまとめても、SQL 文長 100 KB などの個別制限は内側の各文に適用され、30 秒制限はバッチ全体にかかります。
d1 migrations apply はバインディング名とデータベース名のどちらも受け付けますが、バインディング名は後から変更でき、意図しない DB に適用する事故の元になります。公式ドキュメントの推奨どおり、変更できないデータベース名(本章では bookshelf-db)で指定しましょう。
理解度チェック
Q1. run() と all() の関係は?また first() は該当行がないとき何を返す?
all() は run() の別名(機能的に等価)で、どちらも results と meta を持つ D1Result を返します。first() は先頭行のオブジェクトを直接返し、該当行がなければ null を返します。
Q2. batch() に渡した複数の文の実行はどう保証される?途中で失敗したら?
各文は順番に・並行せずに実行され、全体が SQL トランザクションとして扱われます。途中の文が失敗するとその文のエラーが返り、シーケンス全体がロールバックされます。複数文の原子性が必要なら batch() を使います。
Q3. リードレプリカを有効にしただけでは読み取りが速くならないのはなぜ?
レプリカへのルーティングは Sessions API(env.DB.withSession())を使ったクエリにだけ働くためです。従来どおりの env.DB.prepare() 直接呼び出しは全てプライマリで実行されます。なお書き込みは常にプライマリへ転送されます。
Q4. 5,000 行のテーブルに対する全表スキャンの SELECT が 1 行だけ返すとき、rows read はいくつ?
5,000 行です。課金は「返した行数」ではなく「スキャンした行数」で数えられます。検索条件の列にインデックスを張れば、スキャン範囲が絞られ rows read(= コスト)とレイテンシの両方が下がります。
Q5. 適用済みマイグレーションの記録はどこに保存される?
対象データベース内の d1_migrations テーブルに記録されます(テーブル名は wrangler.jsonc の migrations_table で変更可能)。migrations apply はこの記録と migrations/ フォルダを突き合わせ、未適用分だけを順に実行します。
まとめと次章へ
- D1 は SQLite セマンティクスを持つサーバーレス SQL。1 DB = 1 プライマリ(シングルスレッド)で、10 GB × 多数の水平分割が前提の設計
- クエリは
prepare→bind→run/first/raw。複数文の原子性はbatch()、結果のmetaで rows read / written を実測できる - スキーマは migrations(create / list / apply)でバージョン管理し、
--localで試してから--remote→deployの順で本番へ - 料金は rows read / rows written / ストレージの 3 軸。インデックスと
EXPLAIN QUERY PLANで rows read を削る - リードレプリカは Sessions API とセットで初めて有効。Time Travel で過去 30 日(Paid)の任意時点へ復元できる
D1 で「構造化データの置き場」は手に入りました。次章の Durable Objects は、D1 の裏側も支えている「世界に 1 つだけのステートフルなインスタンス」を直接扱い、リアルタイム協調やロックといった D1 / KV では書けない種類の状態管理を可能にします。