CloudflareのD1の使い方|失敗しない接続手順

CloudflareのD1の使い方|失敗しない接続手順

こんにちは。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全体像で整理しています。

D1を選びやすい用途
  • ユーザー、タスク、予約、記事メタデータなど行で管理する情報
  • 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.tswrangler.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

設定にはbindingdatabase_namedatabase_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結果にidtitlecompletedcreated_atが並ぶことです。SQL構文エラーなら行番号付近を確認し、table not foundなら実行対象名と--localの有無を見直します。何度実行しても壊れにくいよう、最初の作成SQLにはIF NOT EXISTSを付けています。

ローカル確認の3段階
  • 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全体が安全になるわけではありません。

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を検討します。

localとremoteの安全ルール
  • 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 FreeWorkers Paid
Rows read500万 / day250億 / month込み、超過0.001 USD / 100万行
Rows written10万 / day5,000万 / month込み、超過1 USD / 100万行
Storage5GB / account5GB込み、超過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 PLANSEARCH ... USING INDEXになっているか確認します。各クエリのmeta.rows_readD1 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がundefinedBinding名設定のbindingとコードを一致
no such tablelocal / remote対象DBへmigrationを適用
管理画面にデータがない--localローカルDBとリモートDBを区別
読み取りが急増rows_read・Cronindex、頻度、全表走査を修正
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も用意したうえで公開しましょう。