Oracleデータベースにおける制約(Constraint)には、いくつかの状態設定項目があり、これらが混同されがちです。主な項目は以下の通りです:
- ENABLE/DISABLE - 制約の有効/無効状態
- VALIDATE/INVALIDATE - 既存データの検証状態
- DEFERRABLE/NOT DEFERRABLE - 制約チェックの遅延可否
これらの状態設定の違いを明確にするために、具体的な実験を通じて確認してみましょう。以下の実験はOracle 12.2.0.1環境で実行しました。
-- テーブルの準備
DROP TABLE sample_constraint PURGE;
SELECT * FROM USER_CONSTRAINTS WHERE table_name='SAMPLE_CONSTRAINT';
-- 1. ENABLE/DISABLEの検証
CREATE TABLE sample_constraint (
record_id NUMBER CONSTRAINT pk_record_id CHECK(record_id IS NOT NULL) DISABLE NOVALIDATE,
item_name VARCHAR2(50) CONSTRAINT nn_item_name CHECK(item_name IS NOT NULL)
);
-- 無効状態の制約に対するデータ挿入
DECLARE
counter NUMBER;
BEGIN
FOR counter IN 1..100 LOOP
INSERT INTO sample_constraint(record_id, item_name)
VALUES(NULL, 'ITEM-' || TO_CHAR(counter));
END LOOP;
COMMIT;
END;
-- 制約を有効化
ALTER TABLE sample_constraint MODIFY CONSTRAINT pk_record_id ENABLE;
-- 制約を再度無効化
ALTER TABLE sample_constraint MODIFY CONSTRAINT pk_record_id DISABLE;
-- 結論:ENABLE/DISABLEは制約が実際に機能するかどうかを制御する。DISABLE状態では制約は完全に無視される。
-- 2. DEFERRABLEの検証
-- テーブルをクリア
TRUNCATE TABLE sample_constraint;
-- 既存制約を削除(DEFERRABLEに変更するには再作成が必要)
ALTER TABLE sample_constraint DROP CONSTRAINT nn_item_name;
ALTER TABLE sample_constraint ADD CONSTRAINT nn_item_name
CHECK(item_name IS NOT NULL) DEFERRABLE INITIALLY DEFERRED ENABLE VALIDATE;
-- 遅延検証制約でのデータ操作
DECLARE
counter NUMBER;
BEGIN
FOR counter IN 1..100 LOOP
INSERT INTO sample_constraint(record_id, item_name)
VALUES(counter, CASE WHEN counter = 50 THEN NULL ELSE 'ITEM-' || counter END);
END LOOP;
-- ここではエラーが発生せず、COMMIT時に検証が行われる
COMMIT;
END;
-- 結論:DEFERRABLE INITIALLY DEFERREDの場合、制約チェックはCOMMIT時に行われる。
-- INITIALLY IMMEDIATEの場合は各文の実行時にチェックされるが、違反があればトランザクション全体がロールバックされる。
-- 3. VALIDATE/NOVALIDATEの検証
ALTER TABLE sample_constraint MODIFY CONSTRAINT pk_record_id DISABLE;
-- 無効状態で無効データを挿入
DECLARE
counter NUMBER;
BEGIN
FOR counter IN 1..10 LOOP
INSERT INTO sample_constraint(record_id, item_name)
VALUES(NULL, 'TEST-' || counter);
END LOOP;
COMMIT;
END;
-- NOVALIDATEで制約を有効化
ALTER TABLE sample_constraint MODIFY CONSTRAINT pk_record_id ENABLE NOVALIDATE;
-- NOVALIDATEは既存データを検証しないことを確認
-- 新しいデータ操作では制約が有効になることを確認
UPDATE sample_constraint SET record_id = NULL WHERE record_id IS NOT NULL;
-- このUPDATEは制約違反となる
これらの実験から以下の点が明確になりました:
- ENABLE/DISABLEは制約が機能するかどうかを完全に決定し、ENABLE状態でなければ他の設定は意味を持ちません。
- VALIDATE/INVALIDATE - VALIDATEは既存データと新規データの両方を検証しますが、NOVALIDATEは新規データと変更データのみを検証対象とします。
- DEFERRABLE/NOT DEFERRABLE - これは単に制約チェックのタイミングを制御するだけであり、最終的にはチェックが実施されます。DEFERRABLE制約は文レベルまたはトランザクションレベルでの検証を可能にします。
実際の開発現場では、テーブルや列に制約を一切設定しない設計を採用するケースがあります。
このアプローチの利点は、テーブル設計の時間を節約し、仕様変更時の修正工数を削減できる点です。
しかし、欠点としては、結局のところアプリケーション側でデータ整合性を保証するロジックを実装する必要があり、さらにすべてのアプリケーションで一貫したチェックを実装しなければなりません。これにより、結果的により多くの開発時間と保守工数がかかる可能性があります。
このような設計習慣は、一般的に好ましくありません。設計段階では時間を節約できるかもしれませんが、最終的には必ずその代償を支払うことになります。データベースが制約機能を提供しているのには理由があるのです。
「急がば回れ」ということわざがあるように、データベースレベルで制約を適切に実装することで、すべてのアプリケーションはデータの信頼性を確保できます。これは特に高可用性が要求される本番システムで重要です。
また、データロード時のパフォーマンスが低下することを懸念する場合、OracleにはDEFERRABLEやENABLE/DISABLEといった妥協点を提供する機能があります。
一般的な手法は、データロード前に制約をDISABLEにし、ロード完了後に再度ENABLEにすることです(この操作はOLAPやDWH環境で特に一般的です)。一意制約(UK)や主キー制約(PK)の場合は、KEEP INDEXオプションを使用できます。
例:
CREATE TABLE sample_unique_table (
product_code VARCHAR2(20) CONSTRAINT uk_product_code UNIQUE
CONSTRAINT nn_product_code CHECK(product_code IS NOT NULL)
);
-- サンプルデータの挿入
BEGIN
FOR i IN 1..10 LOOP
INSERT INTO sample_unique_table(product_code)
VALUES('PRD-' || LPAD(i, 3, '0'));
END LOOP;
COMMIT;
END;
-- 制約を無効化しつつインデックスを保持
ALTER TABLE sample_unique_table MODIFY CONSTRAINT uk_product_code DISABLE KEEP INDEX;
-- インデックスは制約の状態に関係なく利用可能
SELECT * FROM sample_unique_table WHERE product_code = 'PRD-001';
-- 制約の再有効化は簡単に実行可能
ALTER TABLE sample_unique_table MODIFY CONSTRAINT uk_product_code ENABLE;
この方法により、制約が無効な状態でもインデックスを活用したクエリ実行が可能となり、制約の再有効化も簡単に行えます。