コマンド道場

主キーと外部キー(表どうしを結び付ける)の使い方

外部キーが何を指し、親を消すとどうなるか

できた表 keihi

NULL制約
keihi_idinteger不可主キー
busho_idinteger不可外部キー → busho.busho_id・親を消せない
kingakuinteger不可

外部キーはどの表のどの列を指しているかまで定義に残ります。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 つの主キーにもできます(詳しくは下の「関連するトピック」の制約のページ)。

どこが「決める側」で、どこが「指す側」か
  1. CREATE TABLE keihi (経費の表を作る
  2. keihi_id int PRIMARY KEY,★この表の行を決める値
  3. busho_id int NOT NULL★向こうの表を指す値
  4. REFERENCES busho(busho_id),★どの表のどの列を指すか
  5. kingaku int NOT NULLふつうの列
  6. );括弧を閉じて終わり

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);
ERROR: insert or update on table "keihi" violates foreign key constraint "keihi_busho_id_fkey"

「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
);
ERROR: foreign key constraint "keihi_busho_id_fkey" cannot be implemented

「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;
ERROR: update or delete on table "bunrui" violates foreign key constraint "memo_bunrui_id_fkey" on table "memo"

「violates foreign key constraint」で落ちます。指している子(メモ)が残っているあいだは、親(分類)を消せません。

⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表

03親と一緒に子も消す(ON DELETE CASCADE)

結論: ON DELETE CASCADE を書くと、親を消したときに子の行も一緒に消えます。

親が無くなれば子に意味が無くなる場合に使います。注文と注文明細、記事とその下書き、といった「親が消えたら要らない」関係です。

⚠️ 消える範囲を必ず数えてから使ってください。 分類を 1 つ消しただけで、その下のメモも履歴も静かに消えます。しかも連鎖は何段でも続くので、孫・ひ孫まで消えることがあります。

「消せなくて困る」ほうが、「消えて気づかない」より安く済みます。 迷ったら既定のまま(消せない)にしておき、消す手順を業務として決めるほうが安全です。

連鎖削除を指定した表

できた表 memo

NULL制約
memo_idinteger不可主キー
bunrui_idinteger外部キー → bunrui.bunrui_id・親を消すと一緒に消える
honbuntext不可

指し先と、親を消したときにどうなるかが定義として残ります。実際に消してみなくても、この表がどういう約束になっているかを読めます。

分類を消すと、その分類のメモも消える
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_idbunrui_idhonbun
22

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_idbunrui_idhonbun
1NULLいち
22

2

1 行目のメモは消えずに残りbunrui_id だけが NULL になりました。本文は失われていません。

⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表

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 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;
ERROR: null value in column "bunrui_id" of relation "memo" violates not-null constraint

表を作るところは通ってしまい、親を消そうとして初めて落ちます。書けたことと、破綻しないことは別です。

⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

関連するトピック

  • 制約NOT NULL・UNIQUE・CHECK と、複数の列をまとめた主キーはこちらです。
  • CREATE TABLE外部キーを書く場所である CREATE TABLE そのものの説明はこちらです。
  • INNER JOIN外部キーで結び付けた表を、実際に取り出すときの書き方です。
  • DELETE行を消す操作そのものの説明はこちらです。

根拠(一次情報)