【図解】Oracle外部キー・チェック制約の書き方と違い|データ整合性入門

Oracle Master Silver

データベースに「存在しない顧客の注文」や「マイナスの価格」といった不正なデータが登録されてしまい、システムのバグ調査に追われた経験はありませんか?

Oracle Databaseにおいてデータの品質(整合性)を担保する最後の砦が「制約」です。特にOracle 外部キー(Foreign Key)とチェック制約(Check Constraint)は、テーブル設計の基礎でありながら、正しく理解していないとエラーやパフォーマンス低下の原因になります。

本記事では、これら2つの制約の違いや正しい書き方、運用時の注意点を、分かりやすい図解と実機で動くSQLを用いて解説します。

💰 【PR】Oracleエンジニアの市場価値、調べてみませんか?

Oracleのスキルは需要が高く、特定の資格や経験を持っていると年収が大幅にアップするケースがあります。まずはIT専門のエージェントで非公開求人をチェックしてみませんか?


結論:外部キーとチェック制約の使い分け(やることリスト)

忙しい方向けに、制約利用の要点をまとめました。

  • 外部キー制約 (FK)
    • 目的: 「親テーブル(マスタ)にある値しか許可しない」
    • 用途: 顧客ID、商品IDなどの紐付け。
    • 必須設定: 子テーブル側のFK列にはインデックスを作成すること(ロック競合防止)。
  • チェック制約 (Check)
    • 目的: 「値が特定の条件(範囲や形式)を満たすことを保証する」
    • 用途: 価格が正の数、ステータスが有効値(0か1)など。
    • 注意: 複雑すぎるビジネスロジックはアプリ側で制御するほうが変更容易。

1. 外部キー制約(Foreign Key)とは?

外部キー制約は、あるテーブル(子)の列の値が、別のテーブル(親)の主キーまたは一意キーに実在することを保証する仕組みです。これにより、リレーショナルデータベース(RDB)の根幹である「参照整合性」が維持されます。

特徴と仕組み(図解)

以下の図は、Customers(親)とOrders(子)の関係を示しています。

外部キーは「子から親への参照」を強制し、親データの削除時に子データをどう扱うか(連動削除など)を制御します。

       【親テーブル:Customers】                 【子テーブル:Orders】
    +-----------------------------+           +-----------------------------+
    | COLUMN NAME   | TYPE & KEY  |           | COLUMN NAME   | TYPE & KEY  |
    +---------------+-------------+           +---------------+-------------+
    | customer_id   | NUMBER (PK) |<----(※)--| customer_id   | NUMBER (FK) |
    | customer_name | VARCHAR2    |    参照    | order_id      | NUMBER (PK) |
    +-------+---------------------+           | order_date    | DATE        |
            |                                 +-----------------------------+
            |
            |  ★ ON DELETE CASCADE の場合
            +-------------------------------------> 親(ID=1)を削除すると...
                                                    子(ID=1)も自動削除される

    (※) ここにインデックスを作成するのがベストプラクティス
  • 参照整合性: 親にないID(例: 999)は子に登録できません(ORA-02291エラー)。
  • 連動削除: 親データ削除時の挙動(ON DELETE CASCADEなど)を定義できます。

実装・実行例(Oracle SQL)

ここでは図の構成通りにテーブルを作成します。

前提条件:

  • 実行ユーザーに CREATE TABLE 権限があること。
  • 親テーブルから先に作成すること。
-- 1. 親テーブル (customers) の作成
CREATE TABLE customers (
    customer_id NUMBER PRIMARY KEY,
    customer_name VARCHAR2(100)
);

-- 2. 子テーブル (orders) の作成
-- ON DELETE CASCADE: 親が消えたら子も自動削除する設定
CREATE TABLE orders (
    order_id NUMBER PRIMARY KEY,
    customer_id NUMBER,
    order_date DATE,
    CONSTRAINT fk_customer
        FOREIGN KEY (customer_id) 
        REFERENCES customers(customer_id)
        ON DELETE CASCADE
);

-- 3. 正常データの登録
INSERT INTO customers (customer_id, customer_name) VALUES (1, 'Tanaka');
INSERT INTO orders (order_id, customer_id, order_date) VALUES (101, 1, SYSDATE);
COMMIT;
SQL> SELECT * FROM customers;

CUSTOMER_ID CUSTOMER_NAME
----------- ---------------------------------
1 Tanaka

SQL> SELECT * FROM orders;

ORDER_ID CUSTOMER_ID ORDER_DAT
---------- ----------- ---------
101 1 30-DEC-25

ポイント: CONSTRAINT fk_customer のように制約名を明示することをお勧めします。エラー発生時にどの制約違反か即座に判別できるためです。

