主キーと外部キー(表どうしを結び付ける)の使い方
できた表 keihi
| 列 | 型 | NULL | 制約 |
|---|---|---|---|
| keihi_id | integer | 不可 | 主キー |
| busho_id | integer | 不可 | 外部キー → busho.busho_id・親を消せない |
| kingaku | integer | 不可 |
外部キーはどの表のどの列を指しているかまで定義に残ります。ON DELETE を書かなかったので、参照されている部署の行は消せません。これが既定の動きで、たいていの場合これが正しい既定です。
主キーは「この行はどれか」を決め、外部キーは「その値が向こうに実在するか」を守ります。 2 つ合わせて、表どうしのつながりが壊れないようにする仕組みです。⚠️ いちばん効いてくるのは親の行を消そうとしたとき——既定では消せません。連鎖して消すことも、参照だけ外すこともできますが、どれを選ぶかは設計の判断で、あとから変えると既に消えたデータは戻りません。
この書き方が使えない製品があります そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server
- Oracle Database では使えません(text 型の列) / この問題のほかの構文は未確認です
未確認: IBM Db2
このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。
01主キーは行を 1 つに決める
結論: 主キーは「この 1 行を指し示す値」です。 重複せず、NULL にもなりません。
何を主キーにするかは 2 通りあります。社員番号や商品コードのように、業務ですでに使われている値を使う方法と、その表のためだけに用意した通し番号を使う方法です。前者は読んで意味が分かる代わりに、業務の都合で変わることがあります。番号体系の変更が起きると、その値を指している全部の表を直すことになります。
迷ったら通し番号にしておくと、あとで困りにくくなります。業務上の値には UNIQUE を付けておけば、重複は同じように防げます。
⚠️ 主キーは 1 つの表に 1 つだけです。「2 つ目の一意」が要るときは UNIQUE を使います。⚠️ 複数の列をまとめて 1 つの主キーにもできます(詳しくは下の「関連するトピック」の制約のページ)。
CREATE TABLE keihi (経費の表を作るkeihi_id int PRIMARY KEY,★この表の行を決める値busho_id int NOT NULL★向こうの表を指す値REFERENCES busho(busho_id),★どの表のどの列を指すかkingaku int NOT NULLふつうの列);括弧を閉じて終わり
REFERENCES の右に書くのは指し先です。指し先の列には主キーか一意制約が必要で、ただの列は指せません。指した先が 1 行に決まらないと、実在するかを確かめようがないためです。
CREATE TABLE keihi (
keihi_id int PRIMARY KEY,
busho_id int NOT NULL REFERENCES busho(busho_id),
kingaku int NOT NULL
);
INSERT INTO keihi (keihi_id, busho_id, kingaku) VALUES (1, 99, 30000);「violates foreign key constraint」で落ちます。busho に 99 番の部署が無いためです。入る前に止まるので、あとから辻褄合わせをしなくて済みます。
CREATE TABLE keihi (
keihi_id int PRIMARY KEY,
busho_id int NOT NULL REFERENCES busho(busho_name),
kingaku int NOT NULL
);「foreign key constraint "keihi_busho_id_fkey" cannot be implemented」=この外部キーは作れないと出ます。busho_name には UNIQUE が付いていないので、同じ名前の部署が 2 つあったときに指し先が決まりません。指し先には主キーか一意制約が要る、というのはこのためです。
02⚠️ よくある間違い:既定では、参照されている親は消せない
結論: ON DELETE を書かなければ、子から指されている親の行は消せません。
消そうとするとデータベースが拒否します。経費の記録が「どこの部署か分からない」状態になるのを防ぐためで、たいていの場合これが正しい既定です。消したいなら、先に子を片付けるか、あとで説明する指定を書きます。
⚠️ 「エラーが出るから外部キーを外す」は、いちばんやってはいけない直し方です。止めてくれているものを外すと、壊れたデータが静かに増えます。消せないのは、消してはいけないものが残っているからです。
⚠️ 外部キーは NULL を止めません。 参照先に実在するかを見るだけなので、NULL は素通しです。「必ずどこかの部署に属する」を求めるなら NOT NULL も一緒に書いてください。
CREATE TABLE bunrui (
bunrui_id int PRIMARY KEY,
nm text NOT NULL
);
CREATE TABLE memo (
memo_id int PRIMARY KEY,
bunrui_id int REFERENCES bunrui(bunrui_id),
honbun text NOT NULL
);
INSERT INTO bunrui (bunrui_id, nm) VALUES (1, 'renraku');
INSERT INTO memo (memo_id, bunrui_id, honbun) VALUES (1, 1, 'いち');
DELETE FROM bunrui WHERE bunrui_id = 1;「violates foreign key constraint」で落ちます。指している子(メモ)が残っているあいだは、親(分類)を消せません。
⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表
03親と一緒に子も消す(ON DELETE CASCADE)
結論: ON DELETE CASCADE を書くと、親を消したときに子の行も一緒に消えます。
親が無くなれば子に意味が無くなる場合に使います。注文と注文明細、記事とその下書き、といった「親が消えたら要らない」関係です。
⚠️ 消える範囲を必ず数えてから使ってください。 分類を 1 つ消しただけで、その下のメモも履歴も静かに消えます。しかも連鎖は何段でも続くので、孫・ひ孫まで消えることがあります。
「消せなくて困る」ほうが、「消えて気づかない」より安く済みます。 迷ったら既定のまま(消せない)にしておき、消す手順を業務として決めるほうが安全です。
できた表 memo
| 列 | 型 | NULL | 制約 |
|---|---|---|---|
| memo_id | integer | 不可 | 主キー |
| bunrui_id | integer | 可 | 外部キー → bunrui.bunrui_id・親を消すと一緒に消える |
| honbun | text | 不可 |
指し先と、親を消したときにどうなるかが定義として残ります。実際に消してみなくても、この表がどういう約束になっているかを読めます。
CREATE TABLE bunrui (
bunrui_id int PRIMARY KEY,
nm text NOT NULL
);
CREATE TABLE memo (
memo_id int PRIMARY KEY,
bunrui_id int REFERENCES bunrui(bunrui_id) ON DELETE CASCADE,
honbun text NOT NULL
);
INSERT INTO bunrui (bunrui_id, nm) VALUES (1, 'renraku'), (2, 'gijiroku');
INSERT INTO memo (memo_id, bunrui_id, honbun) VALUES (1, 1, 'いち'), (2, 2, 'に');
DELETE FROM bunrui WHERE bunrui_id = 1;
SELECT memo_id, bunrui_id, honbun FROM memo ORDER BY memo_id;| memo_id | bunrui_id | honbun |
|---|---|---|
| 2 | 2 | に |
(1 行)
分類 1 を消しただけで、メモの 1 行目まで消えています。DELETE FROM memo は一度も書いていません。
⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表
04親を消しても子は残す(ON DELETE SET NULL)
結論: ON DELETE SET NULL を書くと、親を消したときに子の参照の欄だけが NULL になり、子の行は残ります。
退会した会員の注文履歴、廃止した部署の経費など、「親はもう無いが、記録は残したい」場面で使います。連鎖削除とは正反対の判断です。
⚠️ 参照の列に NOT NULL は付けられません。 NULL を入れる動きなので、両方を書くと親を消した時点でエラーになります。「必ずどこかに属する」を守りたいなら、この指定は選べません。
⚠️ NULL になった行をどう扱うかまで決めてください。 分類の無いメモが増えると、部署ごとの集計から漏れたり、画面に空欄が並んだりします。残すと決めた以上、残ったあとの見え方まで設計の範囲です。
CREATE TABLE bunrui (
bunrui_id int PRIMARY KEY,
nm text NOT NULL
);
CREATE TABLE memo (
memo_id int PRIMARY KEY,
bunrui_id int REFERENCES bunrui(bunrui_id) ON DELETE SET NULL,
honbun text NOT NULL
);
INSERT INTO bunrui (bunrui_id, nm) VALUES (1, 'renraku'), (2, 'gijiroku');
INSERT INTO memo (memo_id, bunrui_id, honbun) VALUES (1, 1, 'いち'), (2, 2, 'に');
DELETE FROM bunrui WHERE bunrui_id = 1;
SELECT memo_id, bunrui_id, honbun FROM memo ORDER BY memo_id;| memo_id | bunrui_id | honbun |
|---|---|---|
| 1 | NULL | いち |
| 2 | 2 | に |
(2 行)
1 行目のメモは消えずに残り、bunrui_id だけが NULL になりました。本文は失われていません。
⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表
CREATE TABLE bunrui (
bunrui_id int PRIMARY KEY,
nm text NOT NULL
);
CREATE TABLE memo (
memo_id int PRIMARY KEY,
bunrui_id int NOT NULL REFERENCES bunrui(bunrui_id) ON DELETE SET NULL,
honbun text NOT NULL
);
INSERT INTO bunrui (bunrui_id, nm) VALUES (1, 'renraku');
INSERT INTO memo (memo_id, bunrui_id, honbun) VALUES (1, 1, 'いち');
DELETE FROM bunrui WHERE bunrui_id = 1;表を作るところは通ってしまい、親を消そうとして初めて落ちます。書けたことと、破綻しないことは別です。
⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表
自分で打ってみる
このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。
押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。
解いてみる
読んだだけでは書けるようになりません。実行結果で採点します。
関連するトピック
- 制約NOT NULL・UNIQUE・CHECK と、複数の列をまとめた主キーはこちらです。
- CREATE TABLE外部キーを書く場所である CREATE TABLE そのものの説明はこちらです。
- INNER JOIN外部キーで結び付けた表を、実際に取り出すときの書き方です。
- DELETE行を消す操作そのものの説明はこちらです。
根拠(一次情報)
- PostgreSQL 18 マニュアル: 制約(外部キーと ON DELETE)一次情報・確認 2026-09-03
- PostgreSQL 18 マニュアル: CREATE TABLE一次情報・確認 2026-09-03
- SQLite: SQLite Foreign Key Support一次情報・確認 2026-09-03