CloudflareではSQLiteをベースとしたデータベースを扱うのに2つの方法があります。
- Cloudflare D1
- SQLite storage in Durable Object
前者は最初にCloudflare上でデータベースを扱うことが出来る製品として大きく注目を集めてリリースされたSQLiteベースのフルマネージドなデータベースサービスです。大きな特徴としてはSQLiteとしてデータベースを構築しますが、Cloudflareということでリージョンという概念がなく、グローバルにデータベースを扱うことが出来るという点です。
それから経過した後に、Durable Object上に直接SQLiteを配置して扱うような機能がDurable Objectで使用できるようになりました。後者はどちらかというとD1のフルマネージドな機能を取っ払って、SQLiteを直接使用できるようにしたような製品です。(たぶん内部的な実装をオープンにした感じかなという印象です)
さらには直近の2025年4月のDeveloper WeekでDurable Objectの無料枠が追加され、使用するハードルが下がったことから、D1とSQLite storage in Durable Objectの違いを改めて知っておいて損は無いと思います。
Cloudflare Workers上での使用方法の違い
まず使用方法から違いがあります。どちらかというとSQLite storage in Durable Objectの方が癖のある使い方をする印象です。簡単なデータを取得するコードだけ見ていきます。
最初にD1を使うコードから見ていきます。
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
...
"d1_databases": [
{
"binding": "DB",
"database_id": "YOUR_D1_DATABASE_ID",
"database_name": "your_database_name",
}
],
}
src/index.ts
export interface Env {
DB: D1Database;
}
export default {
async fetch(request, env): PromiseResponse> {
const { results } = await env.DB.prepare(
"SELECT * FROM Users WHERE name = ?",
)
.bind("tom")
.all();
return Response.json(results);
},
} satisfies ExportedHandlerEnv>;
Users
というテーブルからname
がtom
のデータを取得するコードです。DB
という環境変数にD1を設定するにはwrangler.jsonc
などに記述することでこのように簡単に使用することが出来ます。
次にSQLite storage in Durable Objectを使うコードを見ていきます。
wrangler.jsonc
{
"$schema": "./node_modules/wrangler/config-schema.json",
...
"durable_objects": {
"bindings": [
{
"name": "YOUR_DURABLE_OBJECT_NAME",
"class_name": "YourDurableObjectClassName"
}
]
},
"migrations": [
{
"tag": "v1",
"new_sqlite_classes": ["YourDurableObjectClassName"]
}
]
}
src/index.ts
export interface Env {
YOUR_DURABLE_OBJECT_NAME: DurableObjectNamespaceimport("./src/index").YourDurableObjectClassName>;
}
export class YourDurableObjectClassName extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
ctx.blockConcurrencyWhile(async () => {
await this._migrate();
});
}
getUsers() {
const cursor = this.sql.exec(
"SELECT * FROM Users WHERE name = ?",
"tom",
)
return { results: cursor.toArray() }
}
async _migrate() {
this.sql.exec(`CREATE TABLE IF NOT EXISTS "Users" (
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL,
email TEXT NOT NULL
);`
);
}
}
export default {
async fetch(request, env): PromiseResponse> {
const id = c.env.CHECK_SQLITE_DURABLE_OBJECT.idFromName("db");
const stub = c.env.CHECK_SQLITE_DURABLE_OBJECT.get(id);
const { results } = stub.getUsers();
return Response.json(results);
},
} satisfies ExportedHandlerEnv>;
パッと見た感じどうでしょうか?D1に比べると使用方法がガラッと変わるのわかると思います。SQLite storage in Durable ObjectにはDurableObject
というDurable Objectを使用するためのクラス宣言が出てきたりします。またD1と大きく違うのはfetch
関数から直接DBを操作するようなDMLを直接実行することは出来ません。
src/index.ts
export class YourDurableObjectClassName extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
}
get db() {
return this.sql
}
}
export default {
async fetch(request, env): PromiseResponse> {
const id = c.env.CHECK_SQLITE_DURABLE_OBJECT.idFromName("db");
const stub = c.env.CHECK_SQLITE_DURABLE_OBJECT.get(id);
const { results } = stub.db.exec("SELECT * FROM Users");
}
} satisfies ExportedHandlerEnv>;
必ずDurableObject
のクラス側にか実装することが出来ません。これはDurableObject
自体が実行するCloudflare Workersとは別のもの(たぶんDurable ObjectもCloudflare Workersで実装されているのでしょう)として定義されて、Cloudflare WorkersとDurable Objectは内部的に通信してデータのやり取りを行うためです。
通信してデータのやり取りを行うのでDurable Objectの内部に保持しているSQLiteおよびその接続情報は外に出せず、あくまでDurable Object内部でSQLiteへの問い合わせ結果をやり取りするくらいしか出来ないようになっています。
運用の違い
実行方法の違い以外に運用に関する違いが大きくあります。「使用できる」と「運用できる」という差には大きな差がありますが、D1とSQLite storage in Durable Objectにはこの運用を見据えた際に大きな差が出てきます。
マイグレーション方法
データベースは一度定義すればその後はデータの増減のみ管理すればいいだけかというとそうではありません。アプリケーションが変化することでそれに耐えるべくデータベースの形も変化していきます。
D1にはwrangler d1 migrate
というコマンドが存在し、データベースを変更するDDLを管理する機能があります。このコマンドを実行することでデフォルトではmigrates/
というディレクトリの中のSQLファイルを実行してくれます。もちろんこのwrangler d1 migrate
は実行したマイグレーションファイルは実行されず、実行されていないマイグレーションファイルだけを実行してくれるという機能も存在しています。これによりD1のデータベースの変更などは比較的容易に可能です。
逆にSQLite storage in Durable ObjectにはD1のようなマイグレーションを行ってくれるCLIや機能は存在しません。ですので、自身でどうにかしてDurable ObjectにあるSQLiteのデータベースをマイグレーションする必要があります。
先ほどのサンプルコードでも出てきたのですが、DurableObject
のクラスを定義の中にこのような記述があったと思います。
export class YourDurableObjectClassName extends DurableObject {
sql: SqlStorage;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
this.sql = ctx.storage.sql;
ctx.blockConcurrencyWhile(async () => {
await this._migrate();
});
}
...
async _migrate() {
this.sql.exec(`CREATE TABLE IF NOT EXISTS "Users" (
id INTEGER PRIMARY KEY NOT NULL,
username TEXT NOT NULL,
email TEXT NOT NULL
);`
);
}
}
blockConcurrencyWhile
という処理の中で_migrate
関数つまりはテーブルを作成するDDLを実行しています。これはこの初期化が完了するまでこのDurable Objectの使用を待つという意味です。なので先程のコードの例だとリクエストが来た段階でDurable Objectのインスタンスを作成していますが、その作成時にDurable Objectのインスタンス化する時つまりはコールドスタートする時にこのマイグレーションが動作することになります。
これはD1とは大きくことなり、いくつかのデメリットが含まれています。
- 実リクエストが来るまで、DBへのマイグレーションが実行されない
- インスタンス化されたものを使用続ける限り問題はないが、コールドスタートになった場合にマイグレーション分の実行時間が必要となる
D1は事前にマイグレーションするためにCloudflare Workersの実行コストとは直接関係ありません。しかし、SQLite storage in Durable Objectはそうではなく、Cloudflare Workersが実行されて初めてマイグレーションが行われます。マイグレーションの結果次第では新バージョンのアプリケーションをデプロイさせないということが少し難しいです。また、実行時にマイグレーションを行うので、Durable Objectのインスタンス化したものがなくなってしまえばまたインスタンス化するのですが、その際にマイグレーション処理が動作してしまいます。このマイグレーションという点ではSQLite storage in Durable Objectは非常に運用しづらい点となっています。
サポート機能の違い
D1にはwrangler d1 migrate
コマンド以外にもマネージドデータベースと便利な機能がいくつは存在しています。例えばwrangler d1 export
でD1のSQLiteのデータを取得できたり、wrangler d1 time-travel
によりタイムトラベル機能によってバックアップされたデータの復元などが容易に行えます。
また、D1には簡易的ですが、Cloudflareのコンソール上にデータベースの中を参照できる機能なども存在します。
このようにD1には必要最低限ですが、マネージドデータベースとして持っておいてほしい機能が存在しますが、SQLite storage in Durable ObjectにはCloudflareからはD1に似た機能は一切提供されません。ですので必要ならば自身で構築する必要があります。
速度の違い
D1は色々と速度に懸念があると言われ続けて来ました。そこで単純にDurable Objectに乗ったSQLiteならばその点はクリアになっているのではないかという想像から実際に測ってみました。
測るために使用したアプリケーションは次のリポジトリのコードを実行してみましたが、ここに記載する内容を以下の条件化で速度を見てみました。
- テーブルに対して
DELETE
を発行して、100件のINSERT
文を1回としたSQLを1000回(10万件ほど)実行する時間 - テーブルに対して
SELECT COUNT(id)
のSQLを実行する時間 - 10万件あるテーブルから
LIMIT 50
でデータを取得するSQLを実行する時間 - 10万件あるテーブルから
LIMIT 50
でデータを取得するSQLを100回実行する時間 - インデックスの効いていないカラムに
LIKE
検索でSQLを実行する時間 - 単純に
INSERT
を1回実行する時間
まずは「テーブルに対してDELETE
を発行して、100件のINSERT
文を1回としたSQLを1000回(10万件ほど)実行する時間」の結果です。
D1 | SQLite storage in Durable Object | |
---|---|---|
1回目 | 45,992 ms | 417 ms |
2回目 | 45,609 ms | 432 ms |
3回目 | 45,477 ms | 453 ms |
4回目 | 43,087 ms | 401 ms |
5回目 | 41,138 ms | 427 ms |
見ての通り、凄まじい差が出ます。D1に対して約100倍の速度でSQLite storage in Durable Objectは処理をします。本当かと疑うレベルですが、実際に次のテーブルのデータ数を取得する処理で正常にデータ件数が返ってくることを確認しているので、合っているとは思います。
次にとても単純なSQLとして「テーブルに対してSELECT COUNT(id)
のSQLを実行する時間」を見ます。
D1 | SQLite storage in Durable Object | |
---|---|---|
1回目 | 50 ms | 29 ms |
2回目 | 38 ms | 26 ms |
3回目 | 58 ms | 21 ms |
4回目 | 39 ms | 23 ms |
5回目 | 90 ms | 26 ms |
これを見ると読み込みでもやはりSQLite storage in Durable Objectの方が早いですが、先程の100倍という差ではなく、2倍程度早いという結果です。それでも2倍というのは脅威ですが。
次にカウントではなく、実際にデータを取得するSQLとして「10万件あるテーブルからLIMIT 50
でデータを取得するSQLを実行する時間」を見ます。
D1 | SQLite storage in Durable Object | |
---|---|---|
1回目 | 55 ms | 19 ms |
2回目 | 37 ms | 19 ms |
3回目 | 33 ms | 21 ms |
4回目 | 50 ms | 21 ms |
5回目 | 40 ms | 16 ms |
D1はさきほどのカウントを取得する処理時間とさほど違いはありませんが、SQLite storage in Durable Object側は気持ち早くなっています。そのせいでD1との差は2.5倍程度まで広がっているようにみえます。
もう少し見るためにそのSQLを100回実行するSQLを発行して速度を見てみます。
D1 | SQLite storage in Durable Object | |
---|---|---|
1回目 | 3,580 ms | 41 ms |
2回目 | 3,709 ms | 44 ms |
3回目 | 3,165 ms | 46 ms |
4回目 | 3,457 ms | 45 ms |
5回目 | 3,128 ms | 41 ms |
これは驚愕の結果です。D1は実行回数が多くなるので比例とまではいきませんが、それなりに実行速度が積み上がります。ところがSQLite storage in Durable Objectに関してはほとんど処理時間が積み上がらず1回の実行の時と倍ほど時間が違うだけです。何がどうなっているか正直わかりません。
最後に読み込みのSQLとして「インデックスの効いていないカラムにLIKE
検索でSQLを実行する時間」を見ます。
D1 | SQLite storage in Durable Object | |
---|---|---|
1回目 | 84 ms | 30 ms |
2回目 | 93 ms | 33 ms |
3回目 | 53 ms | 33 ms |
4回目 | 86 ms | 38 ms |
5回目 | 73 ms | 35 ms |
どちらも処理には少し時間が増えますが、それもでSQLite storage in Durable Objectの速度劣化は小さい方だというのがわかります。
最後に書き込みを見るために「単純にINSERT
を1回実行する時間」を見ます。
D1 | SQLite storage in Durable Object | |
---|---|---|
1回目 | 55 ms | 31 ms |
2回目 | 49 ms | 29 ms |
3回目 | 44 ms | 26 ms |
4回目 | 43 ms | 27 ms |
5回目 | 40 ms | 29 ms |
読み込みよりは若干処理時間は増えます。が、初期のデータを大量に入れるSQLの実行結果と比較するとSQLite storage in Durable Objectは何がどうなっているのかさっぱりわからない速度です。
このようにSQLite storage in Durable ObjectはD1に比べ格段に速度があがります。やはりD1に比べて色々なものを取っ払った生のSQLiteが出ているからなんでしょうか。
まとめ
確かにSQLite storage in Durable Objectは速度に不満のあったD1に比べれば断然に早いです。しかし、D1のようなマネージドのデータベースにほしい機能などがありません。自身で運用できるならばSQLite storage in Durable Objectを使うのもありですが、現実的には少し難しいかもしれません。
D1も機能はある程度ありますが、速度というはコールドスタートに若干の難があるため、これもこれでどこでも両手をあげて採用というのは厳しいです。
どちらもデメリットが飲める特定の用途に使用するというのが今のところの着地点かなとは思います。
Views: 0