- AI ⚙️
生成AIにSQLを書かせるプロンプト|テーブル定義の渡し方とテンプレート
生成AIにSQLを書かせるときは、「CREATE TABLE 文・列の意味の表・サンプル行・結合キー」の4点セットでテーブル定義を渡し、DBの種類と版、ほしい結果の形を添えるのが基本です。 列名を文章で説明するだけでは、型・NULL の可否・値の種類・テーブル同士のつながりが伝わらず、AI は足りない部分を推測で埋めることになります。
この記事では、4点セットのテンプレートと、定義を取り出すコマンドを PostgreSQL・MySQL・SQLite の3種類ぶん載せました。用途別(新規作成・既存 SQL の修正・遅い SQL の見直し・エラー文からの修正)のプロンプトと方言の指定文を一覧にし、後半では受け取った SQL を架空データ入りの SQLite で動かし、件数と集計値で確かめる手順を、コマンドと出力つきで示します。
確認環境:Ubuntu 24.04、PostgreSQL 16.15、MySQL 8.0.46、SQLite 3.45.1。掲載したコマンドと SQL は、この環境で実行した出力を添えています。プロンプトは特定の AI 製品に依存しない書き方で、AI の出力例は載せていません。確かめる対象の SQL は、この記事で用意した例です。
SQLを書かせるプロンプトの早見表
| 渡すもの | 中身 | 取り出し方 | 省くとどうなるか |
|---|---|---|---|
| DBの種類と版 | 例:PostgreSQL 16、MySQL 8.0、SQLite 3.45 | SELECT version(); など |
別のDBの関数(日付の整形など)が混ざる |
| CREATE TABLE 文 | 列名・型・NULL の可否・制約・索引 | pg_dump --schema-only、SHOW CREATE TABLE、.schema |
存在しない列名や、型に合わない比較が出る |
| 列の意味の表 | 列ごとの意味、取りうる値、単位 | 自分で書く(設計書から写す) | status の値や「取り消しを数えるか」を推測される |
| サンプル行 | 1テーブルにつき3〜5行(架空の値) | LIMIT 3 の結果を架空の値に置き換える |
日時の書式や文字の表記ゆれを取り違える |
| 結合キー | どの列とどの列でつながるか、1対多の向き | 外部キーの定義、または自分で1行ずつ書く | 結合条件の取り違えや、行の重複が起きる |
| ほしい結果 | 列の並び、並び順、0件の扱い、期間の境目 | 自分で書く | 0件の行が消える、月末の扱いがずれる |
| 出力形式 | SQL を1つ、前提にした解釈を箇条書き、確かめ方 | 自分で書く | 説明が長くなり、SQL を探しにくい |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「生成AIにSQLを書かせるプロンプト|テーブル定義の渡し方とテンプレート」
テーブル定義は4点セットで渡す
題材は、架空の「社内の備品予約システム」です。社員(members)、備品(items)、予約(reservations)の3テーブルがあります。
1. CREATE TABLE 文
型・制約・索引は、文章で書き直すより CREATE TABLE 文をそのまま貼るほうが正確で、手間もかかりません。
CREATE TABLE members (
id integer PRIMARY KEY,
name varchar(50) NOT NULL,
dept varchar(30) NOT NULL,
joined_on date NOT NULL
);
CREATE TABLE items (
id integer PRIMARY KEY,
name varchar(50) NOT NULL,
category varchar(20) NOT NULL,
status varchar(10) NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'retired'))
);
CREATE TABLE reservations (
id integer PRIMARY KEY,
member_id integer NOT NULL REFERENCES members (id),
item_id integer NOT NULL REFERENCES items (id),
start_at timestamp NOT NULL,
end_at timestamp NOT NULL,
status varchar(10) NOT NULL CHECK (status IN ('booked', 'returned', 'canceled')),
CHECK (end_at > start_at)
);
CREATE INDEX idx_reservations_item ON reservations (item_id);
2. 列の意味の表
CREATE TABLE 文からは読み取れない「値の意味」と「数え方の決まり」を表にします。AI が推測しがちな列だけで十分です。
| テーブル | 列 | 意味 | 取りうる値・注意 |
|---|---|---|---|
| items | status | 備品の状態 | active=貸出対象 / retired=廃棄済み(集計から外す) |
| reservations | start_at | 利用開始日時 | 日本時間。月の集計はこの列で判定する |
| reservations | end_at | 利用終了日時 | 日をまたぐことがある |
| reservations | status | 予約の状態 | booked=予約中 / returned=返却済み / canceled=取り消し(利用回数に数えない) |
3. サンプル行
日時の書式や値の表記を伝えるために、数行だけ添えます。本物のデータは貼らず、架空の値に置き換えます。
reservations(3行)
id | member_id | item_id | start_at | end_at | status
----+-----------+---------+---------------------+---------------------+----------
1 | 1 | 1 | 2026-09-01 10:00:00 | 2026-09-01 12:00:00 | returned
2 | 1 | 3 | 2026-09-03 09:00:00 | 2026-09-03 18:00:00 | returned
3 | 2 | 1 | 2026-09-10 13:00:00 | 2026-09-10 15:00:00 | returned
4. 結合キー
外部キーが定義に入っていても、つながりと向きを1行ずつ書くと確実です。
- reservations.member_id = members.id(社員1人に予約が0件以上)
- reservations.item_id = items.id(備品1つに予約が0件以上)
結合キーを別に書く理由は、定義の出力に外部キーが現れないことがあるためです。たとえば上の CREATE TABLE 文を MySQL 8.0.46 でそのまま実行して SHOW CREATE TABLE を見ると、列の横に書いた REFERENCES は外部キーとして残りませんでした。
Create Table: CREATE TABLE `reservations` (
`id` int NOT NULL,
`member_id` int NOT NULL,
`item_id` int NOT NULL,
`start_at` datetime NOT NULL,
`end_at` datetime NOT NULL,
`status` varchar(10) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_reservations_item` (`item_id`),
CONSTRAINT `reservations_chk_1` CHECK ((`status` in (_utf8mb4'booked',_utf8mb4'returned',_utf8mb4'canceled'))),
CONSTRAINT `reservations_chk_2` CHECK ((`end_at` > `start_at`))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
FOREIGN KEY (member_id) REFERENCES members (id) の形で追加すると、次のように出力に現れます。
CONSTRAINT `fk_res_item` FOREIGN KEY (`item_id`) REFERENCES `items` (`id`),
CONSTRAINT `fk_res_member` FOREIGN KEY (`member_id`) REFERENCES `members` (`id`),
外部キーを張っていないテーブルもよくあるので、結合キーは定義とは別に書くと決めておくと漏れません。
テーブル定義を取り出すコマンド(DB別)
| DB | CREATE TABLE 文を取り出す | 列の一覧を見る | サンプル行を取る |
|---|---|---|---|
| PostgreSQL | pg_dump --schema-only --no-owner --no-privileges -t テーブル名 DB名 |
psql で \d テーブル名 |
SELECT * FROM テーブル名 ORDER BY id LIMIT 3; |
| MySQL | SHOW CREATE TABLE テーブル名\G、または mysqldump --no-data DB名 テーブル名 |
SHOW CREATE TABLE の出力に含まれる |
同上 |
| SQLite | sqlite3 ファイル名 ".schema テーブル名" |
.schema の出力に含まれる |
同上(.mode insert テーブル名 で INSERT 文の形にもできる) |
PostgreSQL:pg_dump --schema-only
pg_dump --schema-only --no-owner --no-privileges -t reservations t362
CREATE TABLE public.reservations (
id integer NOT NULL,
member_id integer NOT NULL,
item_id integer NOT NULL,
start_at timestamp without time zone NOT NULL,
end_at timestamp without time zone NOT NULL,
status character varying(10) NOT NULL,
CONSTRAINT reservations_check CHECK ((end_at > start_at)),
CONSTRAINT reservations_status_check CHECK (((status)::text = ANY ((ARRAY['booked'::character varying, 'returned'::character varying, 'canceled'::character varying])::text[])))
);
…(省略)…
ALTER TABLE ONLY public.reservations
ADD CONSTRAINT reservations_pkey PRIMARY KEY (id);
CREATE INDEX idx_reservations_item ON public.reservations USING btree (item_id);
ALTER TABLE ONLY public.reservations
ADD CONSTRAINT reservations_item_id_fkey FOREIGN KEY (item_id) REFERENCES public.items(id);
ALTER TABLE ONLY public.reservations
ADD CONSTRAINT reservations_member_id_fkey FOREIGN KEY (member_id) REFERENCES public.members(id);
出力の先頭には SET 文やコメント行が並びます(上では省略)。プロンプトに貼るのは CREATE TABLE から下、主キー・索引・外部キーの行までで足ります。PostgreSQL 16 の公式ドキュメントでは、各オプションは次のように説明されています。
| オプション | 公式ドキュメントの説明(要約) |
|---|---|
-s / --schema-only |
オブジェクトの定義だけを出力し、データは出力しない |
-t パターン / --table |
名前がパターンに合うテーブルだけを出力する。複数書ける |
-O / --no-owner |
所有者を設定するコマンドを出力しない |
-x / --no-privileges |
アクセス権限(GRANT/REVOKE)を出力しない |
手早く見たいときは、psql の \d でも列・索引・制約・外部キーが1画面にまとまります。
Table "public.reservations"
Column | Type | Collation | Nullable | Default
-----------+-----------------------------+-----------+----------+---------
id | integer | | not null |
member_id | integer | | not null |
item_id | integer | | not null |
start_at | timestamp without time zone | | not null |
end_at | timestamp without time zone | | not null |
status | character varying(10) | | not null |
Indexes:
"reservations_pkey" PRIMARY KEY, btree (id)
"idx_reservations_item" btree (item_id)
Check constraints:
"reservations_check" CHECK (end_at > start_at)
"reservations_status_check" CHECK (status::text = ANY (ARRAY['booked'::character varying, 'returned'::character varying, 'canceled'::character varying]::text[]))
Foreign-key constraints:
"reservations_item_id_fkey" FOREIGN KEY (item_id) REFERENCES items(id)
"reservations_member_id_fkey" FOREIGN KEY (member_id) REFERENCES members(id)
MySQL:SHOW CREATE TABLE
SHOW CREATE TABLE reservations\G
出力は前の節に載せたとおりです。ファイルに保存したいときは mysqldump --no-data --skip-comments DB名 テーブル名 でも同じ CREATE TABLE 文が得られました(先頭に DROP TABLE IF EXISTS の行が付くので、貼る前に外します)。
SQLite:.schema
sqlite3 check.db ".schema reservations"
CREATE TABLE reservations (
id integer PRIMARY KEY,
member_id integer NOT NULL REFERENCES members (id),
item_id integer NOT NULL REFERENCES items (id),
start_at TEXT NOT NULL,
end_at TEXT NOT NULL,
status varchar(10) NOT NULL CHECK (status IN ('booked', 'returned', 'canceled')),
CHECK (end_at > start_at)
);
CREATE INDEX idx_reservations_item ON reservations (item_id);
SQLite は、作成時に書いた CREATE 文をそのまま返します。サンプル行を INSERT 文の形で取りたいときは .mode insert が使えます。
sqlite3 check.db ".mode insert reservations" "SELECT * FROM reservations ORDER BY id LIMIT 3;"
INSERT INTO reservations VALUES(1,1,1,'2026-09-01 10:00:00','2026-09-01 12:00:00','returned');
INSERT INTO reservations VALUES(2,1,3,'2026-09-03 09:00:00','2026-09-03 18:00:00','returned');
INSERT INTO reservations VALUES(3,2,1,'2026-09-10 13:00:00','2026-09-10 15:00:00','returned');
基本テンプレート(4点セット入り)
{ } の部分を書き換えて使います。タグで区切っておくと、どこが定義でどこが依頼かが AI にも人にも見分けやすくなります。
あなたは {PostgreSQL 16} に詳しいデータベースエンジニアです。
下の情報だけをもとに、依頼に合う SQL を1つ書いてください。
<database>
種類と版: {PostgreSQL 16.15}
タイムゾーン: {日時の列は日本時間で保存}
</database>
<ddl>
{CREATE TABLE 文と CREATE INDEX 文を貼る}
</ddl>
<columns>
{列の意味の表を貼る}
</columns>
<sample_rows>
{1テーブルにつき3〜5行。架空の値}
</sample_rows>
<join_keys>
{reservations.member_id = members.id(社員1人に予約が0件以上)}
{reservations.item_id = items.id(備品1つに予約が0件以上)}
</join_keys>
<request>
{2026年9月の、備品ごとの利用回数を出したい。}
- 月の判定は {start_at} で行う。{9月1日0時以上、10月1日0時より前}
- {取り消し(canceled)は数えない}
- {廃棄済み(retired)の備品は出さない。利用が0回の備品は 0 として出す}
- 出力する列: {備品名, 利用回数}。並び順: {利用回数の多い順、同数は備品名の昇順}
</request>
<rules>
- <ddl> にある表と列だけを使う。足りない情報があれば、SQL を書く前に質問する。
- {PostgreSQL 16} で動く構文と関数だけを使う。
- SELECT 文だけを書く(更新・削除・定義変更は書かない)。
- 予約語は大文字、1行に1列、テーブルには短い別名を付ける。
</rules>
<output_format>
1. SQL(コードブロック1つ)
2. 前提にした解釈(箇条書き。期間の境目、NULL と 0件の扱い、重複の扱い)
3. 結果を確かめるための SQL(件数の合計を出すもの)を1つ
</output_format>
依頼文で特に効くのは、期間の境目・除外する値・0件の扱い・並び順の4つです。ここが空いていると、AI は一般的な解釈で埋めます。後半の検証例では、この4つのうち2つが抜けた SQL がどんな結果になるかを確かめます。
用途別プロンプト
基本テンプレートの <request> より下を差し替えます。<database>〜<join_keys> は共通です。
| 用途 | 追加で渡すもの | 頼み方の要点 |
|---|---|---|
| 新規作成 | ほしい結果の例(2〜3行) | 期間・除外・0件・並び順を書く |
| 既存 SQL の修正 | いまの SQL、変えたい点、変えたくない点 | 変更箇所だけを変え、理由を添えさせる |
| 遅い SQL の見直し | SQL、実行計画、行数の規模、索引 | 結果を変えない書き換えと、索引の案を分けて出させる |
| エラー文からの修正 | SQL、エラー文の全文、DBの種類と版 | 原因を1文で述べさせてから直させる |
新規作成
基本テンプレートをそのまま使います。結果の形を誤解なく伝えたいときは、次を足します。
<expected_shape>
出力の形の例(値は仮のもの):
name | cnt
プロジェクターA | 3
プロジェクターB | 0
</expected_shape>
既存 SQL の修正
<current_sql>
{いま動いている SQL}
</current_sql>
<request>
上の SQL を次のように変えてください。
- 変えたい点: {利用が0回の備品も 0 として出す}
- 変えたくない点: {列の名前と並び順、期間の条件}
</request>
<output_format>
1. 修正後の SQL(コードブロック1つ)
2. 変えた箇所の一覧(表:変更前/変更後/理由)
3. 変更前と変更後で結果が変わる行の条件
</output_format>
遅い SQL の見直し
<current_sql>
{遅い SQL}
</current_sql>
<explain>
{EXPLAIN の出力を全文貼る}
</explain>
<table_size>
{reservations: 約300万行 / items: 約2,000行}
</table_size>
<request>
この SQL が遅い理由を実行計画から説明し、改善案を出してください。
</request>
<rules>
- 結果(行と値)を変えない。
- 「SQL の書き換え」と「索引の追加」は分けて提案する。
- 索引の追加は、更新時の負荷が増える点にも触れる。
</rules>
<output_format>
1. 遅い理由(実行計画のどの行から読み取ったかを示す)
2. 書き換えた SQL
3. 索引の案(CREATE INDEX 文)
4. 改善を確かめる手順(実行計画のどこが変わればよいか)
</output_format>
AI の案は、実行計画が変わったかどうかで確かめられます。たとえば SQLite で start_at に索引を張り、月の判定を2通りの書き方で比べると、次のようになりました。
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM reservations WHERE strftime('%Y-%m', start_at) = '2026-09';
QUERY PLAN
`--SCAN reservations USING COVERING INDEX idx_reservations_start
EXPLAIN QUERY PLAN
SELECT COUNT(*) FROM reservations WHERE start_at >= '2026-09-01' AND start_at < '2026-10-01';
QUERY PLAN
`--SEARCH reservations USING COVERING INDEX idx_reservations_start (start_at>? AND start_at<?)
列を関数で包むと索引を端から端まで読む SCAN、範囲で書くと必要な部分だけを読む SEARCH になります。件数はどちらも 6 で同じでした(この件数は取り消しも含めた9月の予約数です)。
エラー文からの修正
<current_sql>
{エラーになった SQL}
</current_sql>
<error>
{エラー文を全文貼る。行番号や ^ の位置も含める}
</error>
<request>
このエラーの原因を1文で説明し、{PostgreSQL 16} で動く形に直してください。
結果の意味は変えないでください。
</request>
エラー文は要約せず全文を貼るのがコツです。たとえば MySQL 向けの DATE_FORMAT を別のDBで実行すると、次のエラーになります。関数名と位置が含まれているので、AI は原因を特定しやすくなります。
PostgreSQL 16.15:
ERROR: function date_format(timestamp without time zone, unknown) does not exist
LINE 1: SELECT DATE_FORMAT(start_at, '%Y-%m') FROM reservations
^
HINT: No function matches the given name and argument types. You might need to add explicit type casts.
SQLite 3.45.1:
Error: in prepare, no such function: DATE_FORMAT
SELECT DATE_FORMAT(start_at, '%Y-%m') FROM reservations;
^--- error here
逆に、PostgreSQL 向けの to_char や SQLite 向けの strftime を MySQL 8.0.46 で実行したときは、次のとおりでした。
ERROR 1305 (42000) at line 1: FUNCTION t362.to_char does not exist
ERROR 1305 (42000) at line 1: FUNCTION t362.strftime does not exist
方言の指定文(MySQL・PostgreSQL・SQLite)
DBの種類を書かないと、別のDBの関数が混ざることがあります。<rules> に次の1〜2行を入れます。
| DB | 指定文 | 月ごとに集計するときの関数(実行して確認) |
|---|---|---|
| PostgreSQL | 「PostgreSQL 16 で動く構文と関数だけを使う。日付の整形は to_char、文字列の連結は || を使う」 | to_char(start_at, 'YYYY-MM') |
| MySQL | 「MySQL 8.0 で動く構文と関数だけを使う。日付の整形は DATE_FORMAT を使い、識別子を囲むときはバッククォートにする」 | DATE_FORMAT(start_at, '%Y-%m') |
| SQLite | 「SQLite 3.45 で動く構文と関数だけを使う。日時は TEXT 型(YYYY-MM-DD HH:MM:SS)で保存しているので、整形は strftime を使う」 | strftime('%Y-%m', start_at) |
3つとも、同じデータに対して同じ結果になりました(取り消しを除いた月別の予約数)。
-- PostgreSQL
SELECT to_char(start_at, 'YYYY-MM') AS ym, COUNT(*) AS cnt
FROM reservations WHERE status <> 'canceled' GROUP BY ym ORDER BY ym;
-- MySQL
SELECT DATE_FORMAT(start_at, '%Y-%m') AS ym, COUNT(*) AS cnt
FROM reservations WHERE status <> 'canceled' GROUP BY ym ORDER BY ym;
-- SQLite
SELECT strftime('%Y-%m', start_at) AS ym, COUNT(*) AS cnt
FROM reservations WHERE status <> 'canceled' GROUP BY ym ORDER BY ym;
ym | cnt
---------+-----
2026-08 | 1
2026-09 | 5
2026-10 | 1
方言の差が出やすいのは、日付の整形と加減算、文字列の連結、識別子の囲み方、行数の制限、連番の書き方です。どの関数を使ってよいかまで指定文に書くと、やり直しが減ります。
受け取ったSQLを架空データ入りのSQLiteで確かめる
AI から受け取った SQL は、いきなり本番の DB で流さず、答えを手で数えられる小さなデータで確かめます。SQLite はファイル1つで動くので、確認用の場所として手軽です。
手順1:架空データを用意する
定義(上の CREATE TABLE 文の timestamp を TEXT に変えたもの)を schema_sqlite.sql、次のデータを data.sql として保存します。境目になる行をわざと入れるのがポイントです。
INSERT INTO members (id, name, dept, joined_on) VALUES
(1, '青木', '開発部', '2022-04-01'),
(2, '石田', '開発部', '2023-04-01'),
(3, '上野', '総務部', '2021-10-01'),
(4, '江口', '営業部', '2024-04-01');
INSERT INTO items (id, name, category, status) VALUES
(1, 'プロジェクターA', '映像', 'active'),
(2, 'プロジェクターB', '映像', 'active'),
(3, 'ノートPC-01', 'PC', 'active'),
(4, 'ノートPC-02', 'PC', 'retired'),
(5, '会議用マイク', '音響', 'active');
INSERT INTO reservations (id, member_id, item_id, start_at, end_at, status) VALUES
(1, 1, 1, '2026-09-01 10:00:00', '2026-09-01 12:00:00', 'returned'),
(2, 1, 3, '2026-09-03 09:00:00', '2026-09-03 18:00:00', 'returned'),
(3, 2, 1, '2026-09-10 13:00:00', '2026-09-10 15:00:00', 'returned'),
(4, 2, 2, '2026-09-15 10:00:00', '2026-09-15 11:00:00', 'canceled'),
(5, 3, 5, '2026-09-20 14:00:00', '2026-09-20 16:00:00', 'returned'),
(6, 3, 1, '2026-09-30 23:00:00', '2026-10-01 01:00:00', 'returned'),
(7, 1, 5, '2026-10-01 10:00:00', '2026-10-01 11:00:00', 'booked'),
(8, 4, 3, '2026-08-31 09:00:00', '2026-08-31 17:00:00', 'returned');
| 行 | 入れた理由 |
|---|---|
| 予約4(canceled) | 取り消しが除かれるかを見る |
| 予約6(9月30日 23時開始) | 月末の最終日が含まれるかを見る |
| 予約7(10月1日)、予約8(8月31日) | 前後の月が混ざらないかを見る |
| 備品2(予約は取り消しの1件だけ) | 0回の備品が 0 として出るかを見る |
| 備品4(retired) | 廃棄済みが出ないかを見る |
sqlite3 check.db < schema_sqlite.sql
sqlite3 check.db < data.sql
手順2:期待する答えを先に手で出す
「2026年9月、取り消しを除く、備品ごとの利用回数」を表から数えます。対象は予約1・2・3・5・6の合計5件です。
| 備品 | 該当する予約 | 期待する回数 |
|---|---|---|
| プロジェクターA | 1、3、6 | 3 |
| ノートPC-01 | 2 | 1 |
| 会議用マイク | 5 | 1 |
| プロジェクターB | なし | 0 |
合計だけなら、結合のない単純な SQL でも確かめられます。
sqlite3 -header -column check.db "SELECT COUNT(*) AS total FROM reservations WHERE start_at >= '2026-09-01' AND start_at < '2026-10-01' AND status <> 'canceled';"
total
-----
5
手順3:受け取ったSQLを流して比べる
確かめる対象の例として、期間を BETWEEN で書き、内部結合を使った SQL を用意しました(cand_a.sql)。
SELECT i.name, COUNT(*) AS cnt
FROM reservations r
JOIN items i ON i.id = r.item_id
WHERE r.start_at BETWEEN '2026-09-01' AND '2026-09-30'
AND r.status <> 'canceled'
GROUP BY i.name
ORDER BY cnt DESC, i.name;
sqlite3 -header -column check.db < cand_a.sql
name cnt
-------- ---
プロジェクターA 2
ノートPC-01 1
会議用マイク 1
期待と比べると、2か所が合いません。
| 比べる点 | 期待 | 結果 | 原因 |
|---|---|---|---|
| 合計 | 5 | 4 | BETWEEN ... AND '2026-09-30' は 9月30日 0時までなので、23時開始の予約6が外れた |
| プロジェクターB | 0 の行がある | 行がない | 内部結合では、予約のない備品の行が作られない |
エラーにはならず、もっともらしい表が返ってくる点に注目してください。件数と集計値を期待と突き合わせて、初めて差に気づけます。
手順4:差を伝えて直してもらい、もう一度流す
差が見つかったら、次のように事実だけを伝えます。
<current_sql>
{cand_a.sql の中身}
</current_sql>
<check_result>
架空データで確かめた結果、期待と2点が違いました。
- 合計: 期待 5 件、結果 4 件。start_at が 2026-09-30 23:00:00 の行が含まれていない。
- 利用が0回の備品(プロジェクターB)の行が出ていない。期待は cnt = 0 の行。
</check_result>
<request>
期間は「9月1日0時以上、10月1日0時より前」とし、active の備品は0回でも出すように直してください。
</request>
直した SQL(cand_b.sql)は次のとおりです。
SELECT i.name, COUNT(r.id) AS cnt
FROM items i
LEFT JOIN reservations r
ON r.item_id = i.id
AND r.start_at >= '2026-09-01'
AND r.start_at < '2026-10-01'
AND r.status <> 'canceled'
WHERE i.status = 'active'
GROUP BY i.id, i.name
ORDER BY cnt DESC, i.name;
name cnt
-------- ---
プロジェクターA 3
ノートPC-01 1
会議用マイク 1
プロジェクターB 0
4行すべてが期待と一致し、cnt の合計も 5 になりました。同じ2つの SQL を PostgreSQL 16.15 と MySQL 8.0.46 でも流したところ、件数は同じでした(直す前はプロジェクターAが 2、直した後は 3 で、プロジェクターBの 0 の行が加わる)。同数の行の並び順は、DBの文字の並べ方によって入れ替わることがあります。
本番のDBで流す前の確認表
小さなデータで合っていることを確かめたら、次の表に沿って本番に近い環境へ進みます。
| 確認すること | やり方 | 実行して確かめた例 |
|---|---|---|
| SELECT 文だけか | 文の先頭と、; で区切られた2つ目以降の文を目で見る |
― |
| 表と列が実在するか | 定義と見比べる。流せばエラーで分かる | no such function などのエラー文 |
| 書き込めない接続で流す | 読み取り専用ユーザー、読み取り専用トランザクション、読み取り専用で開く | 下の3例 |
| 実行計画を見る | SQL の前に EXPLAIN(SQLite は EXPLAIN QUERY PLAN)を付ける |
下の例 |
| 少ない行で形を見る | 末尾に LIMIT 10 を付けて列と書式を確かめる |
下の例 |
| 件数を確かめる | 結合のない単純な COUNT と合計が合うか |
手順2と手順4 |
| 境目を確かめる | 月末・0件・NULL・除外する値の行で結果を見る | 手順3 |
| 行の重複がないか | 1対多の結合のあとで COUNT(*) と COUNT(DISTINCT 主キー) を比べる |
― |
同じ表のタブ区切りです。表計算ソフトにそのまま貼れます。
確認すること やり方 結果(記入欄)
SELECT 文だけか 文の先頭と、; で区切られた2つ目以降の文を目で見る
表と列が実在するか 定義と見比べる。流せばエラーで分かる
書き込めない接続で流す 読み取り専用ユーザー、読み取り専用トランザクション、読み取り専用で開く
実行計画を見る SQL の前に EXPLAIN(SQLite は EXPLAIN QUERY PLAN)を付ける
少ない行で形を見る 末尾に LIMIT 10 を付けて列と書式を確かめる
件数を確かめる 結合のない単純な COUNT と合計が合うか
境目を確かめる 月末・0件・NULL・除外する値の行で結果を見る
行の重複がないか 1対多の結合のあとで COUNT(*) と COUNT(DISTINCT 主キー) を比べる
読み取り専用で流す
受け取った SQL に更新や削除の文が混ざっていても、書き込めない接続なら実行されません。
PostgreSQL:SELECT だけを許したロールを作る。
CREATE ROLE report_ro LOGIN PASSWORD '{パスワード}';
GRANT CONNECT ON DATABASE t362 TO report_ro;
GRANT USAGE ON SCHEMA public TO report_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO report_ro;
このロールで SELECT COUNT(*) FROM reservations は 8 を返し、DELETE FROM reservations は次のように止まりました。
ERROR: permission denied for table reservations
ロールを作れないときは、読み取り専用のトランザクションでも同じ目的を果たせます。
BEGIN READ ONLY;
DELETE FROM reservations;
ROLLBACK;
BEGIN
ERROR: cannot execute DELETE in a read-only transaction
ROLLBACK
MySQL:SELECT だけを許したユーザーを作る。
CREATE USER 'report_ro'@'localhost' IDENTIFIED BY '{パスワード}';
GRANT SELECT ON t362.* TO 'report_ro'@'localhost';
ERROR 1142 (42000) at line 1: DELETE command denied to user 'report_ro'@'localhost' for table 'reservations'
SQLite:-readonly を付けて開く。
sqlite3 -readonly check.db "DELETE FROM reservations;"
Error: stepping, attempt to write a readonly database (8)
実行計画を見る
直した SQL(cand_b.sql)の前に EXPLAIN を付けた結果です。行数の多いテーブルを端から端まで読んでいないか、索引が使われているかを見ます。
SQLite(EXPLAIN QUERY PLAN):
QUERY PLAN
|--SCAN i
|--SEARCH r USING INDEX idx_reservations_item (item_id=?) LEFT-JOIN
`--USE TEMP B-TREE FOR ORDER BY
PostgreSQL(EXPLAIN。抜粋):
Sort (cost=36.86..36.87 rows=2 width=130)
Sort Key: (count(r.id)) DESC, i.name
-> GroupAggregate (cost=36.82..36.85 rows=2 width=130)
Group Key: i.id
…(省略)…
-> Nested Loop Left Join (cost=4.18..36.81 rows=2 width=126)
-> Seq Scan on items i (cost=0.00..14.12 rows=2 width=122)
Filter: ((status)::text = 'active'::text)
-> Bitmap Heap Scan on reservations r (cost=4.18..11.33 rows=1 width=8)
Recheck Cond: (item_id = i.id)
…(省略)…
-> Bitmap Index Scan on idx_reservations_item (cost=0.00..4.18 rows=4 width=0)
Index Cond: (item_id = i.id)
MySQL(EXPLAIN。主な列を抜粋):
+----+-------------+-------+------+-----------------------+------+------+----------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key | rows | filtered | Extra |
+----+-------------+-------+------+-----------------------+------+------+----------+----------------------------------------------+
| 1 | SIMPLE | i | ALL | NULL | NULL | 5 | 20.00 | Using where; Using temporary; Using filesort |
| 1 | SIMPLE | r | ALL | idx_reservations_item | NULL | 8 | 100.00 | Using where; Using join buffer (hash join) |
+----+-------------+-------+------+-----------------------+------+------+----------+----------------------------------------------+
8行しかないデータなので、MySQL は索引を使わず全体を読む計画を選んでいます。実行計画は行数によって変わるため、小さなデータでは「構文が通ること」と「結果が合うこと」を確かめ、速さは本番に近い行数の環境で実行計画を取り直して判断します。
LIMIT で形を見る
sqlite3 -header -column check.db "SELECT id, item_id, start_at, status FROM reservations ORDER BY id LIMIT 3;"
id item_id start_at status
-- ------- ------------------- --------
1 1 2026-09-01 10:00:00 returned
2 3 2026-09-03 09:00:00 returned
3 1 2026-09-10 13:00:00 returned
LIMIT は返す行数を絞るもので、集計や並べ替えを含む SQL では、絞る前の処理は全体に対して行われます。重い SQL の負荷を下げる目的には、期間の条件を狭めるほうが向いています。
プロンプトに貼る前に見直すこと
| 見直す点 | 理由 | 対応 |
|---|---|---|
| サンプル行に本物の氏名・連絡先・社外秘の値がないか | 外部のサービスに送ることになる | 架空の値に置き換える。組織の利用ルールに従う |
| 接続先・パスワード・接続文字列が混ざっていないか | 定義の出力やスクリプトに含まれることがある | 貼る前に削る |
| テーブルが多すぎないか | 関係のない定義は読み違いのもとになる | 依頼に関係するテーブルだけに絞る |
| 列の意味の表が最新か | 値の種類が増えていることがある | SELECT DISTINCT status FROM ... で実際の値を確かめる |
よくある質問
Q. テーブル定義は文章で説明するのと、CREATE TABLE 文を貼るのと、どちらがよいですか? A. CREATE TABLE 文を貼るほうが正確です。型・NULL の可否・制約・索引が一度に伝わり、書き写しの誤りも入りません。文章は、値の意味や数え方の決まりなど、定義から読み取れないことを補うために使います。
Q. サンプル行は何行くらい必要ですか? A. 1テーブルにつき3〜5行が目安です。目的は日時の書式や値の表記を伝えることなので、多くは要りません。取り消しや NULL など、扱いに迷う値を含む行を1つ入れると効果的です。
Q. テーブルが数十個あるときはどうしますか? A. 依頼に関係するテーブルだけを選んで渡します。どれが関係するか分からないときは、先にテーブル名と1行の説明の一覧だけを渡し、「必要なテーブルを挙げてください」と頼んでから、選ばれたテーブルの4点セットを渡す2段階にします。
Q. 確かめるための架空データも AI に作らせてよいですか? A. INSERT 文の下書きを頼むのは便利です。ただし、期待する答えは自分で数えます。SQL と期待値の両方を同じ相手に任せると、同じ解釈のずれが両方に入り、突き合わせても差が出ません。
まとめ
- テーブル定義は「CREATE TABLE 文・列の意味の表・サンプル行・結合キー」の4点セットで渡す
- 定義は
pg_dump --schema-only、SHOW CREATE TABLE、.schemaで取り出し、結合キーは定義とは別に書く - 依頼文には期間の境目・除外する値・0件の扱い・並び順を書く
- DBの種類と版、使ってよい関数を指定文に入れると、別のDBの関数が混ざりにくい
- エラー文と実行計画は要約せず全文を貼る
- 受け取った SQL は、境目の行を入れた架空データの SQLite で流し、件数と集計値を手で出した期待と比べる
- 本番に近い環境では、読み取り専用の接続・実行計画・LIMIT の順に確かめる
参考・出典
- PostgreSQL 16 Documentation「pg_dump」 https://www.postgresql.org/docs/16/app-pgdump.html (2026年10月5日に閲覧。
--schema-only、--table、--no-owner、--no-privilegesの説明)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「生成AIにSQLを書かせるプロンプト|テーブル定義の渡し方とテンプレート」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。