
こんにちは。Osukeラーニング、運営者の「osuke」です。
Cloudflare Workersで画面やAPIを公開できたあと、「フォームの内容はどこへ保存するのか」「D1を作ったのにWorkerから接続できない」「ローカルでは動くのに本番へデータがない」と止まっていませんか。D1はSQLiteに近いSQLを使える一方、データベース名、Binding名、localとremoteという3つの境界を混同すると、初心者ほど原因を見つけにくくなります。
結論からいうと、D1は「Workerを作る→D1を作る→schemaをローカルへ適用する→Bindingを設定する→prepared statementで問い合わせる→migrationを本番へ適用する」の順に進めれば接続できます。最初から管理画面とCLIを行き来するより、Wrangler設定を中心に一本化し、各段階でSELECT結果を確認するのが失敗を減らす近道です。
この記事はosuke本人のD1利用体験としては書かず、2026年8月26日時点のCloudflare公式資料と、Xで公開された9件の実装・移行・障害・課金・バックアップ事例を照合しました。投稿者の結果はそれぞれの環境に限られ、D1全体の保証ではありません。Workers自体が初めてならCloudflare Workersの始め方、先に無料枠を知りたいならWorkers料金と課金の注意点を確認してください。
記事のポイント
- D1作成からWorkers Bindingまでを同じ名前でつなぐ
- 最初は
--localでschemaとAPIを確認してから本番へ進む - 値は
prepare().bind()で渡し、文字列連結SQLを避ける - migration・index・rows_read・exportを公開前に確認する
Cloudflare D1の使い方を準備から順に進める
D1を最短で動かす鍵は、作業を「データベース作成」「テーブル作成」「Workerとの接続」に分けることです。ここではCloudflare公式のGetting startedに沿い、タスクを保存する小さなAPIを例に、各段階の成功判定まで示します。
D1はWorkersから使えるSQLite系データベース
Cloudflare公式のD1概要では、D1はSQLiteのSQLセマンティクスを使えるマネージド・サーバーレスデータベースと説明されています。サーバー台数や常時起動するDBインスタンスを先に決めず、WorkersやPages FunctionsからBinding経由で問い合わせられるため、個人開発のフォーム、タスク、履歴、設定、一覧データを保存する入口として理解しやすいサービスです。
Xでも用途は一つではありません。にゃん太郎氏はCloudflare・D1(R2)・TanStackでキャンペーン集約カレンダーを作り、美濃加茂まむ氏はIMAPから5分ごとに新着メールをD1へ差分保存しています。後者はUIDを同期位置として記録し、新着1通の追加と重複0件まで確認しており、D1が「表示するデータ」だけでなく「前回どこまで処理したか」という状態管理にも使える例です。
ただし、画像や動画の本体、巨大ファイル、高頻度に同じキーへ書き続ける処理までD1へ集約する必要はありません。D1は構造化した行をSQLで検索・更新するときに向き、ファイル本体はR2、単純なキー参照はKV、強い同時更新制御が必要ならDurable Objectsという選択肢があります。Cloudflare全体の役割は初心者向けのCloudflare全体像で整理しています。
- ユーザー、タスク、予約、記事メタデータなど行で管理する情報
- WHERE・ORDER BY・JOINで検索したいデータ
- Workersと同じCloudflareアカウント内で完結させたいAPI
- SQLite系SQLを使い、小さく始めたいWebアプリ
最初にWorkerプロジェクトと実行環境を用意する
Cloudflare公式Getting startedでは、CloudflareアカウントとNode.jsを用意し、create cloudflareからWorkerプロジェクトを作ります。Node.jsはバージョン管理ツールの利用が案内され、Wranglerはプロジェクトへローカルインストールされます。グローバルへ古いWranglerを入れたまま使うより、各コマンドをnpx wranglerで実行するとプロジェクト側の版を使いやすくなります。
npm create cloudflare@latest -- d1-tutorial
cd d1-tutorial
npx wrangler --version
対話画面では、最初の練習ならHello World、Worker only、TypeScript、Git利用あり、即時deployなしを選びます。先にdeployすると問題が起こるわけではありませんが、D1のschemaとBindingがない状態で公開URLだけを作ると、次の変更が本番操作なのかローカル操作なのか分かりにくくなります。まず生成されたsrc/index.tsとwrangler.jsoncを確認し、開発サーバーが起動するところまでを準備完了とします。
Xでは、Sheva氏がTanStack Start・Hono・Workers・SQLite・Drizzleを組み合わせる構成を紹介しています。便利なフレームワークやORMは有力ですが、初回から全部を足すと接続失敗の場所が増えます。本記事ではWorkerとD1の生のBindingを先に通し、SQLが返ることを確認してからHonoやDrizzleへ置き換える順序を勧めます。
- プロジェクト直下に
wrangler.jsoncがある src/index.tsがあるnpx wrangler --versionがエラーなく返る- Gitで変更前の状態を記録できる
D1データベースを作成して設定へ追加する
プロジェクト内で次のコマンドを実行し、リモートD1を1つ作ります。名前はスペースを避け、用途と環境が分かるprod-d1-tutorialのようなASCII名にします。コマンド後にWranglerから設定へ追加するか聞かれた場合はYesを選ぶと、データベースIDとBinding設定がwrangler.jsoncへ追加されます。
npx wrangler@latest d1 create prod-d1-tutorial
設定にはbinding、database_name、database_idの3項目があります。データベース名はCloudflare上の実体を識別し、Binding名はWorkerコードから呼ぶ変数です。本記事ではBinding名をDBに統一します。コードがenv.DBなのに設定がbinding: "MY_DB"なら接続できないため、大文字小文字を含めて同じ名前にしてください。
{
"d1_databases": [
{
"binding": "DB",
"database_name": "prod-d1-tutorial",
"database_id": "Wranglerが出力したUUID"
}
]
}
D1を先に管理画面で作った場合も、Workers & Pagesの対象WorkerからBindingsへ進み、D1 databaseを追加できます。ただし、管理画面だけで設定し、手元のwrangler.jsoncへ反映しないまま次回deployすると差分が生じます。チームやAIコーディングで再現できる状態を残すなら、最終的なBindingは設定ファイルでも確認し、データベースIDを別の環境のものへ取り違えないようにします。
- database_name: Cloudflare上のDB名
- database_id: DB固有のUUID
- binding: Workerコードから呼ぶ変数名
binding: "DB"ならコードはenv.DB
schema.sqlをローカルD1へ適用して確認する
データベースを作っただけではテーブルがありません。まずプロジェクト直下へschema.sqlを作り、タスク用のtasksテーブルと検索用indexを定義します。主キーはINTEGER PRIMARY KEY、タイトルは必須、完了状態は0か1、作成日時はUTC文字列とします。最初のschemaを小さく保つと、接続の確認前に認証や複雑なJOINへ話が広がりません。
CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_tasks_created_at
ON tasks(created_at);
次に--localを付けてschemaを適用し、テストデータを1件入れ、SELECTします。公式チュートリアルも、最初はローカルD1へ--fileで初期化する流れです。ここで表示されるデータはCloudflare上のリモートDBではなく、プロジェクト内のローカル開発用DBです。管理画面に行が見えなくても失敗ではありません。
npx wrangler d1 execute prod-d1-tutorial --local --file=./schema.sql
npx wrangler d1 execute prod-d1-tutorial --local --command="INSERT INTO tasks (title) VALUES ('D1接続を確認する')"
npx wrangler d1 execute prod-d1-tutorial --local --command="SELECT * FROM tasks"
成功判定は「3 commands executed」のような件数だけでなく、SELECT結果にid、title、completed、created_atが並ぶことです。SQL構文エラーなら行番号付近を確認し、table not foundなら実行対象名と--localの有無を見直します。何度実行しても壊れにくいよう、最初の作成SQLにはIF NOT EXISTSを付けています。
- schema.sqlがエラーなく完了する
- INSERTで1行追加できる
- SELECTで追加したタイトルを読める
- 管理画面に見えないのは
--localなら正常
Workers BindingでD1をenv.DBへ接続する
SQL単体が通ったら、WorkerからD1へ接続します。公式Workers Binding APIによると、設定でbinding: "DB"としたD1はenv.DBとして利用できます。TypeScriptでは環境型へDB: D1Databaseを追加し、fetchの第2引数から受け取ります。接続文字列やDBパスワードを自分で組み立てる方式ではありません。
interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { results } = await env.DB
.prepare("SELECT id, title, completed, created_at FROM tasks ORDER BY id DESC LIMIT 20")
.run();
return Response.json({ tasks: results });
},
};
npx wrangler devで起動し、表示されたローカルURLへアクセスします。先ほど入れた「D1接続を確認する」がJSON内に返れば、Worker→Binding→ローカルD1の経路は完成です。undefinedが出る場合はSQLより先に、設定のBinding名とコードのenv.DBが一致するか、設定ファイルがプロジェクト直下にあるかを確認します。
美濃加茂まむ氏はD1へ保存した実メールをRemote MCPから取得できる入口まで確認しています。D1は複数の利用方法を持ちますが、最初からREST APIやMCPまで広げる必要はありません。まずBindingで1つのSELECTを通すと、データの問題、Workerの問題、外部接続の問題を切り分けられます。
wrangler devが起動する- ローカルURLがHTTP 200を返す
- JSONのtasks配列にテスト行がある
- Binding名が設定とコードで完全一致する
Cloudflare D1を安全に公開・運用する
SELECTが通ったら、登録APIと本番反映へ進みます。ここから重要なのは「動くコード」だけで終わらせず、入力値のBinding、localとremoteの分離、スキーマ変更履歴、復元、行数メトリクスを同じ公開工程へ含めることです。
prepared statementで登録・検索APIを作る
ユーザー入力をSQL文字列へ直接連結せず、prepare()のプレースホルダーへbind()で渡します。公式Getting startedでも、値をBindingすることで任意のSQLを実行されるSQL injectionを防ぐ流れが示されています。タスク登録なら、JSONからtitleを受け取り、空文字や長すぎる値を先に弾いてからINSERTします。
if (request.method === "POST" && pathname === "/api/tasks") {
const body = await request.json<{ title?: string }>();
const title = body.title?.trim();
if (!title || title.length > 120) {
return Response.json({ error: "titleは1〜120文字です" }, { status: 400 });
}
const result = await env.DB
.prepare("INSERT INTO tasks (title) VALUES (?) RETURNING id, title, completed, created_at")
.bind(title)
.first();
return Response.json(result, { status: 201 });
}
登録後は、正常な入力で201と追加行、空文字で400が返ることを両方確認します。成功だけを試すと、AIが生成したコードに入力制限がない、JSON解析例外を処理していない、誰でも更新できるといった問題を見逃します。認証が必要なアプリでは、公開前に利用者IDを取得し、SELECTやUPDATEへ必ず所有者条件を含めてください。
prepared statementは安全性だけでなく、SQLと値の役割を分ける効果があります。検索条件、更新対象、LIMITをすべて文字列連結すると、引用符や型の違いで不具合が増えます。ただしBindingを使っても、テーブル名やORDER BYの列名をユーザー入力から自由に選ばせる設計は別途許可リストが必要です。値をBindingしただけでAPI全体が安全になるわけではありません。
- 値は
?へbind()する - 空文字・長さ・型をWorker側で検証する
- 更新と削除は利用者IDや権限条件を付ける
- テーブル名・列名の動的指定は許可リストにする
localとremoteを分けて本番誤操作を防ぐ
WranglerのD1操作では、--localが手元の開発DB、--remoteがCloudflare上のDBです。公式チュートリアルはローカル初期化を先に行い、リモートへ実行するときは--remoteを付けます。同じdatabase_nameを指定するため、フラグだけを見落とすと「登録したのに管理画面にない」「テストのつもりで本番へINSERTした」という混乱が起こります。
本番へ進む前に、ローカルのSELECT結果を保存し、未コミット変更がないかを確認します。その後、schemaをいきなり本番へ再実行するより、次節のmigrationを適用し、npx wrangler deployでWorkerを公開します。公開URLのGETで空配列または期待した初期データ、POSTで201、異常入力で400を確認すれば、ローカルと同じ経路がリモートでも通っています。
Mehul Mohan氏はD1が全面停止したように見え、当時ステータスページにも表示がないと感じた事例を投稿しています。これは単独報告なのでCloudflare全体の障害とは断定できませんが、外部サービスでは失敗がゼロにならないことは示します。APIはD1エラーを握りつぶさず503などで返し、書き込み再試行が二重登録を生まないようUNIQUE制約やidempotency keyを検討します。
- SQL確認は最初に
--local - 本番操作はコマンド実行前に
--remoteとDB名を読み上げる - 本番schema変更はmigrationに残す
- 外部障害時は503と再試行方針を決める
migrationsとTime Travelで変更に備える
Cloudflare公式Migrationsでは、スキーマ変更を連番付きのSQLファイルとして保存し、適用履歴をd1_migrationsテーブルへ記録します。管理画面でALTER TABLEを直接実行するだけでは、誰がどの順で変更したかをGitから再現できません。最初の試作を越えたら、列追加やindex作成をmigrationへ移します。
npx wrangler d1 migrations create prod-d1-tutorial add-task-owner
npx wrangler d1 migrations apply prod-d1-tutorial --local
npx wrangler d1 migrations apply prod-d1-tutorial --remote
ローカルへ先に適用し、Workerを動かしてからremoteへ進めます。migration実行にはBinding名もDB名も使えますが、公式資料はBinding名が変更され得るため、間違ったBindingへ適用しない観点からDB名の利用を検討するよう案内しています。大きなUPDATEやDELETEは一括で何十万行も変えず、公式Limitsが例示する1,000行程度の小さな単位へ分けます。
Time Travelは常時有効で、Freeは過去7日、Paidは30日まで分単位で復元できます。ただしrestoreは同じDBを上書きし、実行中の問い合わせを中断する破壊的操作です。Sarah Taylor氏は日付付きファイルをローカルとGitHubへ二重保存する運用を紹介しています。長期保存が必要なら公式Export手順も使い、秘密や個人情報を公開リポジトリへ置かないでください。
- migration SQLとGit履歴
- 適用先DB名と実行日時
- Time Travelの復元可能期間
- 長期保存用の暗号化・アクセス制限したexport
indexとrows_readで遅さ・課金を抑える
D1の料金は返却した行数ではなく、クエリが読み取った行数と書き込んだ行数を中心に決まります。2026年8月26日時点の公式Pricingでは、Freeは1日500万rows read・10万rows written・アカウント合計5GBです。Paidは月250億rows readと5,000万rows written、5GBを含み、超過はread 100万行0.001ドル、write 100万行1ドル、保存1GB月0.75ドルです。
| 項目 | Workers Free | Workers Paid |
|---|---|---|
| Rows read | 500万 / day | 250億 / month込み、超過0.001 USD / 100万行 |
| Rows written | 10万 / day | 5,000万 / month込み、超過1 USD / 100万行 |
| Storage | 5GB / account | 5GB込み、超過0.75 USD / GB-month |
| Free超過時 | 問い合わせ停止 | 従量課金 |
Mohit氏はPlanetScaleからD1へCMSを移行後、50万リクエストで1日7億行超を読むクエリを発見し、過大なので調査が必要と述べています。さらに魔法少女伊莉雅氏は30秒間隔の無索引監視SQLにより月982.7億行を読み、約73.27ドル超過した個別事例を公開しました。アクセス数が少なくても「実行頻度×1回の走査行数」が大きければ利用量は膨らみます。
公式Use indexesに従い、WHERE・JOIN・複合条件で頻繁に使う列へindexを作り、EXPLAIN QUERY PLANでSEARCH ... USING INDEXになっているか確認します。各クエリのmeta.rows_readとD1 Metricsも見ます。Miyax氏は行数課金から非正規化を検討していますが、検索性と2MBの行サイズ上限もあるため、まず頻出クエリのindexと計測を優先します。
- 頻出WHERE・JOIN列へindexを設定
EXPLAIN QUERY PLANで全表SCANを探すmeta.rows_readとrows returnedの差を見る- Cronの間隔を掛けて1日・1か月の走査行数を試算
D1で失敗したときの確認順と次の一歩
D1が動かないときは、コード全体を書き直す前に境界を一つずつ確認します。テーブルがないならschema・migration、env.DBがないならBinding、ローカルだけデータがあるなら--localと--remote、遅いならquery plan、Free上限ならMetricsと翌日のUTCリセットという順です。同時に複数箇所を変えると、直った理由も再発条件も残りません。
| 症状 | 最初に確認 | 対処 |
|---|---|---|
| env.DBがundefined | Binding名 | 設定のbindingとコードを一致 |
| no such table | local / remote | 対象DBへmigrationを適用 |
| 管理画面にデータがない | --local | ローカルDBとリモートDBを区別 |
| 読み取りが急増 | rows_read・Cron | index、頻度、全表走査を修正 |
| overloaded | クエリ時間・同時実行 | 重い処理を分割し再試行を制御 |
公式Limitsでは、Freeは最大10 DB、1 DB 500MB、1 Worker invocation当たり50 queries、Paidは50,000 DB、1 DB 10GB、1 invocation当たり1,000 queriesです。各D1は基本的にクエリを1つずつ処理し、1msのクエリなら概算1,000 queries/sec、100msなら10 queries/secと、処理時間がスループットへ直結します。大量処理を1つの長いSQLへ詰めず、小さなbatchに分けます。
今日の一歩は、既存WorkersプロジェクトでD1を1個作り、tasksテーブルへ1行入れ、env.DBからJSONで読むところまでです。登録・認証・ORM・R2連携は、その1本の経路が通ってから加えてください。XではAirtable月約3,600円からD1 0円になったという小規模利用例もありますが、0円はその投稿者の条件です。自分の画面でrows read・written・storageを確認して初めて、自分の無料運用と判断できます。
- D1を1個作成する
- schemaを
--localへ適用する - INSERTとSELECTを1回ずつ成功させる
env.DBからJSONを返す- 本番前にmigration・index・Metricsを確認する
D1、Workers、R2などCloudflareの各機能は更新が続きます。コマンド、料金、上限、復元方法は実行前に公式ドキュメントとCloudflare管理画面を最終確認してください。とくにTime Travel restoreは既存DBを上書きし、Budget Alertは通知であって利用停止を保証する仕組みではありません。重要データは復元手順をテストし、別経路のexportも用意したうえで公開しましょう。

