制約の内部:参照整合性・チェック制約の実行機構
外部キーやUNIQUEが「いつ」評価されるかを知れば、謎の制約違反やデッドロック、循環参照INSERTの行き詰まりを設計段階で回避できます。制約が内部でどう実行されるかを理屈から押さえましょう。
- 1.外部キーは内部的にトリガ/システムトリガとして実装され、子表のINSERT・UPDATE時に親キーの存在を確認し、親表のUPDATE・DELETE時にカスケード等のアクションを発火する。検証は親キー側の索引を引いて行うため、参照先には索引が要る。
- 2.UNIQUE制約は専用の一意索引で実装され、挿入される索引エントリの重複を索引探索で検出する。NULLは「不明」なので多くのRDBで重複扱いされず、複数行が許される。
- 3.制約は既定では文単位(statement-level)で即時評価されるが、DEFERRABLE INITIALLY DEFERRED 指定でコミット時まで評価を遅延でき、循環参照や順序依存のINSERTを成立させられる。
制約は「宣言」だが実体は「実行コード」
FOREIGN KEY や CHECK、UNIQUE は宣言的に書きますが、DBMS 内部ではそれぞれ特定のタイミングで走る検証コードに変換されます。重要なのは「制約がいつ評価されるか」で、これを把握していないと、正しいはずの一括 INSERT が中途で失敗したり、無害に見える DELETE が連鎖して大量行を消したり、循環参照のあるデータを投入できずに行き詰まったりします。本稿では参照整合性・チェック・一意・遅延評価の4つを、内部の実行機構として分解します。
制約の意味(何を禁止するか)はSQL標準で決まっていますが、実務でのつまずきはほぼすべて評価タイミングに起因します。行単位か文単位か、即時かコミット時か――この軸で各制約を整理すると、エラーもデッドロックも予測可能になります。
外部キー:内部トリガとしての実装
参照整合性制約(referential integrity)は、子表(参照する側)の外部キー列の値が、親表(参照される側)の候補キーに必ず存在することを保証します。多くのRDB(PostgreSQL、Oracle、InnoDBなど)はこれをシステムトリガ――ユーザーには見えない内部トリガ――として実装します。発火点は2方向あります。
| イベント | 発火する側 | 内部で行う検証 |
|---|---|---|
| 子表に INSERT / 子の外部キー列を UPDATE | 子表側トリガ | 新しい外部キー値で親表の候補キー索引を探索し、一致行が無ければ違反 |
| 親表の候補キーを UPDATE / 親行を DELETE | 親表側トリガ | その親キーを参照する子行を検索し、参照アクション(NO ACTION/CASCADE 等)を実行 |
ポイントは、検証が親表の候補キー側の索引探索で行われることです。子に行が入るたびに親キーの存在確認を行うため、参照先(親の候補キー)には索引が必須で、これは主キーや UNIQUE が自動的に張る一意索引で満たされます。一方、親の DELETE 側では「その親を指す子行」を探す必要があり、子表の外部キー列に索引が無いと全表走査になります。多くのRDBが子側の外部キー索引を自動作成しないため(InnoDB は作る)、親の削除・更新が遅い場合の典型原因がこれです。
子表 INSERT(fk=5) の検証(概念):
親表の候補キー索引で key=5 を探索
存在する → OK
存在しない → 参照整合性違反でこの文を中断
親表 DELETE(pk=5) の検証(概念):
子表で fk=5 を持つ行を検索(子側索引が無いと全表走査)
見つかった子行に対し ON DELETE のアクションを適用
カスケードと参照アクション
親側のUPDATE/DELETEで子行が宙に浮く(dangling reference)のを防ぐため、外部キーには参照アクションを指定します。これらは前述の親表側トリガが、見つかった子行に対して実行する処理です。
| アクション | 親 DELETE / UPDATE 時の子への処理 | 備考 |
|---|---|---|
| NO ACTION(既定) | 子行が残っていれば違反。文末(または遅延時はコミット時)に検査 | 検査を最後まで遅らせる点が RESTRICT と異なる |
| RESTRICT | 子行が残っていれば即座に違反。遅延できない | その操作の時点で打ち切る |
| CASCADE | 子行も連鎖的に DELETE / 子の外部キー値を新値に UPDATE | 連鎖はさらにその子へ波及しうる |
| SET NULL | 子の外部キー列を NULL に設定 | FK 列が NOT NULL だと失敗する |
| SET DEFAULT | 子の外部キー列を既定値に設定 | 既定値が親に存在しないと別の違反を生む |
CASCADE は再帰的に作用します。A を親とする B、B を親とする C があり全て ON DELETE CASCADE なら、A の1行削除が B・C の関連行をまとめて消します。これは内部的に「親トリガが子の DELETE を発行 → その DELETE がさらに孫のトリガを発火」という連鎖で、削除行数も発火回数も実行時まで確定しません。NO ACTION と RESTRICT の違いも内部的で、RESTRICT はそのアクションの実行時点で即チェックするのに対し、NO ACTION は検査を文末(遅延可能制約なら後述のコミット時)まで先送りします。この差は、同一文の中で子を消してから親を消すような操作が NO ACTION だと通る、という形で現れます。
ON DELETE SET NULL を指定しても、外部キー列が NOT NULL 制約を持っていれば、NULL 代入が NOT NULL 違反を起こして親削除そのものが失敗します。参照アクションは「別の制約検証を誘発する」点に注意が必要で、一つの制約の挙動を単独では決められません。
UNIQUE 制約:一意索引そのもの
一意制約(UNIQUE)と主キー(PRIMARY KEY)は、専用の一意索引によって実装されるのが普通です。制約と索引は概念上別物ですが、一意性の検証は「重複キーが既に索引に存在しないか」を索引探索で確かめる操作なので、RDBは制約宣言時に裏で一意索引を作成し、その索引のエントリ挿入時に重複を検出します。つまり UNIQUE 制約には索引依存があり、索引を介さずに一意性を保証する手段は実質ありません(→ クラスタ化インデックスと非クラスタ化インデックスの内部差、索引構造の詳細は B+Tree インデックスの内部構造)。
検証タイミングは行単位で、ある行の索引エントリを挿入する瞬間に、同じキーを持つ可視なエントリが無いかを確かめます。検出されれば一意制約違反です。NULL の扱いは三値論理に従い、多くのRDBでは NULL どうしは「等しいか不明」なので重複とみなされず、UNIQUE 列に複数の NULL を入れられます(→ 3値論理とNULLの意味論・落とし穴)。複合 UNIQUE では一部の列が NULL だと行全体が重複判定から外れる挙動が一般的です。
SQL標準・PostgreSQL・Oracle・MySQL の多くは UNIQUE 列に複数 NULL を許します(NULL は重複しない)。ただし PostgreSQL 15+ の UNIQUE NULLS NOT DISTINCT のように、NULL を重複扱いにするオプションもあります。SQL Server は逆に既定で NULL を1つしか許しません。「UNIQUE は NULL を何個まで許すか」は移植時の落とし穴なので、対象RDBの仕様を確認すること。
CHECK 制約:行単位の述語評価
CHECK 制約は、指定した述語(真偽を返す条件式)が各行について偽にならないことを要求します。評価は行のINSERT/UPDATE時に、その行の値だけを使って行われます。ここで三値論理が効きます。CHECK は「結果が FALSE のときだけ拒否」し、UNKNOWN(NULL を含む比較)は許容します。たとえば CHECK (price > 0) は price が NULL だと NULL > 0 = UNKNOWN となり、違反になりません。NULL を弾きたいなら別途 NOT NULL が要ります。
-- price=NULL は UNKNOWN なので CHECK を通過してしまう
CREATE TABLE item (
price numeric CHECK (price > 0) -- NULL は許される
);
-- NULL も弾くなら
CREATE TABLE item2 (
price numeric NOT NULL CHECK (price > 0)
);
標準的な CHECK は単一行・現在値のみを参照でき、他の行や他テーブルを参照する集約・サブクエリは(多くのRDBで)使えません。テーブル横断や複数行にまたがる不変条件は CHECK では表現しきれず、トリガやアサーション相当の仕組みが必要です。
即時評価と遅延評価(DEFERRABLE)
ここまでの検証は既定で即時(immediate)――各文の終わりまでに評価されます。しかし DEFERRABLE INITIALLY DEFERRED を指定すると、制約検証をトランザクションのコミット時まで遅延できます(主に外部キーで使う。標準SQLの機能で、PostgreSQL・Oracle が対応、MySQL/InnoDB は外部キー検査の遅延を基本サポートしません)。
これが効くのは循環参照や相互参照のケースです。表 A が表 B を参照し、B も A を参照するとき、即時評価では「どちらを先に INSERT しても相手がまだ無い」ため詰みます。遅延制約なら、両方を INSERT してからコミット直前に一括検証するので、途中の不整合が許容されます。
-- 相互参照を成立させる典型
ALTER TABLE a
ADD CONSTRAINT a_b_fk FOREIGN KEY (b_id) REFERENCES b(id)
DEFERRABLE INITIALLY DEFERRED;
BEGIN;
INSERT INTO a (id, b_id) VALUES (1, 10); -- b(10) はまだ無いが遅延中なのでOK
INSERT INTO b (id, a_id) VALUES (10, 1); -- ここで両者が揃う
COMMIT; -- ここで初めて両方向の参照整合性をまとめて検証
| 評価モード | 検証タイミング | 主な用途・特性 |
|---|---|---|
| 即時(既定 / NOT DEFERRABLE) | 各文の完了時 | 違反を早期に検出。エラー位置が特定しやすい |
| 遅延(DEFERRABLE INITIALLY DEFERRED) | COMMIT 時に一括 | 循環参照・順序依存INSERT・一括更新を成立させられる |
| DEFERRABLE INITIALLY IMMEDIATE | 既定は即時だが SET CONSTRAINTS で切替可 | 通常は即時、特定TxだけDEFERREDに動的変更 |
遅延制約は、違反がコミットの瞬間に初めて顕在化します。どの文が原因かが分かりにくく、トランザクション全体がロールバックされるため、大きな一括処理ほどやり直しコストが上がります。循環参照のように本当に必要な場合に限定し、安易な全制約遅延は避けるのが定石です。
ロック・MVCCとの関係
参照整合性の検証は、検証中に親行が消えたり一意キーが二重挿入されたりしないよう、同時実行制御と協調します。外部キー検証は子のINSERT時に親行へ共有ロック(行ロック)を取り、検証完了まで親が削除・更新されないようにするのが一般的です。これが、無関係に見える子表へのINSERTが親表の更新と競合して待機やデッドロックを生む原因になります。複数トランザクションが互いの親行を参照し合うと、ロック獲得順序の食い違いでデッドロックに至ります(→ ロック方式の前提は 2相ロックと直列化可能性、評価タイミングと分離レベルの関係は トランザクション分離レベル)。一意制約も同様で、同じキーを同時に挿入しようとした2トランザクションは、一方が他方のコミット/ロールバックを待ってから重複判定が確定します。
まとめ
- 外部キーは内部のシステムトリガとして、子のINSERT/UPDATE時に親キー存在を、親のDELETE/UPDATE時に参照アクション(CASCADE等)を実行する。検証は親の候補キー索引を引くため参照先には索引が要り、親削除の性能には子側の外部キー索引が効く。
- UNIQUE/PRIMARY KEY は一意索引で実装され、索引エントリ挿入時に重複を検出する。NULL は三値論理で重複扱いされないのが多数派で、複数 NULL を許す(RDBで差あり)。
- CHECK は行単位で述語を評価し、結果が
FALSEのときだけ拒否する。NULL を含むUNKNOWNは通すので、NULL 排除にはNOT NULLを併用する。 - 制約は既定で即時(文単位)評価だが、
DEFERRABLE INITIALLY DEFERREDでコミット時評価に遅延でき、循環参照や順序依存INSERTを成立させられる。代償として違反がコミット時に飛ぶ。 - 検証はロック・MVCCと協調するため、子のINSERTが親行へのロックを誘発し、想定外の待機・デッドロックの原因になりうる。
データベース Article
制約の内部:参照整合性・チェック制約の実行機構を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
データベース
比較で見る軸
難易度: advanced / カテゴリ: データベース / タグ数: 6
導入後に効く点
UNIQUE制約は専用の一意索引で実装され、挿入される索引エントリの重複を索引探索で検出する。NULLは「不明」なので多くのRDBで重複扱いされず、複数行が許される。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- データベース
- タグ数
- 6
判断チェックリスト
- 自社の用途が「データベース / 制約」に近いか確認する。
- 強みである「外部キーは内部的にトリガ/システムトリガとして実装され、子表のINSERT・UPDATE時に親キーの存在を確認し、親表のUPDATE・DELETE時にカスケード等のアクションを発火する。検証は親キー側の索引を引いて行うため、参照先には索引が要る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。