외래키(FK) —
테이블 사이를 연결하는 값
지난 강의의 기본키는 한 테이블 안에서 행 하나를 정확히 찾는 이름표였습니다. 그런데 회원은 members 테이블에, 주문은 orders 테이블에 따로 들어 있죠. 주문 한 건만 보고 어느 회원의 주문인지 알아내려면 — 자식 테이블에 부모 행의 주소를 적어두면 됩니다. 그 주소가 외래키입니다.
이번 섹션의 여정
완료 ✓
완료 ✓
지금 여기
테이블 만들기 · 변경 · 정리
오늘 확인할 네 가지
주문 한 건은 누구의 주문일까
먼저 members 테이블입니다. 3-4에서 본 그대로, member_id가 기본키예요.
| member_id (PK) | member_name | gender | age | joined_at |
|---|---|---|---|---|
| 1 | 김철수 | M | 35 | 2025-01-15 |
| 2 | 이영희 | F | 28 | 2025-02-03 |
| 3 | 박민수 | M | 42 | 2025-02-10 |
| ⋮ | ||||
| 20 | 백서윤 | F | 21 | 2026-04-02 |
이번에는 orders 테이블입니다.
| order_id (PK) | member_id (FK) | ordered_at |
|---|---|---|
| 1 | 1 | 2025-02-01 |
| 2 | 2 | 2025-02-15 |
| 3 | 3 | 2025-03-01 |
| 4 | 4 | 2025-03-15 |
| ⋮ | ||
주문 1번에는 회원 이름 대신 회원 번호 1만 적혀 있습니다. 이름을 다시 적지 않은 이유는 간단해요 — 번호 1을 따라 members 테이블로 가면 김철수 회원을 정확히 찾을 수 있기 때문입니다.
이렇게 다른 테이블의 행을 가리키는 컬럼을 외래키, 영어로 Foreign Key, 줄여서 FK라고 부릅니다. orders.member_id는 members.member_id를 가리키는 주소 역할을 합니다.
값 하나를 따라가 보기
말로만 보면 추상적이니 값의 흐름을 천천히 따라가 보겠습니다.
| 단계 | 무엇을 하는가 | 얻는 것 |
|---|---|---|
| ① | orders 에서 order_id 가 1인 행을 찾는다 | member_id = 1 |
| ② | 그 숫자 1을 들고 members 로 간다 | 기본키 member_id 에서 1번 행 |
| ③ | members 1번 행을 읽는다 | member_name = 김철수 |
같은 방식으로 주문 2번도 읽어보세요. member_id가 2이고, members의 2번은 이영희입니다. 주문 3번은 member_id가 3, members의 3번은 박민수예요.
여기서 중요한 지점이 하나 있습니다. 두 테이블에 숫자 1이 우연히 같아 보이는 게 아니라는 점이에요.
orders.member_id가 members.member_id를 참조하도록 외래키 규칙이 실제로
설정되어 있습니다. 이 차이가 왜 중요한지는 뒤에서 다시 짚겠습니다.
부모 테이블과 자식 테이블
외래키를 읽을 때는 부모와 자식이라는 말을 자주 씁니다. 두 단어의 뜻은 딱 하나로 정해져 있어요.
| 테이블 | 이 컬럼 | 어떤 키인가 | 역할 |
|---|---|---|---|
| members | member_id | 기본키 (PK) | 가리킴을 받는 쪽 — 부모 |
| orders | member_id | 외래키 (FK) | 주소를 적어두는 쪽 — 자식 |
우리 예에서는 화살표를 orders.member_id에서 members.member_id 쪽으로 그립니다. 자식의 외래키에서 부모의 기본키로 따라가는 화살표예요.
부모가 더 중요하고 자식이 덜 중요하다는 뜻이 아닙니다. 데이터가 어느 방향을 가리키는지 설명하는 이름일 뿐이에요. 주문 데이터가 회원 데이터보다 덜 중요할 이유는 없죠.
한 부모를 여러 자식이 가리킬 수 있다
orders를 더 내려가 보면 member_id 1이 여러 번 나옵니다.
| order_id (PK) | member_id (FK) | ordered_at |
|---|---|---|
| 1 | 1 | 2025-02-01 |
| 6 | 1 | 2025-04-10 |
| 14 | 1 | 2025-08-01 |
| 20 | 1 | 2025-10-12 |
| 27 | 1 | 2025-12-10 |
| 34 | 1 | 2026-02-15 |
부모인 회원 한 행을, 자식인 주문 여러 행이 가리킬 수 있습니다. 그렇다면 반대는 어떨까요? 부모 행이 있다고 자식 행이 꼭 있어야 하는 것은 아닙니다.
우리 샘플 데이터의 20번 백서윤 회원은 members에는 있지만 orders에는
주문이 하나도 없습니다. 가입은 했는데 아직 구매하지 않은 상태예요. 회원으로
가입했다고 반드시 주문해야 하는 건 아니니까요 — 데이터가 깨진 게 아니라 그냥
아직 주문이 없는 것입니다.
이런 관계를 어떤 종류와 개수로 설계할지는 섹션 4에서 제대로 다룹니다. 지금은 외래키 값 하나를 따라가면 연결된 부모 행을 찾을 수 있다는 흐름에만 집중하면 충분해요.
참조 무결성 — 존재하는 부모만 가리킨다
이제 외래키가 단순한 메모와 다른 가장 중요한 이유를 볼 차례입니다.
새 주문을 저장하면서 member_id에 999를 넣었다고 해보겠습니다. 그런데 members 테이블에는 999번 회원이 없어요. 이 주문을 그대로 허용하면 누구의 주문인지 영원히 찾을 수 없는 기록이 생깁니다.
외래키가 설정되어 있으면 데이터베이스가 저장하기 전에 부모 테이블을 확인합니다. 없는 번호라면 오류를 내고 저장을 막아요.
이렇게 테이블 사이의 연결이 깨지지 않도록 지키는 규칙을 참조 무결성이라고 합니다.
위 화면에서 눈여겨볼 건 첫 줄의 문법이 아니라 결과 메시지입니다. 데이터를 넣는 SQL은 뒤에서 따로 배웁니다 — 지금은 “없는 부모를 가리키면 DB가 막는다” 는 사실만 가져가세요.
우리 orders.member_id에는 NOT NULL도 함께 걸려 있습니다. 그래서 회원 번호를 비워둘 수도 없고, 존재하지 않는 번호를 적을 수도 없습니다. 모든 주문은 반드시 실제 회원 한 명을 가리켜야 해요.
외래키라고 해서 무조건 NOT NULL 은 아닙니다
방금 문장을 이렇게 오해하기 쉽습니다 — “외래키 컬럼은 원래 비울 수 없구나.” 그렇지 않아요.
| FK 컬럼의 설계 | 비울 수 있나 | 비워두면 무슨 뜻 |
|---|---|---|
| NULL 허용 | 가능 | 아직 연결된 부모가 없다 |
| NOT NULL | 불가 | 반드시 부모 하나를 가리켜야 한다 |
외래키 컬럼이 NULL을 허용하도록 설계된 테이블이라면, 아직 연결된 부모가 없다는 뜻으로 NULL을 넣을 수 있습니다. 우리 주문 테이블이 비울 수 없는 건 orders.member_id를 NOT NULL로 정했기 때문이에요.
orders.member_id와 products.category_id는 둘 다 NOT NULL 입니다.
NULL을 허용하는 외래키는 이 강의에서 개념만 짚는 가상의 설계예요.
부모 행을 함부로 지울 수 없는 이유
참조 무결성은 데이터를 넣을 때만이 아니라 지울 때도 작동합니다.
김철수 회원 1번을 여러 주문이 가리키고 있는데 회원 1번을 먼저 지워버리면, 남은 주문 6건이 갈 곳을 잃습니다. 우리 실습 DB에는 삭제 동작을 따로 바꾸는 옵션이 없어서 — MySQL이 외래키 오류를 내고 삭제를 막습니다.
같은 규칙을 여러 삭제 시도에 적용해 보면 이렇게 갈립니다.
| 삭제 시도 | 결과 | 이유 |
|---|---|---|
| 회원 1번(김철수) | 차단 | 주문 6건이 이 회원을 가리키고 있음 |
| 회원 20번(백서윤) | 성공 | 이 회원을 가리키는 주문이 하나도 없음 |
| 주문 1번 | 차단 | order_items 두 줄이 주문 1번을 가리키고 있음 |
반대 방향도 확인해 두세요. 자식인 주문 행이 사라진다고 부모인 회원까지 자동으로 사라지지는 않습니다. 주문 34번이 지워지더라도 회원 1번 김철수는 members에 그대로 남아 있어요 — 검사는 언제나 부모를 지울 때 자식이 남아 있는지 보는 방향으로만 작동합니다.
삭제할 때 자식까지 함께 지울지, 막을지, 연결만 비울지는 설계에 따라 다른 정책을 정할 수 있습니다. 그 선택지의 문법은 이번 강의에서 다루지 않아요. 오늘은 우리 DB에서는 연결된 자식이 있는 부모를 함부로 지울 수 없다는 것만 정확히 기억하면 됩니다.
컬럼 이름이 같다고 외래키가 되는 건 아닙니다
여기서 아주 흔한 오해 하나를 짚겠습니다. 두 테이블에 member_id라는 같은 이름의 컬럼이 있다고 해서 자동으로 연결되지 않습니다.
DB 가 아는 것두 컬럼이 같은 이름이라는 사실뿐
999 를 넣으면그대로 저장된다 — 막아주지 않는다
부모 삭제아무 검사 없이 지워진다
DB 가 아는 것이 값이 어느 테이블의 어느 컬럼을 가리키는지
999 를 넣으면부모를 확인하고 저장을 거부한다
부모 삭제가리키는 자식이 있으면 차단한다
같은 값이 나란히 놓여 있는 것과 — 연결이 보장되는 것은 다릅니다.
연결과 검사를 실제로 만들어내는 건 설계도에 적힌 이 한 줄입니다. 우리 orders 테이블에 실제로 들어 있는 규칙이에요.
CONSTRAINT fk_orders_member
FOREIGN KEY (member_id) REFERENCES members(member_id)쉽게 읽으면 *“이 테이블의 member_id는 members 테이블의 member_id를 참조한다”*는 뜻입니다. 그리고 반대로 — 컬럼 이름이 서로 달라도 외래키 규칙으로 연결할 수 있습니다. 이름을 맞추는 건 사람이 읽기 쉽게 하려는 관례이고, 실제 연결은 FOREIGN KEY 제약조건이 만듭니다.
카테고리와 상품에서도 같은 구조
이 구조는 회원과 주문에만 있는 게 아닙니다. categories와 products에서 똑같이 반복돼요.
| category_id (PK) | category_name |
|---|---|
| 1 | 의류 |
| 2 | 전자기기 |
| 3 | 식품 |
| ⋮ | |
| product_id (PK) | product_name | price | category_id (FK) |
|---|---|---|---|
| 1 | 라운드 티셔츠 | 19000 | 1 |
| 8 | 패딩 점퍼 | 129000 | 1 |
| 9 | 무선 이어폰 | 89000 | 2 |
| 11 | 보조배터리 | 25000 | 2 |
| 16 | 프리미엄 그래놀라 | 12000 | 3 |
무선 이어폰의 category_id는 2이고, 2번 카테고리는 전자기기입니다. 여기서는 categories가 부모, products가 자식이에요. 화살표는 products.category_id → categories.category_id.
| 자식 (FK) | 부모 (PK) | 읽는 법 |
|---|---|---|
| orders.member_id | members.member_id | 이 주문은 어느 회원의 것인가 |
| products.category_id | categories.category_id | 이 상품은 어느 카테고리인가 |
여러 상품이 같은 카테고리 번호를 가질 수 있지만, 존재하지 않는 카테고리 번호를 상품에 넣을 수는 없습니다. 회원과 주문에서 본 규칙이 그대로예요.
지금은 값을 따라가며 연결된 행을 찾는 개념만 잡습니다. 두 테이블을 실제로
하나의 결과로 합치는 JOIN 문법은 섹션 5에서 제대로 다룹니다.
한눈에 정리
| 개념 | 한 줄 정의 | shop 에서는 |
|---|---|---|
| 외래키 (FK) | 자식 테이블에 적어둔 부모 행의 주소 | orders.member_id |
| 부모 테이블 | 가리킴을 받는 쪽 — 기본키를 가진 테이블 | members · categories |
| 자식 테이블 | 외래키로 가리키는 쪽 | orders · products · order_items |
| 화살표 방향 | 언제나 자식의 FK → 부모의 PK | orders.member_id → members.member_id |
| 참조 무결성 | 존재하지 않는 부모는 가리킬 수 없다는 규칙 | member_id = 999 저장 거부 |
| 부모 삭제 | 가리키는 자식이 남아 있으면 삭제 차단 | 주문이 있는 회원 1번은 삭제 불가 |
| FK ≠ NOT NULL | 빈값 허용 여부는 별개의 약속 | 우리 FK 컬럼은 모두 NOT NULL |
외래키가 UNIQUE처럼 중복을 허용하지 않는 다른 키를 참조할 수도 있습니다.
다만 우리 샘플 DB의 외래키는 전부 부모 테이블의 기본키를 가리켜요. 입문
단계에서는 이 구조부터 확실히 익히면 충분합니다.
여기까지 왔다면 컬럼, 데이터 타입, 기본키, 외래키 — 테이블을 만드는 재료를 전부 살펴본 셈입니다. 다음은 이 재료를 SQL로 직접 적어서 새 테이블을 만들 차례예요.