外部キー制約、ちゃんと「戦略的」に貼れてる?:PostgreSQLで学ぶ整合性の守り方
やあ。最近、データベースのパフォーマンスチューニングの相談を受けることが増えたんだけど、ふと気づいたことがあるんだ。「外部キー制約(Foreign Key)」を単なる「データの紐付けツール」だと思っていないかな?
確かに、外部キーはデータの整合性を保つための最強のガードレールだ。でも、それをどう設定するか、あるいは「あえて貼らない」という選択をするとき、そこにDBエンジニアとしての設計思想が宿る。今日は、実務で絶対に知っておくべき外部キーの作法について、少し深い話をしよう。
—
1. 「守る」ための設計:ON DELETEとON UPDATEの使い分け
外部キー制約の醍醐味は、親レコードが消されたときにどう振る舞うかを定義できることだ。ここを適当に決めると、後々データが整合性を失って大惨事になる。
よくあるユースケースで考えてみよう。
- CASCADE(連鎖削除):
親がいなくなったら子も不要。例えば「ブログ記事」が削除されたら、それに紐づく「コメント」も消えていい、という場合だ。
- SET NULL:
親は消えるけど、子の記録は残したいケース。「退職した社員」が作成した「タスク」を誰がやったか残しておきたいなら、`user_id` をNULLにするのが正解だ。
- RESTRICT / NO ACTION:
「まだ参照されているから削除させない」。これがデフォルトだけど、実は一番堅実だ。迂闊に消してほしくない重要なデータには、あえてこれを明示しておくのがプロの慎重さだね。
ALTER TABLE comments
ADD CONSTRAINT fk_post
FOREIGN KEY (post_id) REFERENCES posts(id)
ON DELETE CASCADE;
—
2. 見落とされがちな「インデックス」の罠
ここからが本題だ。現場のレビューで僕が必ずチェックするのが、「外部キーカラムにインデックスを貼っているか」という点。
PostgreSQLは、外部キー制約そのものには自動でインデックスを貼ってくれない。これがなぜ問題かというと、親テーブルのデータを削除したり更新したりするとき、PostgreSQLは「このIDを参照している子テーブルのレコードがないか」をチェックするために、子テーブルを全スキャン(Seq Scan)し始めるんだ。
親テーブルのデータを消すたびに全テーブルがスキャンされる……想像しただけで背筋が凍るだろう?
原則:外部キーカラムには、基本的にインデックスを貼る。
もしクエリの実行計画で `Seq Scan` が出ていたら、まずはここを疑うべきだ。ただ、頻繁に参照・更新が発生するテーブルなら、インデックスの更新コスト(書き込み負荷)とのトレードオフになる。このバランスを見極めるのが、データベースエンジニアの腕の見せ所だよ。
—
3. 実務で使うときの「ちょっとした裏技」
大規模なシステムになると、外部キーを貼ることで逆に運用が辛くなることもある。「論理削除」を多用するシステムだと、外部キーの制約とバッティングしてデッドロックが多発したりね。
そんなときは、以下のポイントを思い出してほしい。
- NOT VALID を活用する:
巨大な既存テーブルに後付けで外部キーを貼ると、テーブル全体がロックされてシステムが止まってしまう。まずは `NOT VALID` で制約を追加してロックを最小限にし、その後に `VALIDATE CONSTRAINT` で時間をかけて検証する。これだけでリリース時のヒヤヒヤが激減するはずだ。
— テーブルをロックせずに制約を追加
ALTER TABLE orders ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
— 後からバックグラウンドで検証
ALTER TABLE orders VALIDATE CONSTRAINT fk_user;
—
最後に:制約は「枷」ではなく「武器」だ
新人エンジニアの頃は「外部キーがあるとデータの投入順序を気にしなきゃいけないから面倒だな」と思うかもしれない。でも、データが壊れたときに復旧させるコストと、制約を設計する手間を天秤にかけてみてほしい。
制約を適切に設計することは、将来の自分(や、深夜に障害対応で叩き起こされる同僚)への一番のプレゼントになる。
データベースは嘘をつかない。君が正しく設計すれば、データベースは一生懸命君のデータを守ってくれる。ぜひ、明日のコードから意識してみてくれ。
また何か疑問があれば、いつでも聞いてくれよ。エンジニア同士、一緒にいいシステムを作っていこう。
コメント