外鍵約束是關係型數據庫維護引用完整性的基石。Oracle 在外鍵定義中提供了三種刪除行為(`NO ACTION`、`CASCADE`、`SET NULL`),但原生不支援級聯更新。本文通過完整的 DDL/DML 示例,深度剖析三種刪除行為的語義差異、執行邏輯與性能影響,並給出基於「延遲約束 + 觸發器」的級聯更新實現方案及其局限性。
-- 班級表 (父表) CREATE TABLE tb_class ( id NUMBER NOT NULL, name VARCHAR2(50), CONSTRAINT pk_tb_class PRIMARY KEY (id) ); -- 學生表 (子表) CREATE TABLE tb_student ( id NUMBER NOT NULL, name VARCHAR2(50), class_id NUMBER, CONSTRAINT pk_tb_student PRIMARY KEY (id), CONSTRAINT fk_tb_student_class_id FOREIGN KEY (class_id) REFERENCES tb_class (id) ); -- 初始化數據 INSERT INTO tb_class (id, name) VALUES (1, '一班'); INSERT INTO tb_class (id, name) VALUES (2, '二班'); INSERT INTO tb_class (id, name) VALUES (3, '三班'); INSERT INTO tb_student (id, name, class_id) VALUES (1, '小明', 1); INSERT INTO tb_student (id, name, class_id) VALUES (2, '小剛', 1); INSERT INTO tb_student (id, name, class_id) VALUES (3, '小王', 1); INSERT INTO tb_student (id, name, class_id) VALUES (4, '二明', 2); INSERT INTO tb_student (id, name, class_id) VALUES (5, '二剛', 2); INSERT INTO tb_student (id, name, class_id) VALUES (6, '二王', 2); INSERT INTO tb_student (id, name, class_id) VALUES (7, '大明', 3); INSERT INTO tb_student (id, name, class_id) VALUES (8, '大剛', 3); INSERT INTO tb_student (id, name, class_id) VALUES (9, '大王', 3); COMMIT;
語義: 當刪除父表被引用行時,若子表存在引用該值的行,則拋出異常阻止刪除。
-- 嘗試刪除三班 (ID=3),子表有 3 條記錄引用 DELETE FROM tb_class WHERE id = 3; -- ORA-02292: integrity constraint (FK_TB_STUDENT_CLASS_ID) violated - child record found -- 正確順序:先刪子表,再刪父表 DELETE FROM tb_student WHERE class_id = 3; DELETE FROM tb_class WHERE id = 3;
語義: 刪除父表行時,將子表對應外鍵列設置為 NULL。前提:外鍵列必須允許 NULL (無 NOT NULL 約束)。
-- 修改外鍵為 SET NULL (Oracle 不支援 MODIFY CONSTRAINT,需重建) ALTER TABLE tb_student DROP CONSTRAINT fk_tb_student_class_id; ALTER TABLE tb_student ADD CONSTRAINT fk_tb_student_class_id FOREIGN KEY (class_id) REFERENCES tb_class (id) ON DELETE SET NULL; -- 刪除一班 (ID=1) DELETE FROM tb_class WHERE id = 1; -- 結果:學生表中 class_id=1 的 3 條記錄 class_id 變為 NULL
語義: 刪除父表行時,自動級聯刪除子表中所有引用該值的行。最高效但風險最大。
-- 修改外鍵為 CASCADE ALTER TABLE tb_student DROP CONSTRAINT fk_tb_student_class_id; ALTER TABLE tb_student ADD CONSTRAINT fk_tb_student_class_id FOREIGN KEY (class_id) REFERENCES tb_class (id) ON DELETE CASCADE; -- 刪除二班 (ID=2) DELETE FROM tb_class WHERE id = 2; -- 結果:班級表刪除 1 行,學生表自動級聯刪除 3 行 (ID 4,5,6)
Oracle 不支援 `ON UPDATE CASCADE`。若業務需修改父表主鍵並同步子表外鍵,需採用「延遲約束 + 行級觸發器」方案。
-- 1. 重建外鍵為 DEFERRABLE INITIALLY DEFERRED (提交時檢查)
ALTER TABLE tb_student DROP CONSTRAINT fk_tb_student_class_id;
ALTER TABLE tb_student ADD CONSTRAINT fk_tb_student_class_id
FOREIGN KEY (class_id) REFERENCES tb_class (id) ON DELETE CASCADE
DEFERRABLE INITIALLY DEFERRED;
-- 2. 創建父表主鍵更新觸發器
CREATE OR REPLACE TRIGGER tgr_tb_class_update
AFTER UPDATE OF id ON tb_class
FOR EACH ROW
BEGIN
IF :OLD.id <> :NEW.id THEN
UPDATE tb_student SET class_id = :NEW.id WHERE class_id = :OLD.id;
END IF;
END;
/
-- 3. 測試:將一班 ID 從 1 改為 10
UPDATE tb_class SET id = 10 WHERE id = 1;
COMMIT; -- 提交時延遲約束檢查通過
-- 結果:班級表 ID 變為 10,學生表 class_id 同步變為 10
-- 典型自引用表:組織架構、分類樹 CREATE TABLE tb_org ( id NUMBER PRIMARY KEY, name VARCHAR2(50), parent_id NUMBER REFERENCES tb_org(id) -- 自引用外鍵 ); -- 若採用上述觸發器方案: -- UPDATE tb_org SET id = 100 WHERE id = 1; -- 觸發器嘗試 UPDATE tb_org SET parent_id = 100 WHERE parent_id = 1; -- 但此 UPDATE 會再次觸發觸發器 -> 遞歸 -> ORA-04091: table is mutating -- 甚至導致無限循環或死鎖。
| 場景 | 推薦策略 | 關鍵動作 |
|---|---|---|
| 嚴格業務、需審計 | NO ACTION (默認) | 應用顯式處理子表;外鍵列建索引 |
| 子表允許「無歸屬」 | SET NULL | 外鍵列無 NOT NULL;業務處理 NULL 邏輯 |
| 主從強綁定、數據量可控 | CASCADE | 外鍵列建索引;避免多層級聯;知曉觸發器不觸發 |
| 需修改父表主鍵 (非自引用) | 延遲約束 + 觸發器 | DEFERRABLE INITIALLY DEFERRED;AFTER UPDATE FOR EACH ROW 觸發器 |
| 自引用表需修改主鍵 | 應用層批量更新 / 替代鍵 | 放棄數據庫級級聯更新 |
外鍵策略選型本質是「數據完整性保障」與「業務靈活性、性能」的權衡。建議在設計階段結合業務領域專家、架構師、DBA 聯合評估,並將決策記錄在數據字典中,避免上線後頻繁 `ALTER TABLE` 導致鎖衝突與應用故障。