2026年09月18日 11:00 / - PV

「オンラインで完結する DB を比較したい」と思って調べ始めたのですが、料金と無料枠を並べただけの表なら公式ページを見れば済みます。そこで、Claude Code on the web(claude.ai/code)のサンドボックスから実際に5つの DB へ繋いでもらい、何が起きたかを記録するという形にしました。
Claude Code web を選んだのは、手元の環境を一切汚さずに済むからです。ブラウザからリポジトリを指定すると Anthropic 側の VM が立ち上がり、そこで作業が進みます。結果は git に push されるので、後から読み返せます。
対象はこの5つです。
PlanetScale は 2024 年 4 月に無料枠が消えて最安でも月 5 ドルかかるので、今回は外しました。Xata も無料枠を廃止して 14 日間のクレジットだけになっているので外しています。
結果から言うと、DB の性能や設定ではなく、サンドボックスのネットワークが全部を決めました。
| サービス | TCP 直結 | HTTP ドライバ | テーブル作成→INSERT→SELECT | SELECT 5回の中央値 |
|---|---|---|---|---|
| Supabase | × | ○(到達のみ) | × DDL を打てない | 513ms(往復のみ) |
| Neon | × | ○ | ○ 唯一完走 | 62ms |
| Turso | ×(WebSocket) | ○ | ○ | 184ms |
| Prisma Postgres | × | -(経路が無い) | × | 全滅 |
| Cloudflare D1 | -(経路が無い) | 未検証 | 未検証 | - |
サンドボックスの正体は次の3点で、5つの結果はすべてこれで説明がつきます。
/proc/net/if_inet6 が存在せず、IPv6 アドレスへ connect すると EAFNOSUPPORT で蹴られるwss:// は HTTP 400 で返るつまり「HTTPS(443) の上に載っている DB だけが使える」という一言に集約されます。Neon と Turso が動いたのは HTTP ドライバを持っていたからで、Prisma Postgres が全滅したのは TCP しか無いからです。
実験用に小さなリポジトリを1つ作り、CLAUDE.md に手順を書いておきました。Claude Code web はセッション開始時にリポジトリの CLAUDE.md を読むので、指示はここに集約できます。
## サービスごとに同じ手順
1. 環境変数が届いているか確認する。値は表示しない
2. TCP 直結を試す。Postgres 系は psql と pg、SQLite 系は該当プロトコル
3. HTTP ベースのドライバに切り替えて試す
4. items(id, body, created_at) テーブルを作り、3 行 INSERT して SELECT する
5. 接続確立から SELECT 完了までを 5 回計測する
6. RESULT.md の冒頭に「結論:TCP ○/×、HTTP ○/×、最短〜最長 ms」を書く
## やらないこと
- 秘密情報を echo しない、ファイルに書かない、コミットしない
- 失敗を隠すために手順を飛ばさない接続文字列は Claude Code web のクラウド環境に入れます。claude.ai/code の入力欄の左にある環境名をクリックすると「クラウド環境を追加」があり、そこで次を設定しました。
環境には「セットアップスクリプト」欄があり、npm install と書いておきました。すると初回セッションがこれで落ちました。
Setup script failed with exit code 254.
npm error code ENOENT
npm error path /home/user/package.json
npm error enoent Could not read package.json
セットアップスクリプトの作業ディレクトリは /home/user で、クローンされたリポジトリの中ではありません。 cd を自分で書くか、そもそもスクリプトを空にしてプロンプト側で「まず npm install を実行して」と言うほうが確実です。今回は後者にしました。
サンドボックスで失敗したとき「鍵が悪いのか、環境が悪いのか」を切り分けるため、同じ接続文字列で手元の Mac から先に繋いでおきました。
| 接続 | 手元の Mac | サンドボックス |
|---|---|---|
| Supabase 直結(IPv6) | OK 65ms | 到達不能(IPv6 なし) |
| Supabase Session pooler(IPv4) | OK 153ms | 到達不能(5432 閉) |
| Neon TCP | OK 1379ms(初回) | 到達不能(5432 閉) |
| Neon HTTP | OK 589ms | OK 53〜261ms |
| Turso HTTP | OK 211ms | OK 158〜643ms |
| Prisma direct | OK 174ms | 到達不能(5432 閉) |
| Prisma pooled | OK 186ms | 到達不能(5432 閉) |
手元では全部成功しているので、失敗はすべてサンドボックス側の制約だと言い切れます。この一手間が無いと、あとで「Supabase の設定が悪いのでは」と延々疑うことになります。
いちばん時間を溶かしたのがここです。同梱の psql(16.13)で直結すると、こうなります。
$ psql "$SUPABASE_DB_URL" -c 'select version();'
psql: error:
psql: error: の後ろに何も無い。62ms で返っているのでネットワーク待ちもしていません。原因が分からないので、Node の生ソケットで同じアドレスに繋いでエラーコードを取りに行きました。
$ node -e '...net.connect(5432, "2406:da14:311:...")...'
ERROR code=EAFNOSUPPORT errno=-97 syscall=connect
EAFNOSUPPORT(Address family not supported)。Supabase の直結ホスト db.xxxx.supabase.co は AAAA レコードしか持たない IPv6 専用で、サンドボックスには IPv6 スタックがありません。libpq は候補アドレスを1つも作れず、strerror に載せる errno も持たないまま抜けたので、エラー文が空になったようです。
psql が黙ったら、Node の net.connect にエラーコードを吐かせる。 これが今回いちばんの教訓でした。
IPv4 で繋がる Session pooler(aws-0-ap-northeast-1.pooler.supabase.com)に切り替えると、今度は別の理由で落ちます。
$ psql "$SUPABASE_DB_POOLER_URL" -c 'select version();'
psql: error: connection to server at "aws-0-ap-northeast-1.pooler.supabase.com" (54.64.190.72), port 5432 failed: timeout expired
3アドレスとも 20 秒のタイムアウトを使い切っています。同じ IP の 443 は 2ms で開くので、5432 だけがポート単位で落とされている。直結は IPv6 が無いため、pooler はポートが塞がれているため。理由が別々なのが厄介でした。
HTTP 経路(PostgREST)は届きます。
$ node -e '...createClient(SUPABASE_URL, SUPABASE_ANON_KEY).from("items").select()...'
error={"code":"PGRST205","message":"Could not find the table 'public.items' in the schema cache"}
PGRST205 は PostgREST が返したアプリケーションエラーで、リクエストが Supabase まで届き、認証を通り、スキーマを引いた上で「そんなテーブルは無い」と答えています。経路は生きている。
ただし、そこから先に進めません。PostgREST は DDL を実行できず、anon key で DDL を代行する RPC も置かれていません。DDL を打てる TCP は塞がれ、開いている HTTP には DDL の権限が無い。 この噛み合わせで詰みました。
回避策は「テーブルは手元から作る」です。手元の Mac から CREATE TABLE して 3 行入れたのですが、直後に anon key で読むとまだ 404 のままでした。
$ psql "$SUPABASE_DB_URL" -c "NOTIFY pgrst, 'reload schema'"
PostgREST はスキーマをキャッシュしているので、SQL で作ったテーブルは NOTIFY pgrst, 'reload schema' を打つまで REST から見えません。 これを打った数秒後に 200 で 3 行返るようになりました。細かい話ですが、知らないと「作ったのに無いと言われる」で止まります。
Neon は5つの中で唯一、テーブル作成から SELECT まで完走しました。
TCP はほかと同じく 5432 で無応答です。
$ psql "$NEON_DATABASE_URL" -c 'select version();'
psql: error: connection to server at "ep-xxxx-pooler.us-east-2.aws.neon.tech" (3.143.47.40), port 5432 failed: timeout expired
ここで @neondatabase/serverless に切り替えます。接続文字列は TCP のときと同じものをそのまま渡します。
import { neon } from '@neondatabase/serverless'
const sql = neon(process.env.NEON_DATABASE_URL)
await sql`select version()`
// OK 1107ms :: PostgreSQL 18.6 on aarch64-unknown-linux-gnu
同じ文字列、同じホスト、同じ資格情報で、TCP はタイムアウト、HTTP は 1.1 秒で成功。neon() は 5432 ではなく HTTPS の SQL-over-HTTP エンドポイントに投げるので、443 しか開いていない環境でも通ります。DDL もそのまま通りました。
CREATE TABLE ok 499ms
INSERT ok 46ms
SELECT ok 91ms
5回計測の中央値は 62ms。リージョンは us-east-2(オハイオ)で、Neon に東京リージョンは今もありません(アジアはシンガポールとシドニーのみ)。それでも一番速かったのは、HTTP 経路が軽いのと、初回以外はスケールゼロからの起動を挟まないためです。手元からの初回 TCP が 1.4 秒かかっているのは、その起動が乗っています。
Neon の始め方は以前まとめています。
参考記事:
Turso は Postgres ではないので、TCP 直結に相当するのは Hrana over WebSocket(wss://)です。まず環境変数の libsql:// の URL をそのまま渡すと、あっさり通りました。
OK 987ms :: sqlite 3.47.0
ここで「Turso は素通し」と書きかけたのですが、libsql:// が実際にどのトランスポートを使ったのかを確認していません。スキームを明示して分けました。
https:// 明示 OK 922ms
libsql:// 既定 OK 164ms
wss:// 明示 NG HRANA_WEBSOCKET_ERROR: Unexpected server response: 400
さらに createClient(...).constructor.name を見ると、libsql:// は HttpClient を返していました。最初に通ったのは初めから HTTP 経路で、WebSocket は試せていなかった。 wss:// を明示すると 400 です。TLS ハンドシェイク後に HTTP ステータスが返っているので、ポートが閉じているのではなく、手前のプロキシで WebSocket の Upgrade が落ちていると読めます。
HTTP 経路ではテーブル作成まで完走しました。1回目は自分のミスで落ちています。
create table items (... created_at text not null default (datetime("now")))
SQLITE_UNKNOWN: default value of column [created_at] is not constant
SQLite は二重引用符を識別子として解釈するので、"now" がカラム名扱いになって DEFAULT が定数でないと拒否されました。シングルクォートに直して通過。5回計測の中央値は 184ms、リージョンは東京です。近いのに Neon より遅いのは、SQLite の HTTP 経路が1リクエストごとに重いためのようで、ここは深追いしていません。
Prisma Postgres が配る接続文字列は、pooled も direct も TCP の postgres:// だけです。
$ psql "$PRISMA_DIRECT_URL" -c 'select version();'
psql: error: connection to server at "db.prisma.io" (66.135.15.127), port 5432 failed: timeout expired
同じ IP の 443 は 1ms で開くので、ポート制限であることは確定です。Accelerate 経由の HTTP 接続(prisma+postgres://)は、単体の Accelerate が 2026 年 12 月 1 日に終了予定で、新しいコンソールでは配られませんでした。
TCP が唯一の経路で、それが塞がれた時点で打つ手が無い。 5サービス中、ここだけです。DB として悪いわけではなく、この環境との相性の問題です。
D1 は Workers バインディングと REST API しか無いので、もともと HTTP 専用です。相性は一番よいはずですが、今回はアカウント ID とトークンを用意できず、手順1で止まりました。api.cloudflare.com 自体には届いていて、認証ヘッダなしで叩くと 400 が返ります。経路は生きているので、鍵があれば通る見込みが高いのですが、叩いた記録は無いので○とは書きません。
net.connect で EAFNOSUPPORT を見るまで分からなかったlibsql:// は黙って HTTP に落ちる。試したつもりの経路を試せていないことがあるNOTIFY pgrst, 'reload schema' を忘れないnpm install だけ書くと ENOENT で落ちる料金は変わるので、使う前に公式で確かめてください。今回の実験と関係する範囲だけ書きます。
SQLite 系をサーバレスで使う話は、こちらにも追記しています。
参考記事:
TCP の 5432 では繋げません。外向きに開いているのは 443 だけなので、Neon の @neondatabase/serverless のような HTTP 経由のドライバを持つサービスに限られます。Supabase なら PostgREST、Turso なら HTTP モードが該当します。
今回の結果はすべて Full での話です。Full にしても、通るのは HTTPS プロキシを経由する 443 だけで、任意ポートの TCP と WebSocket は通りませんでした。ドメインの制限が外れるだけで、プロトコルの制限は残ります。
同じ IP の 443 に net.connect してみてください。443 が数 ms で開いて目的のポートが無応答なら、ポート制限です。名前解決が ENOTFOUND で、Node の dns.resolve6 では引けるなら、IPv6 の問題です。エラーが HTTP ステータスで返っているなら、ネットワークではなく認証か権限の問題です。
この環境に限れば Neon です。接続文字列を1本持っておけば、TCP でも HTTP でも同じ文字列で繋がるので、環境の制約に振り回されません。東京リージョンが要るなら Turso か Supabase になりますが、Supabase は anon key だけでは DDL が打てない点を先に押さえておく必要があります。
NOTIFY pgrst, 'reload schema' を打つ失敗したコマンドと出力は、すべて実験リポジトリの RESULT.md にそのまま残してあります。1週間放置したあとに同じ手順をもう一度流して、Supabase の一時停止と D1 の日次上限がどう出るかは追記する予定です。
この記事が何かのお役に立てれば幸いです。
最後までお読みいただきありがとうございました!
Share Me!