Day5 前半のゴール
「“あいまいなデータ”をDBレベルで拒否できるようになる」
今日は「制約(constraint)」の中でも、特に重要な2つに絞ります。
PRIMARY KEY
FOREIGN KEY
SQLiteでも出てきましたが、MySQLでは「本気で使う」前提になります。
Day5 前半のゴールは、
PRIMARY KEY が“単なるID”ではなく“行の一意性の保証”だと理解する
FOREIGN KEY が“テーブル間の関係を壊させないためのガード”だと理解する
SQLiteとMySQLでのFOREIGN KEYの扱いの差を意識できる
ここまで行ければ、テーブル設計のレベルが一段上がります。
制約とは何か
「“こういうデータは入れてはいけない”というルール」
制約(constraint)は、
「このテーブルには、こういうデータしか入れてはいけない」というルールです。
NOT NULL
→ このカラムは必ず値が必要
UNIQUE
→ 重複してはいけない
PRIMARY KEY
→ 行を一意に識別する“代表”
FOREIGN KEY
→ 他のテーブルに存在する値しか入れてはいけない
アプリ側のバリデーション(if文など)と違って、
DB側が「絶対に破らせないルール」として持ちます。
これがあると、
アプリのバグや不正なリクエストが来ても、
DBが「それはおかしい」と弾いてくれる
という“最後の砦”になります。
PRIMARY KEY の役割
「“この行はこれで一意に決まる”という“代表カラム”」
PRIMARY KEY は、
「このテーブルの1行を一意に識別するためのカラム(またはカラムの組み合わせ)」です。
よくあるのは、AUTO_INCREMENT の整数IDです。
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(255) NOT NULL
);
SQLここでの id は、
NULL になってはいけない
重複してはいけない
行を特定するときの“代表”
という性質を持ちます。
SQLiteでも INTEGER PRIMARY KEY をよく使っていましたが、
MySQLでは「PRIMARY KEY をどう設計するか」が、
インデックスやJOINの性能にも直結します。
なぜ“意味のある値”をPRIMARY KEYにしないのか
「メールアドレスや会員番号を“主キーにしない”理由」
初心者がやりがちなのが、
メールアドレスをPRIMARY KEYにする
会員番号(人間が見るID)をPRIMARY KEYにする
という設計です。
一見便利そうですが、問題が多いです。
メールアドレスが変わったら?
→ PRIMARY KEY を更新することになる(重い・危険)
会員番号のルールを変えたくなったら?
→ 既存データとの整合性が崩れる
漏えいしたときに、
→ PRIMARY KEY だけで個人が特定されやすくなる
だからこそ、MySQLでは、
内部用の連番ID(INT AUTO_INCREMENT)をPRIMARY KEYにする
メールアドレスや会員番号は「ただのカラム」+必要ならUNIQUE制約
という設計が基本になります。
PRIMARY KEY は「システム内部の都合で安定している値」にする。
これが、セキュリティと運用の両面で重要です。
複合PRIMARY KEYという考え方
「“2つセットで一意”というケースもある」
PRIMARY KEY は、1カラムとは限りません。
「この2つの組み合わせで一意」というケースもあります。
例えば、「ユーザーごとの設定」を持つテーブル。
CREATE TABLE user_settings (
user_id INT NOT NULL,
key VARCHAR(50) NOT NULL,
value VARCHAR(255),
PRIMARY KEY (user_id, key)
);
SQLここでは、
user_id だけでは一意にならない(1ユーザーに複数設定がある)
key だけでも一意にならない(全ユーザーで同じkeyがありうる)
user_id + key の組み合わせなら一意
という状況です。
このように、
「何をもって“1行”とみなすか」
を考えて PRIMARY KEY を決めるのが、本来の設計です。
ただし、初心者のうちは、
基本は単一の AUTO_INCREMENT ID
必要になったら複合PRIMARY KEYも検討
くらいのスタンスでOKです。
FOREIGN KEY の役割
「“親にいない子ども”をDBレベルで禁止する」
次に、今日のメインテーマの1つ、FOREIGN KEY です。
FOREIGN KEY は、
「このカラムに入る値は、必ず別のテーブルの特定カラムに存在していなければならない」
という制約です。
典型的な例は、「ユーザー」と「注文」です。
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
) ENGINE=InnoDB;
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
amount INT NOT NULL,
CONSTRAINT fk_orders_user
FOREIGN KEY (user_id)
REFERENCES users(id)
) ENGINE=InnoDB;
SQLここでのルールは、
orders.user_id に入れてよいのは、
users.id に存在する値だけ
ということです。
つまり、
存在しないユーザーIDで注文を作ろうとすると、
DBがエラーにしてくれる
という“ガード”になります。
SQLite と MySQL の FOREIGN KEY の違い
「SQLiteは“オプション”、MySQL+InnoDBは“本気で使う前提”」
SQLiteにもFOREIGN KEYはありますが、
デフォルトで無効だったり、
アプリ側があまり使っていなかったりするケースが多いです。
PRAGMA foreign_keys = ON; を明示しないと効かない、
という罠もありました。
一方、MySQL+InnoDBでは、
FOREIGN KEY を前提に設計する
→ DBがテーブル間の整合性を守る
という文化が強いです。
その結果、
アプリ側で「この user_id は本当に存在するか?」を毎回チェックしなくても、
DBが「存在しないIDは受け付けない」と保証してくれる
という状態を作れます。
これは、セキュリティ的にも品質的にも非常に大きいです。
FOREIGN KEY が守ってくれるもの
「“孤児データ”と“おかしな参照”を防ぐ」
FOREIGN KEY がないと、こんなことが起こります。
users からユーザーを削除したのに、
orders にはそのユーザーの注文が残り続ける
存在しない user_id を持つ注文が大量に溜まる
こうした「親のいない子ども」のようなデータを、
孤児レコード(orphan)と呼びます。
FOREIGN KEY を張っておけば、
親がいないIDを子テーブルに入れようとするとエラー
親を消そうとしたとき、子が残っていればエラー(または連動削除)
という形で、DBが整合性を守ってくれます。
SQLiteでは「アプリ側で気をつける」ことが多かったかもしれませんが、
MySQLでは「DBに任せる」方向にシフトできます。
Day5 前半のまとめ
PRIMARY KEY は「行を一意に識別するための代表カラム(または組み合わせ)」であり、NULL不可・重複不可という性質を持つ。MySQLでは、内部用の連番ID(INT AUTO_INCREMENT)をPRIMARY KEYにするのが基本で、メールアドレスや会員番号のような“意味のある値”を主キーにしないことで、セキュリティと運用の柔軟性を確保できる。
複合PRIMARY KEYは、「user_id+key」のように2つ以上のカラムの組み合わせで1行を一意にする設計で、何をもって“1行”とみなすかを考えるときの選択肢になるが、まずは単一のAUTO_INCREMENT IDを基本形として押さえておけばよい。
FOREIGN KEY は「このカラムの値は、必ず別テーブルの特定カラムに存在していなければならない」という制約であり、存在しないユーザーIDを持つ注文などの“おかしな参照”や“孤児データ”をDBレベルで防いでくれる。
SQLiteではFOREIGN KEYがデフォルト無効だったり、アプリ側でチェックしていたりすることが多かったのに対し、MySQL+InnoDBではFOREIGN KEYを前提に設計し、DBに整合性チェックを任せる文化が強く、これが品質とセキュリティの大きな支えになる。
後半では、
実際にPRIMARY KEY+FOREIGN KEY付きのテーブルを作り、
「存在しないIDをINSERTしようとするとどうなるか」「親を消そうとするとどうなるか」を
具体的なクエリで確認しながら、制約の“効き方”を体で覚えていきます。