動作確認:エラーと連動削除

意図的に整合性を壊す操作を行い、Oracleがどう守ってくれるか確認します。

ケース1:存在しない親IDを指定

INSERT INTO orders (order_id, customer_id, order_date) VALUES (102, 999, SYSDATE);

結果:

SQL> INSERT INTO orders (order_id, customer_id, order_date) VALUES (102, 999, SYSDATE);
INSERT INTO orders (order_id, customer_id, order_date) VALUES (102, 999, SYSDATE)
*
ERROR at line 1:
ORA-02291: integrity constraint (USER1.FK_CUSTOMER) violated - parent key not found

ORA-02291: 整合性制約(…)に違反しました – 親キーがありません

→ 顧客ID 999 は customers テーブルに存在しないためブロックされました。

ケース2:親データの削除(カスケード削除)

DELETE FROM customers WHERE customer_id = 1;
-- 確認
SELECT * FROM orders WHERE customer_id = 1;

結果:

SQL> DELETE FROM customers WHERE customer_id = 1;

1 row deleted.

SQL> SELECT * FROM orders WHERE customer_id = 1;

no rows selected

レコードが選択されませんでした。

→ ON DELETE CASCADE により、関連する注文データも自動的に削除されました。

💰 【PR】Oracleエンジニアの市場価値、調べてみませんか?

Oracleのスキルは需要が高く、特定の資格や経験を持っていると年収が大幅にアップするケースがあります。まずはIT専門のエージェントで非公開求人をチェックしてみませんか?


2. チェック制約(Check Constraint)とは?

チェック制約は、列に格納される値が特定の条件式(True)を満たすことを強制する機能です。「マイナスの価格」や「未来の生年月日」といった論理的にあり得ないデータの混入を防ぎます。

特徴と仕組み(図解)

チェック制約は、データの入り口に立つ「門番」のようなイメージです。

             【テーブル:Products】
    +-----------------------------------------+
    | product_id  (PK)                        |
    +-----------------------------------------+
    | product_name                            |
    +-----------------------------------------+
    | price                                   |
    |   +---------------------------------+   |
    |   | [門番] CHECK (price > 0)        |   |
    |   |  ・入力値: 1000  → OK (通過)    |   |
    |   |  ・入力値: -500  → NG (ブロック)|   |
    |   +---------------------------------+   |
    +-----------------------------------------+
    | stock_quantity                          |
    |   +---------------------------------+   |
    |   | [門番] CHECK (stock >= 0)       |   |
    |   +---------------------------------+   |
    +-----------------------------------------+
  • 柔軟性: > 0, IN ('A', 'B') などSQLのWHERE句に近い条件を記述可能。
  • NULLの扱い: 条件式の結果が NULL になる場合(値が入っていない場合)、Oracleは「違反していない」とみなして通します(ここが落とし穴になりがちです)。

実装・実行例

商品テーブル products で、価格と在庫数がマイナスにならないよう制御します。

-- テーブル作成
CREATE TABLE products (
    product_id NUMBER PRIMARY KEY,
    product_name VARCHAR2(100),
    price NUMBER CONSTRAINT chk_price_pos CHECK (price > 0),
    stock_quantity NUMBER CONSTRAINT chk_stock_min CHECK (stock_quantity >= 0)
);

-- 正常データの登録
INSERT INTO products (product_id, product_name, price, stock_quantity)
VALUES (1, 'Laptop', 100000, 10);
COMMIT;
SQL> SELECT * FROM products;

PRODUCT_ID PRODUCT_NA PRICE STOCK_QUANTITY
---------- ---------- ---------- --------------
1 Laptop 100000 10

動作確認:違反データの挿入

INSERT INTO products (product_id, product_name, price, stock_quantity)
VALUES (2, 'Smartphone', -50000, 5);

結果:

SQL> INSERT INTO products (product_id, product_name, price, stock_quantity)
2 VALUES (2, 'Smartphone', -50000, 5);
INSERT INTO products (product_id, product_name, price, stock_quantity)
*
ERROR at line 1:
ORA-02290: check constraint (USER1.CHK_PRICE_POS) violated

ORA-02290: チェック制約(…)に違反しました

→ price > 0 の条件を満たさないためブロックされました。

応用:複数列にまたがるチェック制約

「基本給とボーナスの合計が一定額を超えてはいけない」といった、同一行内の複数列を使った条件も定義可能です。

CREATE TABLE employees (
    emp_id NUMBER PRIMARY KEY,
    salary NUMBER,
    bonus NUMBER,
    CONSTRAINT chk_total_comp CHECK (salary + bonus <= 100000)
);

