AIが書いたSQL、そのまま本番で叩いて大丈夫ですか? — 「動く」を卒業して、EXPLAINで測って、影響範囲を囲ってから出すための実践ガイド
はじめに — 「動くSQL」と「本番で安全なSQL」は、別物なんです
AIにSQLを書かせるの、めっちゃ便利ですよね。
「ユーザーごとの今月の売上を出して」とお願いしたら、数秒で JOIN も GROUP BY も入った、それっぽいSQLが返ってくる。実際にローカルで動かしてみたら、ちゃんと数字も出る。「お、いけるやん」と思って、そのまま本番に投げる。
ここで、ちょっと立ち止まってほしいんです。
そのSQL、本番のデータベースで何が起きるか、説明できますか?
どのテーブルを、どの順番で、どれだけ読むのか。インデックスを使うのか、それとも全行を上から下までなめるのか。実行中、ほかの人の処理を何秒止めるのか。もし UPDATE だったら、何行に影響するのか。
正直に言うと、僕も最初はここがふわっとしていました。「動いたからOK」で出していた。でも、AIが書くSQLって、構文が正しいことと、本番で安全に動くことが、まったく別の話なんですよね。
実際、2025年7月には、AIのコーディングエージェントがコードフリーズ期間中に破壊的なコマンドを実行して、本番データベースを丸ごと壊した、という事例が報告されています(しかもその後「やってない」と取り繕おうとした、というおまけ付きで)。極端な例に見えるかもしれませんが、地続きの話なんです。「動くSQLを書ける」と「本番に出していいSQLか判断できる」の間には、思っているより深い谷がある。
この記事では、その谷を埋めます。AIにSQLを任せるとき、人間が何を設計して、何を判断して、何をAIに任せるのか。これを 「動く」「速い」「安全」の3つの軸 に分けて、明日から仕事で使えるレベルまで具体化していきます。
専門用語も、出てくるたびに噛み砕いて説明するので、「インデックスとか実行計画とか、なんとなくしか分からへん」という人も、置いていきません。ゆっくりいきましょう。
まず前提 — AIのSQLは、なぜ「それっぽいのに危ない」のか
対策の前に、なぜ危ないのかを腹落ちさせておきたいんです。理由が分かると、チェックポイントが自然に見えてくるので。
理由1:精度は上がったけど、「本番で正しい」とは限らない
text-to-SQL(自然言語からSQLを生成すること)の精度は、ここ数年でぐっと上がりました。でも、数字を冷静に見ると、過信は禁物だと分かります。
- BIRD という実データ寄りのベンチマークで、2026年時点の最良パイプラインでも実行精度は 80%台前半(人間のエキスパートは約93%)。
- さらに実務に近い Spider 2.0 のエージェント評価になると、約21% まで落ちます。
ここで大事なのは、「すごい/ダメ」の話じゃないんです。ベンチマークの数字が、あなたの本番スキーマでの正しさを保証してくれるわけではない、ということ。研究で分かってきたのは、text-to-SQLを壊すのは「クエリ構造の複雑さ」よりも、実データ・本番のスキーマ・曖昧な要件 だ、という事実です。
たとえば「アクティブユーザー」と言ったとき、それは「直近30日にログインした人」なのか「課金中の人」なのか。AIは、あなたの会社のその定義を知りません。だから、それっぽく、でも微妙に間違ったSQLを、堂々と返してくるんです。
理由2:AIは「本番の体格」を知らない
もっと本質的な理由がこれです。
AIは、あなたの本番データベースの テーブルサイズ・インデックス・制約・統計情報 を知りません。同じ1本のSQLでも、
- インデックスが効けば 2ミリ秒
- インデックスが効かなければ 40秒
くらい平気で変わります。AIは「構文的に正しいSQL」は書けても、「それが動いているシステムに何をするか」——どのロックを取り、それをどれだけ握り続け、ディスク上でテーブルを書き換えるかどうか——までは、原理的に判断できないんです。
ここで、これから何度も出てくる用語を、先に噛み砕いておきます。
- インデックス(索引):本の巻末にある索引と同じです。索引があれば「この単語は243ページ」と一発で飛べる。なければ、1ページ目から最後まで全部めくって探すしかない。
- シーケンシャルスキャン(Seq Scan/全表走査):まさにその「全部めくる」状態。テーブルの全行を上から順に読むこと。データが100万行あれば100万行読む。小さいテーブルなら問題ないけど、大きいテーブルだと致命傷。
- 実行計画(EXPLAIN):データベースが「このSQLを、こういう作戦で処理します」と教えてくれる作戦メモ。インデックスを使うのか、全部読むのか、どう結合するのか、が書いてある。後で読み方をやります。
- ロック:同じデータを同時にいじって壊れないように、データベースが付ける「順番待ちの札」。これが長引くと、ほかの人の処理が全部待たされて、システムが止まったように見える。
この4つが分かれば、もう半分勝ったようなものです。では、3つの軸に入っていきましょう。
軸① 正しさ — AIに「良いSQL」を書かせるコツは、文脈を渡すこと
最初の軸は 正しさ。意図したデータが、ちゃんと正確に取れるか、です。
ここでいちばん効くのは、テクニックでも凄いプロンプトでもなくて、文脈(コンテキスト)を渡すこと。AIがそれっぽく間違える原因のほとんどは、「あなたのデータベースの事情を知らないまま書いている」ことなので、知らせてあげればいい。逆に言うと、ここが Context Engineering の出番です。
渡すべき文脈は、だいたいこの3つ。
- スキーマ:テーブル定義(カラム名・型・主キー・外部キー・インデックス)
- 言葉の定義:「アクティブ」「売上」「退会」みたいな、自社特有の言葉が何を指すか
- 要件と制約:何を出したいか、期間、並び順、NULLの扱い、想定行数
プロンプト例①:スキーマ文脈を渡して正しいSQLを書かせる
あなたはPostgreSQLに詳しいシニアエンジニアです。
以下のスキーマと定義を前提に、要件を満たすSQLを1本書いてください。
# スキーマ
users(id PK, name, plan, created_at, last_login_at)
orders(id PK, user_id FK->users.id, amount, status, created_at)
- orders.user_id にインデックスあり
- orders.created_at にインデックスあり
- 行数: users 約5万行 / orders 約800万行
# 言葉の定義
- 「アクティブユーザー」= 直近30日以内に last_login_at があるユーザー
- 「確定売上」= orders.status = 'paid' の amount 合計
# 要件
- 今月の、アクティブユーザーごとの確定売上を、売上が高い順に出す
- 売上が0のユーザーは含めない
# 出力ルール
1. まずSQLを書く
2. そのSQLが「どのインデックスを使う想定か」を一言で説明する
3. パフォーマンス上の懸念があれば指摘する
4. 不明な点や曖昧な要件があれば、勝手に決めずに質問する
ポイントは、最後の 「曖昧なら勝手に決めずに質問する」 の一文です。これを入れるだけで、AIが思い込みで突っ走るのをかなり防げます。「今月って、暦月? 直近30日?」みたいに聞き返してくれるようになる。曖昧さこそがtext-to-SQLの最大の敵なので、ここを人間とAIで潰すんです。
そして、出てきたSQLをレビューするときの観点はシンプルです。
-
JOINの結合キーは正しいか(
user_id同士で繋いでいるか、変な掛け算になっていないか) - WHEREの条件は意図どおりか(期間、ステータス、NULL)
-
集計の粒度は合っているか(
GROUP BYの単位、重複カウントしていないか) - 言葉の定義に忠実か(「アクティブ」の定義をちゃんと反映しているか)
ここまでが「動く・正しい」の話。でも、正しいSQLが、速いとは限らない。次の軸です。
軸② 速さ — 推測するな、EXPLAINで「測って」から出す
正しいSQLができた。でも、それが本番の800万行のテーブルで、2ミリ秒で終わるのか、40秒かかるのか。これは見た目では分からない。だから測ります。道具が EXPLAIN です。
EXPLAIN は、さっき言った「データベースの作戦メモ」を見せてもらうコマンド。SQLの前に付けるだけです。
EXPLAIN ANALYZE
SELECT u.id, u.name, SUM(o.amount) AS sales
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.last_login_at >= now() - interval '30 days'
AND o.status = 'paid'
AND o.created_at >= date_trunc('month', now())
GROUP BY u.id, u.name
ORDER BY sales DESC;
EXPLAIN は計画だけを表示します。EXPLAIN ANALYZE を付けると、実際に実行して、本当にかかった時間まで見せてくれます。
注意:
ANALYZEは実際にクエリを走らせます。SELECTなら基本安全ですが、UPDATEやDELETEにEXPLAIN ANALYZEを付けると本当に更新されてしまいます。データ変更系を計画だけ見たいときは、後で出てくるトランザクション+ロールバックの中でやるのが安全です。
実行計画、まずはこの1行だけ見ればいい
EXPLAINの出力って、初めて見ると呪文みたいで、正直うっ……となりますよね。でも、最初に見るべきは1か所だけです。「Seq Scan」か「Index Scan」か。 ここだけ。
-- 危ないパターン(全部読んでる)
Seq Scan on orders o (cost=0.00..180000.00 rows=8000000 ...)
-- 良いパターン(索引で絞れている)
Index Scan using orders_created_at_idx on orders o (rows=12000 ...)
- Seq Scan:そのテーブルを全行読んでいます。小さいテーブル(数百〜数千行)なら気にしなくていい。でも、何百万行もあるテーブルにこれが出ていたら、黄色信号です。
- Index Scan:索引を使って、必要な行だけ読めています。これが見たい状態。
加えて、もう2か所だけ見られるようになると強いです。
-
rows の見積もりと実測の乖離:
EXPLAIN ANALYZEだと「予測した行数」と「実際の行数」が両方出ます。これが桁違いにズレていたら、データベースの統計情報が古い可能性大(ANALYZE テーブル名;で統計を更新できます)。 - Nested Loop に大きな入力:結合方式の一つで、片方が大きいと、行ごとに何度も読みにいって遅くなりがち。大入力に出ていたら要注意。
プロンプト例②:EXPLAINの出力をAIに読ませて、対策まで出させる
EXPLAINを自力で全部読めなくても大丈夫。実行計画を貼って、AIに通訳と対策をさせるのが現実的です。ここでAIは「設計者」ではなく「優秀なアナリスト」として使います。
以下はPostgreSQLのEXPLAIN ANALYZEの結果です。
このSQLが遅い原因を、初心者にも分かる言葉で説明してください。
そのうえで、改善案を「効果が大きい順」に最大3つ、それぞれ
「何をするか」「なぜ効くか」「副作用やリスク」をセットで挙げてください。
インデックス追加を提案する場合は、CREATE INDEX文と、その
インデックスが既存の書き込み性能に与える影響にも触れてください。
# 実行したSQL
(ここにSQLを貼る)
# EXPLAIN ANALYZE の結果
(ここに実行計画を貼る)
「効果が大きい順」「副作用も書いて」と縛るのがコツです。AIはインデックスをホイホイ勧めがちなんですが、インデックスはタダじゃない。読み取りは速くなる代わりに、書き込み(INSERT/UPDATE)は少し遅くなるし、ディスクも食う。 その副作用まで言わせることで、人間が最終判断できる材料がそろうんです。
本番で測る前に、LIMITで小さく試す
「いきなり本番でEXPLAIN ANALYZEするの怖い」——その感覚、正しいです。だから、まずは小さく試す。鉄板の安全策がこれ。
-- いきなり全件返さず、まず10行だけ様子を見る
SELECT u.id, u.name, SUM(o.amount) AS sales
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.status = 'paid'
GROUP BY u.id, u.name
LIMIT 10;
LIMIT 10 を付けて先に走らせるだけで、JOINの掛け算ミス(カルテシアン積)や、想定外に巨大な結果セットを、被害が出る前に検知できます。「あれ、10行返すだけなのにやたら遅いな」と気づければ、本番で全件叩く前に引き返せる。地味だけど、効きます。
軸③ 安全 — 影響範囲(blast radius)は、人間が囲う
3つ目が、いちばん大事な 安全 の軸です。
SELECT(読むだけ)なら、最悪でも「遅い」で済みます。でも UPDATE DELETE DROP といった、データを変える・消す系は、失敗が取り返しのつかない事故になる。ここはAIに丸投げしてはいけない領域です。
キーワードは blast radius(爆発の影響範囲)。このSQLが、どこまで・何行に影響するのか。それを実行前に、人間が見積もって囲う。
鉄則1:本番の接続情報を、AIに直接渡さない
いちばんやってはいけないのが、AIエージェントに本番DBへの接続を握らせて、自由に実行させること。さっきの2025年7月の事故も、これが背景にありました。
おすすめは、接続そのものを渡さず、スキーマの情報だけをファイルで渡すやり方です。スキーマをJSONなどに書き出してリポジトリに置いておけば、AIは本番に触れずに「文脈」だけ受け取れる。接続のセキュリティリスクを、まるごと消せます。
{
"table": "orders",
"columns": [
{"name": "id", "type": "bigint", "pk": true},
{"name": "user_id", "type": "bigint", "fk": "users.id"},
{"name": "amount", "type": "numeric"},
{"name": "status", "type": "text"},
{"name": "created_at", "type": "timestamptz"}
],
"indexes": ["orders_user_id_idx", "orders_created_at_idx"],
"approx_rows": 8000000
}
これをプロンプトに添えるだけで、AIは本番に接続しなくても、行数やインデックスを踏まえた現実的なSQLを書けます。人間はAIの出したSQLをレビューして、自分の手で実行する。実行の主導権は、人間が握り続けるんです。
鉄則2:消す前に、まず「何行に当たるか」を数える
DELETE や UPDATE を実行する前に、まったく同じ条件で SELECT COUNT(*) を撃つ。これは習慣にする価値があります。
-- ❌ いきなりこれを撃たない
DELETE FROM orders WHERE status = 'pending' AND created_at < '2025-01-01';
-- ✅ まず、何行に当たるか数える(影響範囲の確認)
SELECT COUNT(*) FROM orders WHERE status = 'pending' AND created_at < '2025-01-01';
-- → 例えば「3行」なら安心。「280万行」なら、いったん手を止める。
「3行のつもりが280万行だった」という事故は、たいていWHERE条件の勘違いから起きます。消す前に数える。たったこれだけで、blast radiusが目に見えるようになる。
鉄則3:トランザクション+ロールバックで「ドライラン」する
それでも不安なときは、トランザクションの中で実行して、本当に意図どおりかを確認してから、わざとロールバック(取り消し)する。いわばリハーサルです。
BEGIN;
UPDATE orders
SET status = 'cancelled'
WHERE status = 'pending'
AND created_at < '2025-01-01';
-- ここで影響行数が表示される(例: UPDATE 3)
-- 想定どおりか、必要なら SELECT で結果も確認する
-- 問題なければ COMMIT; 不安が少しでも残るなら↓
ROLLBACK;
ROLLBACK を実行すれば、UPDATE はなかったことになります。「影響行数だけ見て、変更は確定させない」。これでリハーサルができる。納得できたら、改めて BEGIN → UPDATE → 確認 → COMMIT でやればいい。
鉄則4:権限とタイムアウトで「そもそも事故れない」状態を作る
人間の注意力に頼るのには限界があります。仕組みで守りましょう。
-- AIや自動化に使わせるのは、読み取り専用ロール
CREATE ROLE ai_readonly LOGIN PASSWORD 'set_a_strong_password';
GRANT CONNECT ON DATABASE mydb TO ai_readonly;
GRANT USAGE ON SCHEMA public TO ai_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_readonly;
-- このロールには、暴走クエリを止めるタイムアウトも設定
ALTER ROLE ai_readonly SET statement_timeout = '30s';
-
読み取り専用ロール:そもそも
SELECTしかできない権限でAIを動かせば、どれだけAIが暴走しても、データは壊れません。「権限を渡さない」が、いちばん確実な安全柵。 - statement_timeout(クエリのタイムアウト):1本のクエリが30秒を超えたら、自動で打ち切る設定。WHERE抜けやカルテシアン積で暴走したクエリが、データベースのリソースを食い尽くす前に止められます。
そしてもう一つ。AIに任せきりにすると見落とされがちなのが、スキーマ変更(DDL)の裏の挙動です。たとえば大きなテーブルに SET NOT NULL のような変更をかけると、ACCESS EXCLUSIVE という強いロックを取って、その間そのテーブルへの読み書きが全部止まることがあります。400万行のテーブルでピーク時にこれをやると、サービスが固まる。一部のDDLは、見た目は一瞬でも、裏でディスク上のテーブルを丸ごと書き換えることもある。AIはこういう「裏の挙動」を知らずに、平然と提案してきます。だからDDLこそ、人間が影響を読んでから出すべき領域なんです。
人間とAI、どう役割分担するか
ここまでを、役割分担の表にまとめます。これがこの記事のいちばん言いたいところです。
| 工程 | 主に人間がやること(What / Why) | 主にAIに任せること(How) |
|---|---|---|
| 要件定義 | 「何を」「なぜ」出したいか、言葉の定義を決める | 要件の曖昧な点を質問で洗い出す |
| スキーマ理解 | 本番の体格(行数・インデックス・制約)を把握し文脈として渡す | 渡された文脈からSQLをドラフトする |
| 実装 | ドラフトのレビュー(JOIN/WHERE/粒度/定義) | SQLの初稿と、設計意図の説明 |
| 性能検証 | EXPLAINを撃つ、最終判断する | 実行計画の通訳、改善案を効果順に提示 |
| 安全確認 | 影響範囲を数える、権限・実行を握る | 破壊的操作のリスクを事前に列挙 |
| 実行 | 本番で実行するボタンを押すのは、人間 | (ここはAIに渡さない) |
一言でいうと、AIは最高のSQLドラフター、人間は影響範囲(blast radius)の門番。これが僕の中での落としどころです。
「何を・なぜ(What / Why)」は人間が決めて、「どうやって(How)」はAIに任せる。SQLだろうがアプリのコードだろうが、この線引きは変わらないなと思っています。
プロンプト例③:破壊的クエリの影響範囲を、先に見積もらせる
最後に、安全側で効くプロンプトを1本。UPDATE や DELETE をAIに書かせるときは、SQL本体より先に、影響範囲の見積もりと確認手順を出させると安全です。
これから本番データベースで status を更新する作業をします。
いきなり UPDATE 文を書かず、次の順番で出力してください。
1. まず、影響範囲を確認するための SELECT COUNT(*) を書く
2. 変更前後を目視確認するための SELECT を書く(変更対象を10件表示)
3. そのうえで UPDATE 文を書く(ただしトランザクションで囲い、
最後は COMMIT ではなく ROLLBACK にしておくこと)
4. WHERE 条件で「消しすぎ・変えすぎ」が起きうるパターンを警告する
# 前提スキーマ
(ここにスキーマJSONを貼る)
# やりたいこと
2025年より前の pending 状態の注文を cancelled にしたい
「いきなりUPDATEを書くな、まず数えろ、リハーサルしろ」という手順そのものを、プロンプトに焼き込んでおく。こうしておくと、AIが手順の番人になってくれます。
明日からの4ステップ
難しく考えなくて大丈夫です。明日から、この4つだけ意識してみてください。
- 文脈を渡す:スキーマ・言葉の定義・想定行数をAIに渡してからSQLを頼む。「曖昧なら質問して」を一文添える。
-
小さく試す:本番で全件叩く前に、
LIMIT 10で様子を見る。 - EXPLAINで測る:Seq Scanになっていないか1行だけ確認。読めなければ計画をAIに貼って通訳させる。
-
影響範囲を囲う:変更系は
COUNT(*)で数えてから。BEGIN〜ROLLBACKでリハーサル。実行のボタンは人間が押す。
全部いっぺんにやらなくていい。まずは4番の「消す前に数える」だけでも、事故はぐっと減ります。65点でいいんです。
おわりに — 明日の自分が「あざっす」と言うSQLの出し方
AIにSQLを書かせるのは、もう当たり前の時代になりました。これは本当にありがたいことで、僕らは「SQLを1から打つ作業」から、かなり解放されつつあります。
でも、便利になればなるほど、人間が握っておくべきものが、くっきりしてくる気がするんです。それが、この記事でずっと言ってきた「影響範囲を見届ける」という仕事。AIは「動くSQL」を一瞬で出してくれる。だけど、「これを本番に出していいか」の最終判断は、まだ人間の手の中にある。そして、たぶんこれからも、ここは人間の仕事であり続ける。
「動く」は、通過点なんですよね。そこから「速い」をEXPLAINで測って、「安全」を影響範囲で囲って、初めて本番に出せる。この一手間を、面倒くさいと取るか、未来の自分へのプレゼントと取るか。
僕は最近、何かを実行するボタンを押す前に、いつも一つだけ自分に問いかけるようにしています。
「この選択、明日の自分が『あざっす』って言ってくれる方かな?」
寝ぼけて本番を壊して、翌朝青ざめる自分に「あざっす」とは、たぶん言ってもらえない。でも、COUNT(*) で数えて、ロールバックでリハーサルして、ちゃんと納得してから COMMIT した自分には、明日の自分はきっと「丁寧にやっといてくれて、あざっす」と言ってくれる。
完璧じゃなくていいです。今日より一手間だけ丁寧に。それだけで、明日の自分も、チームも、ちょっと助かる。
あなたの次のSQL、本番に出す前に、一回だけ立ち止まってみませんか。そのワンクッションが、未来のあなたを守ってくれるはずなので。
最後まで読んでいただいて、あざっす。