コードを書く作業の多くを生成 AI に任せられるようになりました。画面も API もテストも、頼めば数分で出てきます。その一方で、AI に任せにくく、後から直すのが一番高くつく部分が残っています。DB の設計です。
この記事では、生成 AI と一緒に開発するときに DB 設計が以前より重要になる理由を 3 つ、実際に動かした SQL で確かめます。
結論
- 制約は、AI が書いたコードの不具合を止める最後の検査になる。 アプリのコードが誰に書かれても、DB に入る値の条件は DB が守る
- スキーマは、AI がコードと SQL を書くときに読む仕様になる。 型・制約・コメントが書かれているほど、AI は正しく推測できる
- コードは作り直せても、データは作り直せない。 AI が書いたマイグレーションの 1 行で、データは消える
検証は PGlite(PostgreSQL 18.3 の WebAssembly 版)で行いました。
理由 1:制約は AI が書いたコードの不具合を止める
AI が書くコードは、もっともらしく見えて細部が違うことがあります。ステータスの綴りが 1 文字違う、数量のマイナスを考えていない、存在しない ID を渡している、といったものです。レビューで見落とせば、そのまま本番のデータに入ります。
同じ注文テーブルを、制約なしと制約ありの 2 通りで作って試します。
CREATE TABLE products (id bigint PRIMARY KEY, name text NOT NULL);
INSERT INTO products VALUES (1, 'ノート'), (2, 'ペン');
-- 制約なし
CREATE TABLE orders_loose (
id bigint,
product_id bigint,
quantity int,
status text,
email text
);
-- 制約あり
CREATE TABLE orders_strict (
id bigint PRIMARY KEY,
product_id bigint NOT NULL REFERENCES products (id),
quantity int NOT NULL CHECK (quantity > 0),
status text NOT NULL CHECK (status IN ('pending', 'paid', 'cancelled')),
email text NOT NULL
);
それぞれに 1 件ずつ正しい注文を入れたあと、AI が書いたコードにありがちな誤りを含む行を 5 つ入れてみます。
| 入れた値 | orders_loose | orders_strict |
|---|---|---|
status が 'canceled'(綴り違い) |
入る |
orders_strict_status_check 違反で拒否 |
quantity が -1
|
入る |
orders_strict_quantity_check 違反で拒否 |
product_id が 999(存在しない) |
入る |
orders_strict_product_id_fkey 違反で拒否 |
email が NULL
|
入る | NOT NULL 違反で拒否 |
| id が既存と重複 | 入る |
orders_strict_pkey 違反で拒否 |
結果は、orders_loose が 6 行、orders_strict が 1 行でした。制約なしのテーブルには誤った行がすべて黙って入り、制約ありのテーブルはすべてをエラーで止めました。
制約なしで入った誤りは、集計や請求の段階になって初めて見つかります。そのときにはどのコードが入れたのかを追うのも大変です。制約があれば、誤りはそのコードが最初に動いた瞬間にエラーとして見つかります。コードを書く速さが上がるほど、この差は大きくなります。
理由 2:スキーマは AI が読む仕様になる
コーディングエージェントも、自然文から SQL を作る機能(text-to-SQL)も、まずスキーマを読んでからコードや SQL を書きます。そのとき AI が手がかりにできるのは、列名と型、制約、コメントだけです。
列の説明の効果を調べた研究があります。text-to-SQL のベンチマーク BIRD-Bench の開発用データ(1,534 問)で、列の説明を与えると、GPT-4o の正解率は説明なしの 30.1% から、人が書いた説明ありで 33.3% に上がりました。さらに LLM に詳しい説明を書かせると 36.8% まで上がっています(Synthetic SQL Column Descriptions and Their Impact on Text-to-SQL Performance)。この研究では、名前から意味を推測しにくい列ほど正解率が下がることも報告されています。
PostgreSQL なら、説明は COMMENT ON で DB 自体に持たせられます。
COMMENT ON TABLE orders_strict IS '注文。1 行 = 1 商品の注文';
COMMENT ON COLUMN orders_strict.quantity IS '注文数量(個)。1 以上';
COMMENT ON COLUMN orders_strict.status IS 'pending: 支払い待ち / paid: 支払い済み / cancelled: 取り消し';
COMMENT ON COLUMN orders_strict.email IS '注文時点の連絡先。会員情報の変更では更新しない';
コメントは、DB に接続できるツールや AI からいつでも読めます。
SELECT a.attname AS column_name,
format_type(a.atttypid, a.atttypmod) AS data_type,
a.attnotnull AS not_null,
col_description(a.attrelid, a.attnum) AS comment
FROM pg_attribute a
WHERE a.attrelid = 'orders_strict'::regclass
AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum;
column_name | data_type | not_null | comment
-------------+-----------+----------+--------------------------------------------------------------
id | bigint | t |
product_id | bigint | t |
quantity | integer | t | 注文数量(個)。1 以上
status | text | t | pending: 支払い待ち / paid: 支払い済み / cancelled: 取り消し
email | text | t | 注文時点の連絡先。会員情報の変更では更新しない
制約も同じように読めます。status に入れてよい値は、コメントを読まなくても CHECK 制約からわかります。
SELECT conname, pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'orders_strict'::regclass
AND contype IN ('p', 'u', 'f', 'c')
ORDER BY conname;
conname | definition
-------------------------------+-----------------------------------------------------------------------------------
orders_strict_pkey | PRIMARY KEY (id)
orders_strict_product_id_fkey | FOREIGN KEY (product_id) REFERENCES products(id)
orders_strict_quantity_check | CHECK ((quantity > 0))
orders_strict_status_check | CHECK ((status = ANY (ARRAY['pending'::text, 'paid'::text, 'cancelled'::text])))
status int に 1 や 2 を入れる設計だったらどうでしょう。AI は 2 が何を意味するのかを、どこからも知ることができません。人間の新しいメンバーにとっても同じです。AI に読みやすいスキーマは、人間にとっても読みやすいスキーマです。
理由 3:コードは作り直せても、データは作り直せない
生成 AI のおかげで、コードは気に入らなければ作り直せるものになりました。データは違います。本番の DB に入った値は、消したり壊したりすると元に戻せません。
わかりやすい例が列名の変更です。pref を prefecture に変えるマイグレーションは、2 通りに書けます。
-- 書き方 A:消して足す
ALTER TABLE customers DROP COLUMN pref;
ALTER TABLE customers ADD COLUMN prefecture text;
-- 書き方 B:名前だけ変える
ALTER TABLE customers RENAME COLUMN pref TO prefecture;
3 行(富山県・東京都・大阪府)が入ったテーブルで、それぞれを実行した後の中身です。
-- A のあと
id | prefecture
----+------------
1 |
2 |
3 |
-- B のあと
id | prefecture
----+------------
1 | 富山県
2 | 東京都
3 | 大阪府
どちらも構文は正しく、エラーも出ません。違いは「既存のデータが残るかどうか」だけです。スキーマの差分だけを見て SQL を作ると、pref が消えて prefecture が増えたようにも見えるので、A が出てくることがあります。マイグレーションのレビューで見るべきなのは、SQL が動くかどうかではなく、データがどうなるかです。
AI と一緒に DB を設計するときに決めておくこと
- スキーマをテキストでリポジトリに置く。 DDL や DBML として Git で管理すれば、AI にそのまま渡せて、変更は差分でレビューできる
- 制約を先に決める。 NOT NULL、外部キー、UNIQUE、CHECK は、アプリのコードより先に書く。後から付けると、既存の不正なデータの掃除から始めることになる
- 列の意味と単位を書く。 金額は税込か、単位は円か、いつ更新されるのか。名前で表せないことはコメントに書く
-
AI が作ったマイグレーションは、データへの影響を見てからレビューを通す。
DROP、型の変更、NOT NULLの追加は、既存のデータに必ず影響する - 機械で検査できることは機械に任せる。 主キーのないテーブル、インデックスのない外部キーなどは、Lint で毎回確かめられる
筆者は、DBML でスキーマを書いて ER 図・DDL・テーブル定義書を生成する DB 設計ツール「Joinery」を開発しています。AI アシスタントによるスキーマの変更は差分を確認してから反映する作りにしていて、この記事で挙げた Lint もあります。設計の進め方は DB 設計ガイド にまとめています。