3. 外部キーとチェック制約の違い

項目外部キー制約 (FK)チェック制約 (Check)
目的テーブル間の整合性維持値そのものの妥当性維持
参照先他のテーブル(親)の列同一テーブル内の列
検証内容「親に存在するか?」「条件式を満たすか?」
主な用途マスタ参照、親子関係金額範囲、区分値、形式チェック
NULLの扱い許可(ただしNOT NULL制約がない場合)許可(条件評価がUNKNOWNとなるため)

4. トラブルシューティング(よくあるエラー)

制約を設定すると発生する代表的なORAエラーと、その対処法です。

エラーコードメッセージ要約原因対処法
ORA-02291親キーがありません子テーブルに登録しようとしたFK値が、親テーブルに存在しない。親テーブルに該当データを登録するか、入力値を修正する。
ORA-02292子レコードがあります親テーブルのデータを削除しようとしたが、そのIDを使っている子データが存在する。子データを先に削除するか、FK定義時に ON DELETE CASCADE を検討する。
ORA-02290チェック制約に違反しましたCHECK条件を満たさない値を登録・更新しようとした。入力データがビジネスルール(条件式)に合致しているか確認する。

5. 運用・セキュリティ上の注意点とベストプラクティス

Oracle Databaseを安定稼働させるために、以下の点に注意してください。

1. 外部キー列には必ずインデックスを貼る

これはパフォーマンス上の最重要事項です。

外部キー列にインデックスがないと、親テーブルの主キーを更新・削除した際、子テーブル全体に対してテーブルロックが発生する場合があります。システム全体の遅延原因となるため、FK列にはインデックスを作成する習慣をつけましょう。

-- FK列へのインデックス作成例
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

2. ビジネスロジックの書きすぎに注意

チェック制約は強力ですが、あまりに複雑なルール(例:消費税計算を含んだ金額チェックなど)をDB側に持たせると、仕様変更時の修正が困難になります。

  • 普遍的なルール(価格は正の数、年月日は有効な日付)→ DBの制約
  • 変わりやすいルール(キャンペーン価格の適用など)→ アプリケーションという使い分けが推奨されます。

3. データロード時の一時無効化

大量データをロードする際、制約チェックが処理時間のボトルネックになることがあります。一時的に制約を DISABLE(無効化)し、ロード後に ENABLE(有効化・検証)することで時間を短縮できる場合があります。

リスク: 無効化中に不正データが入る可能性があります。本番環境では慎重に行ってください。


FAQ:よくある質問

Q1. 外部キー制約をつけると遅くなりますか?

A. 挿入・更新時に親テーブルの参照チェックが走るため、制約なしに比べればごくわずかなオーバーヘッドはあります。しかし、データの不整合を後から修正するコストに比べれば無視できる範囲です。前述の通り、FK列へのインデックス作成でロック問題は回避してください。

Q2. 既にデータが入っているテーブルに制約を追加できますか?

A. 可能です。ただし、既存データが制約に違反している場合、ALTER TABLE ... ADD CONSTRAINT は失敗します。その場合、先に不正データを修正するか、NOVALIDATE オプションを使用して「新規データのみチェックする」設定にする必要があります。

Q3. 親テーブルを削除(DROP)できません。

A. 子テーブルから参照されている親テーブルは削除できません(ORA-02449)。DROP TABLE customers CASCADE CONSTRAINTS; のようにオプションを付けると、制約ごと強制削除できますが、影響範囲が広いため注意が必要です。


まとめ

Oracleの外部キーとチェック制約は、データの品質を守るための基本機能です。

  1. 外部キーは、マスタデータとの紐付けを保証し、孤立データの発生を防ぐ。
  2. チェック制約は、異常値の混入を防ぎ、最低限のデータ品質をDB側で担保する。
  3. 外部キー列にはインデックスを作成し、ロック競合を防ぐ。
  4. エラー発生時は ORA-02291(親なし)ORA-02290(チェック違反) を確認する。

SQLでの実装は一度覚えてしまえば簡単です。まずは開発環境で実際にテーブルを作成し、わざとエラーを出して挙動を確認してみることをお勧めします。

本記事は Oracle Database 19c を対象に解説します(他バージョンは画面や既定値が異なる場合があります)。


[参考]
Oracle Databaseデータベース管理者ガイド 19c

💰 【PR】Oracleエンジニアの市場価値、調べてみませんか?

Oracleのスキルは需要が高く、特定の資格や経験を持っていると年収が大幅にアップするケースがあります。まずはIT専門のエージェントで非公開求人をチェックしてみませんか?

コメント

タイトルとURLをコピーしました