외래 키
고친 사람 github-actions[bot]
외래 키는 한 테이블에 적힌 값이 다른 테이블의 어느 행을 가리키게 만듭니다. 가리킬 것이 없는 값은 저장 자체가 거절됩니다. 주문에 적힌 회원 번호가 있는 회원인지를 데이터베이스가 대신 확인해 줍니다.
쉽고 빠른 이해
외래 키는 한 테이블의 값이 다른 테이블에 있는 행을 가리키도록 데이터베이스에 맡겨 두는 약속입니다. 주문 테이블의 회원 번호 열에 외래 키를 걸면 없는 회원 번호로는 주문이 안 들어갑니다.
이 약속이 없으면 어느 회원의 것도 아닌 주문이 조용히 쌓입니다. 읽는 코드는 회원을 붙여 읽을 때마다 아무것도 안 붙는 경우를 따로 처리해야 합니다.
어떻게 도나:
- 가리키는 열과 가리켜지는 열을 짝지어 적어 둡니다
- 행을 넣거나 고칠 때 가리킨 값이 저쪽에 있는지 데이터베이스가 확인합니다
- 가리켜지던 행을 지울 때도 같은 검사를 해서 막거나 미리 정해 둔 처리를 합니다
대가는 쓰기가 무거워진다는 것입니다. 넣고 지울 때마다 다른 테이블을 확인하고 그동안 그 행을 붙잡아 둡니다. 테이블을 서버 여럿에 나눠 담으면 이 검사를 한 서버 안에서 끝낼 수 없어 외래 키를 아예 안 거는 선택을 하기도 합니다.
상세
회사 조직도를 떠올리면 쉽습니다. 사원 카드에는 소속 부서 번호가 적혀 있습니다. 그 번호는 조직도에 있는 부서 중 하나여야 합니다. 조직도에 없는 번호가 적힌 카드는 그 사람이 어디 소속인지 알려 주지 못합니다.
외래 키는 그런 번호 칸에 「조직도에 있는 번호만 적어라」는 규칙을 붙여 두는 일입니다. 규칙을 지키는 것은 사람이 아니라 저장하는 데이터베이스입니다.
외래 키는 한 테이블의 열 하나, 또는 열 몇 개를 묶은 것입니다. 그 열에 든 값은 다른 테이블에 있는 행을 가리켜야 한다는 규칙을 같이 답니다. 값을 담는 열과 그 열에 건 규칙을 합쳐 외래 키라고 부릅니다.
가리키는 테이블을 자식 테이블, 가리켜지는 테이블을 부모 테이블이라고 부릅니다. 주문이 회원을 가리키면 주문이 자식이고 회원이 부모입니다. 먼저 만든 테이블이 부모라는 뜻이 아닙니다. 가리키는 테이블이 자식, 가리켜지는 테이블이 부모입니다.
부모 테이블과 자식 테이블
회원을 담은 members 와 주문을 담은 orders 두 테이블이 있습니다. 주문마다 누가 주문했는지를
적어야 합니다. 회원 이름을 베껴 적지 않고 회원 번호만 적습니다.
members — 회원
| id | name |
|---|---|
| 1 | 유미 |
| 2 | 태호 |
orders — 주문
| id | member_id |
|---|---|
| 101 | 2 |
| 102 | 2 |
orders 의 member_id 가 외래 키입니다. 값 2 는 그 자체로는 뜻이 없습니다. members 에서
id 가 2 인 행을 가리켜야 뜻이 생깁니다.
flowchart TD
subgraph P["members · 부모 테이블"]
P1["id 1 · 유미"]
P2["id 2 · 태호"]
end
subgraph C["orders · 자식 테이블"]
C1["주문 101 · member_id 2"]
C2["주문 102 · member_id 2"]
end
C1 -->|가리킨다| P2
C2 -->|가리킨다| P2
한 부모 행을 여러 자식 행이 가리켜도 됩니다. 한 회원이 주문을 여러 번 하는 것이 그렇습니다. 반대로 한 자식 행이 두 부모 행을 동시에 가리킬 수는 없습니다. 열에 든 값은 하나뿐이기 때문입니다.
부모가 될 수 있는 열
부모 테이블에서 가리켜지는 열은 아무 열이나 되지 않습니다. 가리킨 값으로 행 하나가 정해져야 하므로, 그 열의 값은 행마다 서로 달라야 합니다. 값이 겹치면 어느 행을 가리킨 것인지 정할 수 없습니다.
그래서 부모가 되는 열은 대개 기본 키입니다. 기본 키는 테이블에서 행 하나를 콕 집어 가리키라고 미리 골라 둔 열입니다. 기본 키가 아니어도 값이 겹치지 않게 막아 둔 열, 곧 유일 제약이 걸린 열이면 부모가 될 수 있습니다.
아래는 SQL(Structured Query Language, 구조화 질의 언어)로 두 테이블을 만들고 주문을 넣어 본 것입니다. 두 번째 주문은 없는 회원 번호를 가리킵니다.
CREATE TABLE members (
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
member_id INTEGER REFERENCES members(id)
);
INSERT INTO orders VALUES (101, 2); -- 통과
INSERT INTO orders VALUES (103, 99); -- 거절
REFERENCES members(id) 한 줄이 외래 키 선언입니다. 이 줄을 적어 두면 다음부터는 잘못된 값이
들어오는 것을 애플리케이션 코드가 아니라 데이터베이스가 막습니다.
데이터베이스가 대신 하는 두 가지 검사
외래 키는 넣을 때 한 번, 지울 때 한 번 일합니다. 두 검사가 짝을 이루어야 가리키던 것이 사라지는 일이 안 생깁니다.
넣을 때 검사는 자식 테이블을 확인합니다. 자식 행을 넣거나 그 열의 값을 고칠 때, 그 값이 부모에 있는지 확인하고 없으면 거절합니다.
지울 때 검사는 부모 테이블을 확인합니다. 부모 행을 지우거나 가리켜지던 값을 고칠 때, 그 행을 가리키는 자식이 남아 있는지 확인합니다.
flowchart TD
subgraph A["넣거나 고칠 때"]
W["자식 행을 넣거나 고친다"] --> Q{"그 값이 부모에 있나"}
Q -->|없다| X["거절하고 안 넣는다"]
Q -->|있다| OK["저장한다"]
end
subgraph B["지울 때"]
D["부모 행을 지운다"] --> P{"가리키는 자식이 남았나"}
P -->|없다| DEL["그냥 지운다"]
P -->|있다| R["정해 둔 처리를 따른다"]
end
이 두 검사가 지키려는 성질이 참조 무결성입니다. 가리키는 값이 있으면 가리켜지는 행도 있다는 성질입니다. 외래 키와 참조 무결성은 같은 말이 아닙니다. 참조 무결성은 지키려는 성질입니다. 외래 키는 그 성질을 테이블 정의에 적어 데이터베이스에 맡기는 수단입니다.
부모 행을 지울 때의 처리 갈래
부모 행을 지울 때 그것을 가리키는 자식이 남아 있으면 어떻게 할지는 외래 키를 선언할 때 미리 고릅니다. 미리 안 고르고 그때그때 판단하게 두면 쓰는 코드마다 처리가 갈립니다.
| 고르는 처리 | 부모 행을 지우면 | SQL 표기 |
|---|---|---|
| 막기 | 지우기가 거절됩니다. 자식을 먼저 치워야 합니다 | RESTRICT |
| 연쇄 지우기 | 그 행을 가리키던 자식 행도 같이 지워집니다 | CASCADE |
| 널로 바꾸기 | 자식 행은 남고 가리키던 열이 널이 됩니다 | SET NULL |
| 미리 적어 둔 값으로 바꾸기 | 자식 행은 남고 그 값으로 채워집니다 | SET DEFAULT |
값이 없는 상태를 널이라고 합니다. 숫자 0 이나 빈 글자와 다른, 「아직 안 정해졌다」는
뜻입니다.
같은 막기를 NO ACTION 으로 적기도 합니다. 이름이 둘이지만 막는다는 결과는 같습니다.
고른 갈래는 외래 키 선언 뒤에 이어 적습니다. 앞의 orders 를 막기로 하려면 이렇게 씁니다.
member_id INTEGER REFERENCES members(id) ON DELETE RESTRICT
막기가 무난한 선택입니다. 지우는 일이 거절되면 무엇이 걸려 있는지를 지우려던 사람이 알게 됩니다. 연쇄 지우기는 한 줄만 지워도 다른 테이블의 여러 행이 함께 사라집니다. 어디까지 따라가는지를 알고 걸어야 합니다.
주문에 담긴 상품을 한 줄씩 적는 order_items 테이블이 orders 를 가리킨다고 해 봅니다.
orders 와 order_items 에 둘 다 연쇄 지우기를 걸어 두면 회원 한 행을 지우는 것이 두 단계로
번집니다.
flowchart TD
subgraph L1["1단계 · 지우려는 행"]
M["members · id 2"]
end
subgraph L2["2단계 · 그 회원을 가리키던 주문"]
O1["orders · 101"]
O2["orders · 102"]
end
subgraph L3["3단계 · 그 주문을 가리키던 항목"]
I1["order_items · 주문 101 의 항목"]
I2["order_items · 주문 102 의 항목"]
end
M -->|함께 지워진다| O1
M -->|함께 지워진다| O2
O1 -->|함께 지워진다| I1
O2 -->|함께 지워진다| I2
값을 고칠 때도 같은 갈래를 따로 고를 수 있습니다. 가리켜지던 값이 바뀌면 그것을 가리키던 자식의 값도 같이 바꿀지, 아니면 바꾸기를 막을지입니다.
외래 키 열이 비어 있을 때
외래 키 열에 값이 안 들어 있을 수도 있습니다. 그 상태가 앞 표에서 말한 널입니다. 외래 키 열이 널이면 가리키는 대상이 없습니다. 검사할 것이 없어 그대로 들어갑니다.
그래서 「반드시 누군가를 가리켜야 한다」를 바라면 외래 키만으로는 모자랍니다. 널을 못 넣게 막는 널 제약을 그 열에 같이 걸어야 합니다. 둘은 서로 다른 것을 막습니다. 널 제약은 널을 막고, 외래 키는 엉뚱한 값을 막습니다.
flowchart TD
V["자식 행의 외래 키 열에 값이 들어온다"] --> N{"값이 널인가"}
N -->|아니다| F{"그 값이 부모에 있나"}
F -->|있다| OK["저장한다"]
F -->|없다| NG["거절한다"]
N -->|널이다| G{"널 제약이 걸렸나"}
G -->|안 걸렸다| OK2["검사 없이 저장한다"]
G -->|걸렸다| NG2["거절한다"]
열 여러 개로 된 외래 키
부모가 되는 키가 열 하나가 아니라 여러 열을 묶은 것일 때도 있습니다. 여러 열을 묶어 행 하나를
식별하는 키가 복합 키입니다. 지점을 지역 region 과 지점 번호 code 두 열로 식별하는
테이블이 그런 경우입니다.
이때 자식 테이블도 같은 개수의 열을 묶어 선언합니다. 두 값이 한꺼번에 맞는 부모 행이 있어야 통과합니다. 열 하나씩 따로 맞추는 것이 아닙니다.
flowchart TD
subgraph PC["부모 테이블 · 복합 키"]
PK["region · code"]
end
subgraph CC["자식 테이블 · 외래 키"]
CK["region · code"]
end
CK -->|"두 열을 한 묶음으로 맞춘다"| PK
외래 키 검사에 드는 비용
검사는 거저가 아닙니다. 자식 행 하나를 넣을 때마다 부모 테이블에서 그 값을 찾아야 합니다. 특정 열의 값으로 행을 빨리 찾도록 곁에 만들어 두는 구조가 인덱스입니다. 기본 키나 유일 제약이 걸린 열에는 인덱스가 이미 있어서 이 조회는 대개 문제가 안 됩니다.
반대 방향은 그렇지 않습니다. 부모 행을 지울 때는 그것을 가리키는 자식이 있는지를 자식 테이블에서 찾아야 합니다. 그런데 자식 테이블의 외래 키 열에는 인덱스가 저절로 생기지 않습니다. 인덱스가 없으면 부모 행 하나를 지울 때마다 자식 테이블을 처음부터 끝까지 훑게 됩니다.
검사하는 동안 부모 행이 사라지면 안 되므로 데이터베이스는 그 행을 잠시 붙잡아 둡니다. 이것이 잠금입니다. 같은 부모 행을 가리키는 주문이 여러 갈래에서 동시에 들어오면 서로 기다리는 일이 생길 수 있습니다.
외래 키를 안 거는 선택
외래 키를 선언하지 않고 애플리케이션 코드가 직접 확인하게 두는 선택도 있습니다. 쓰기가 가벼워지기 때문입니다. 테이블을 서버 여럿에 나눠 담았을 때는 서버를 넘나드는 검사도 피할 수 있습니다. 자료를 한 번에 많이 밀어 넣는 작업에서도 검사를 끄고 진행하곤 합니다.
대가는 어긋난 데이터가 조용히 쌓인다는 것입니다. 가리키던 부모가 사라진 자식 행을 고아 레코드라고 부릅니다. 외래 키가 없으면 이것이 오류 없이 저장됩니다. 한참 뒤 읽을 때에야 드러납니다.
한 줄이라도 쌓이면 나중에 외래 키를 거는 일도 막힙니다. 이미 어긋난 행 때문에 선언이 거절되므로 그 행들을 먼저 찾아 치워야 합니다.
가르는 기준은 이 검사를 누가 지느냐입니다. 자료를 건드리는 경로가 하나뿐이고 그 코드를 전부 관리하고 있다면 코드에 맡길 수 있습니다. 경로가 여럿이거나 사람이 직접 자료를 손대는 일이 있다면 검사를 데이터베이스에 두는 것이 새는 곳을 줄입니다.
관련 항목
외래 키가 지키는 성질
참조 무결성 · 제약 · 일관성 · 트랜잭션 · 원자성
외래 키가 가리키는 대상이 되는 키
기본 키 · 후보 키 · 복합 키 · 대리 키 · 자연 키 · 유일 제약 · 식별자
외래 키가 놓이는 데이터 단위
테이블 · 행 · 열 · 셀 · 스키마 · 관계형 데이터베이스 · 데이터 모델링
외래 키를 선언하고 바꾸는 명령
DDL · CREATE TABLE · ALTER TABLE · SQL · 스키마 마이그레이션
외래 키를 따라가 값을 읽는 질의 연산
외래 키 검사에 드는 비용과 그것을 줄이는 구조
인덱스 · 잠금 · 데드락 · 실행 계획 · 벌크 적재
외래 키 열에 같이 거는 다른 제약
널 · 널 제약 · 검사 제약 · 기본값 · 데이터 타입
외래 키를 걸지 않는 저장 방식
NoSQL · 문서 데이터베이스 · 스키마리스 · 샤딩 · 반정규화
외래 키 설계에서 자주 터지는 문제
다른 이름: 외래키 · foreign key · FK