나만의 기록을 위한 예제와 포스팅입니다!
복합키를 참조하는 외래키에 NULL이 있다면 부모에 없는 값이 들어갈 수 있다.
데이터를 확인하다 보니 기본키를 참조하는 외래키에 부모에 없는 값이 들어가 있다.
내 부족한 검색 능력으로는 관련된 글을 쉽게 찾을 수 없기 때문에 기록을 위해 적어본다.
우선 기본키(PK)는 2개 이상의 컬럼을 갖는 복합키이며, 이를 참조하는 테이블에 부모에 없는 값이 들어갔다.
간단하게 말하자면 부모 테이블은 두 컬럼만 가지며 그 두 컬럼이 기본키로 되어 있고 (1, 'a') 라는 1개 로우만 가지고 있을 때, 자식의 외래키는 (null, '가나다') 이런 데이터가 있었다.
데이터를 본 순간 나는 아래와 같은 생각을 하게 됐다.
1. 왜 부모에 없는 값이 들어갔지?
2. 아니 애초에 참조된 부모 컬럼은 PK였고 PK는 NULL이 못 들어가잖아!?
3. DBMS에 이런 버그가 있다고? 에이 내 환경이 문제겠지..........
4. 뭐야 관련된 글도 별로 없어! 나 DBMS에 대한 버그 찾았다!!
[4]번은 조금 오버했지만, 저런 생각을 0.01초는 했다...
간단한 ERD와 상황을 재연해 보자!
아래는 학번과 강좌코드를 기본키로 갖는 수강 테이블과 이를 FK로 가지면서 nullable한 출석 테이블이 있다.
( 컬럼의 옵션이 nullable한 게 주요 옵션이며 포인트다.)
부모에는 (1, A01) 값만 들어갔는데, 이를 참조하는 컬럼에서 null과 말도 안 되는
아래 참고 코드를 보면 알겠지만 외래키 제약조건을 걸었음에도 부모에 없는 값이 들어갔다(?)
부모를 참조하는 두 컬럼에 값이 존재하면서, 부모에 없는 값을 삽입할 때 FK 무결성 에러 발생(당연한 소리?)하는 경우를 제외하고는 한 컬럼에 NULL 있는 경우 FK 무결성 검증을 무시한다.
-- PostgreSQL 13.7
-- 수강 테이블
CREATE TABLE enrollment
(
student_id Integer NOT NULL,
class_code Character varying(5) NOT NULL
);
ALTER TABLE enrollment ADD CONSTRAINT pk_enrollment PRIMARY KEY (student_id,class_code);
-- 출석 테이블
CREATE TABLE attendance
(
num Integer NOT NULL,
student_id Integer,
class_code Character varying(5),
enter_time Timestamp(3) NOT NULL
);
ALTER TABLE attendance ADD CONSTRAINT pk_attendance PRIMARY KEY (num);
-- FK 생성
ALTER TABLE attendance ADD CONSTRAINT enrollment_atendance FOREIGN KEY (student_id, class_code) REFERENCES enrollment (student_id, class_code) ON DELETE CASCADE ON UPDATE NO ACTION;
-- INSERT DATA
INSERT INTO enrollment(student_id, class_code) VALUES (1, 'A01');
INSERT INTO attendance(num, student_id, class_code, enter_time) VALUES
(1, 1, 'A01', '2024-02-15 13:00:00')
, (2, 1, NULL, '2024-02-15 14:00:00')
, (3, NULL, 'A01', '2024-02-15 16:00:00')
, (4, NULL, NULL, '2024-02-15 17:00:00')
, (5, NULL, '가나다', '2024-02-15 18:00:00')
, (6, 999, null, '2024-02-15 19:00:00');
우선 왜 이렇게 만들었는지에 대한 속 시원한 글은 찾지 못했다.
그래서 내 결론은 그냥 이렇게 만들었구나 싶다...
PostgreSQL에 한해서는 FK를 만들 때 옵션을 줄 수 있다.
A value inserted into the referencing column(s) is matched against the values of the referenced table and referenced columns using the given match type. There are three match types: MATCH FULL, MATCH PARTIAL, and MATCH SIMPLE (which is the default). MATCH FULL will not allow one column of a multicolumn foreign key to be null unless all foreign key columns are null; if they are all null, the row is not required to have a match in the referenced table. MATCH SIMPLE allows any of the foreign key columns to be null; if any of them are null, the row is not required to have a match in the referenced table. MATCH PARTIAL is not yet implemented. (Of course, NOT NULL constraints can be applied to the referencing column(s) to prevent these cases from arising.)
PostgreSQL FK 관련 도큐먼트
MATCH FULL 옵션을 사용하면
하지만 MSSQL에서는 관련 옵션이 없었다. 관련된 부분에 대한 도큐먼트는 아래와 같다.
null 옵션을 가진 컬럼에 대한 해결방법을 찾고 싶었지만 없었으며, MSSQL에서는 관련 컬럼을 NOT NULL로 지정해서 해결한다.
FOREIGN KEY 제약 조건의 열에 NULL 외의 다른 값을 입력한 경우에는 그 값이 참조되는 열에 있어야 합니다. 그렇지 않으면 외래 키 위반 오류 메시지가 반환됩니다. 복합 외래 키 제약 조건의 모든 값을 확인하려면 참여하는 모든 열에 NOT NULL을 지정합니다.
MSSQL FK 관련 도큐먼트
그렇다면 MATCH SIMPLE 옵션을 가진 FK에 CASCADE를 걸고 삭제를 하면 데이터는 어떻게 될까?
무언가 당연하겠지만 결과는, 모두 값이 있는 num 1번만 삭제되었다.
![]() | ![]() |
---|
물론 해당 경우가 나오도록 설계하는 케이스는 많이 없을 거라 생각한다. 공부가 부족해 위와 같은 현상을 겪으면서 당황했으며 관련 글도 쉽게 찾을 수 없어 놀랐다.(찾으면서 이상한 댓글도 많이 봤다.. 그래서 DBMS의 오류(?) 이딴 생각을 하게 됐...)
또, MATCH PARTIAL 옵션에 대해서는 인덱스를 탐색하여 검증하는 방식이 두 번째 컬럼부터는 비효율적이어서 아직 구현되지 않은 걸까? 라는 짧은 생각을 해본다.
원인에 대해서는 관련 글이나 설명이 있다면 알려주세요.....
현상만으로 보면 관찰하신 내용이 틀리지 않습니다.
분명히 참조 관계를 맺었는데 부모 테이블에 없는 값이 들어갔으니까요.
그래서 "복합키를 참조하는 외래키에 NULL이 있다면 부모에 없는 값이 들어갈 수 있다" 라고 결론지으셨는데, 사실 옳은 결론은 아닙니다.
선택적 참조 관계에 대해 어느 정도를 이해하고 계실까요.. 설명의 시작 점을 찾기 조금 어려운데요.
다만.. 이 문제를 단일키가 아니라 복합키에서 찾으신 것을 보면, 단일 참조키가 NULL을 허용했을 때 여기에 NULL이 들어가는 것은 문제로 보지 않는 것 같습니다. 만약, 그렇다면 선택적 참조 관계에 대한 기본적 이해를 갖고 계시다고 볼 수 있을 테니, 부족한 부분만 설명을 이어 갈까 합니다.
먼저.. 예시로 든 수강과 출석이라는 관계는 사실 이 현상을 설명하기에 적절한 예는 아닙니다. 출석에서 수강을 참조할 때 이 관계는 보통 필수 관계이지 선택적 관계는 아니기 때문입니다. 실제 설계에서는 두 컬럼 (student_id, class_code) 모두 NULL을 허용하지 않도록 설계하겠죠.
하지만, 본문의 맥락에서 그게 중요한 것은 아니고 "복합키를 참조했을 때 참조키에 NULL을 허용하게 한 상황", 바로 이 때 벌어지는 한 가지 현상에 대한 것이겠죠.
여기서 다시 한 번 확인!
student_id와 class_code 둘 다 NULL인 값을 attendance 테이블에 INSERT 할 때, 이때도 참조키 오류가 발생하지 않고 들어가는데요. 이것도 문제라고 생각하시나요? 혹시나 이것도 이상하다고 하시면, 선택적 관계가 무엇이고 왜 필요한지 확인하는 것이 먼저인데요. 위에 전제한 것처럼 이것은 이미 이해하고 계시리라 생각하겠습니다.
그렇다면 문제가 되는 것은 이제 한 가지입니다.
왜 두 컬럼 중 하나의 컬럼만 NULL일 때도 INSERT가 성공하는가?
그래서 내리신 결론은 "복합키를 참조하는 외래키에 NULL이 있다면 부모에 없는 값이 들어갈 수 있다" 인 것 같습니다.
그런데 올바른 결론은 "외래키가 NULL을 허용한다면 부모 테이블을 참조하지 않을 수 있다" 입니다.
첫 째, 복합키와 단일키는 이 문제와 관련이 없습니다.
둘 째, 부모에 없는 값이 들어간 것이 아니라 부모 테이블을 참조하지 않는 값이 들어간 것입니다. 바꿔말하면 "참조 관계에 있지만 부모 테이블에 없는 값이 들어간 것"이 아니라, "참조 관계가 성립하지 않는 종류의 값이 들어간 것"입니다.
이게 무슨 뜻일까요?
attendance 테이블이 enrollment 테이블을 참조할 때 참조키가 (단일키이든 복합키이든) NULL을 허용하도록 설계했기 때문에, 두 테이블의 참조 관계는 "선택적"이 됩니다. 즉, enrollment 테이블을 참조하지 않는 행(row)도 attendance 테이블에 넣을 수 있게 한다는 뜻입니다.
하지만 선택적이더라도 두 컬럼 (student_id & class_code) 모두에 값이 들어 있다면, 이 값의 조합은 반드시 enrollment 테이블에 딱 1개의 행으로 존재해야 합니다.
자, 여기가 본론입니다.
두 컬럼 중 하나가 NULL이라면? 결론부터 얘기하면.. 이 때는 참조 관계가 성립하지 않습니다. 그런 값의 조합은 enrollment 테이블에서 1개의 행을 찾게 해주는 기본키 또는 유일키가 될 수 없기 때문입니다. 더구나, 이 예제에서는 유일키가 아니라 기본키를 참조하는 참조키이기 때문에.. 애초에 NULL이 포함된 조합으로는 enrollment에 존재하는 것 자체가 불가능한 값이죠.
바꿔 말하면, RDB의 참조 관계는 참조키의 값으로 부모 테이블을 검색했을 때 특정 1개의 행을 식별(= 키)할 수 있을 때만 그 관계가 성립합니다. 그런데 NULL이 섞이면 그런 행이 부모 테이블에 1개만 존재하는 것을 보장할 수 없기 때문에 참조 관계가 아닌 것으로 판단하는 겁니다.
이해하는데 필요한 배경 지식이 하나 더 있는데.. 바로 NULL 값의 특성입니다.
NULL은 어떤 값과 조합해도 그 결과가 NULL이 되고, NULL은 NULL과 비교할 수 없다는 사실입니다.
간단한 예로 1 + NULL은 NULL 입니다. CONCAT(NULL, 'abc') 역시 NULL인데.. 어쨌든 NULL을 다른 값과 연산한 결과는 NULL이고, 마찬가지로 NULL인 컬럼을 다른 컬럼과 조합하면 그 결과 역시 NULL로 평가합니다.
그런데 NULL은 NULL과 같지도 않고 다르지도 않습니다. NULL = NULL도 false이고, NULL <> NULL도 false 라는 뜻입니다.
따라서, 어떤 테이블에서 복합키 컬럼에 NULL이 포함되면.. 그 행은 어떤 다른 행과도 비교가 불가능하고, 이런 이유에서라도 참조 관계가 성립하지 않겠죠.
글이 길어졌는데, 이해하시는데 도움 되셨으면 좋겠습니다.