Devin.KR

SQLD · 기본

데이터 모델링과 SQL 기본

관계와 식별자 - 카디널리티 선택성 식별 비식별 관계와 주식별자 (SQLD 기본 3장)

관계의 차수와 선택성, 식별 관계와 비식별 관계의 차이, 주식별자가 갖춰야 할 네 가지 성질을 외래 키·복합 키·인조 키가 실제로 막는 일과 함께 확인한다.

개발자 · 원고 갱신

이 장에서 배우는 것

2장에서 엔터티와 속성을 나눴다. 엔터티는 혼자 있지 않다. 사원은 부서에 속하고, 주문 상세는 주문에 딸려 있다. 이 장은 엔터티 사이의 관계와, 인스턴스를 하나로 가려내는 식별자를 다룬다. 관계를 어떻게 읽느냐에 따라 외래 키가 NULL 을 허용할지, 부모를 지울 때 자식이 어떻게 될지가 정해진다.

  • 관계의 차수(1:1, 1:N, M:N)와 선택성(필수·선택)을 읽고, M:N 을 관계 엔터티로 푸는 이유를 정리한다.
  • 식별 관계와 비식별 관계가 자식 테이블의 기본 키 구성을 어떻게 바꾸는지 확인한다.
  • 주식별자의 네 가지 성질(유일성·최소성·불변성·존재성)과, 인조 식별자를 쓸 때 반드시 함께 둬야 하는 제약을 실행으로 확인한다.

핵심 개념

퇴사한 직원의 부서를 지웠더니 그 부서 사원들의 부서 칸이 가리키는 곳이 사라졌다는 사고는 흔하다. 반대로 부서를 못 지우게 막아 두었더니 조직 개편 때마다 DBA 를 불러야 한다는 불만도 흔하다. 둘 다 관계를 설계할 때 "부모가 없어지면 자식은 어떻게 되는가"를 정하지 않아서 생긴다.

관계의 표기: 차수와 선택성

관계는 두 엔터티 사이의 연관이다. 관계를 읽을 때는 두 가지를 본다. 차수(카디널리티)는 한쪽 인스턴스 하나에 다른 쪽이 몇 개 대응하는지(1:1, 1:N, M:N)다. 선택성(참여도)은 대응이 반드시 있어야 하는지(필수) 없어도 되는지(선택)다. "부서 하나에는 사원이 여러 명 있을 수 있고, 사원은 부서가 없을 수도 있다"는 문장에서 앞은 차수, 뒤는 선택성이다.

선 끝의 기호가 상대 쪽 개수를 말한다. 세로 막대는 하나(필수), 동그라미는 없어도 됨(선택), 까마귀발은 여럿이다. 비식별 관계는 점선, 식별 관계는 실선으로 그린다.

그림 · IE 표기로 읽는 관계의 차수와 선택성 — 선 끝의 기호가 상대 쪽 개수를 말한다. 세로 막대는 하나(필수), 동그라미는 없어도 됨(선택), 까마귀발은 여럿이다. 비식별 관계는 점선, 식별 관계는 실선으로 그린다.

관계를 종류로 나누면 존재에 의한 관계(사원은 부서에 소속된다)와 행위에 의한 관계(고객이 주문한다)가 있다. ERD 에서는 선 하나로 그리지만, UML 클래스 다이어그램은 연관과 의존으로 구분해 그린다.

M:N 관계는 테이블 두 개로 표현할 수 없다. 학생 하나가 여러 과목을, 과목 하나가 여러 학생을 가지므로, 어느 쪽에 외래 키를 둬도 값이 여러 개가 된다. 그래서 1장의 수강(enroll)처럼 관계 자체를 엔터티로 만들어 1:N 두 개로 푼다.

식별 관계와 비식별 관계

구분식별 관계비식별 관계
부모 키의 위치자식의 기본 키 일부가 된다자식의 일반 속성(외래 키)이 된다
주문행(주문번호, 행번호)사원(사원번호, ..., 부서번호)
자식이 부모 없이 존재불가능선택 관계면 가능(외래 키 NULL)
장점부모 키로 자식을 바로 찾는다. 조인이 줄어든다자식 키가 짧고, 부모가 바뀌어도 자식 키는 그대로다
단점단계가 깊어지면 자식 기본 키가 계속 길어진다부모 정보를 보려면 조인이 필요하다

식별 관계에서는 부모 키 order_id 가 자식 기본 키의 일부가 되고, 비식별 관계에서는 dept_id 가 기본 키 밖의 외래 키가 된다. 그래서 비식별·선택 관계의 자식은 부모 없이(NULL) 들어갈 수 있다.

그림 · 식별 관계와 비식별 관계에서 부모 키가 옮겨 가는 자리 — 식별 관계에서는 부모 키 order_id 가 자식 기본 키의 일부가 되고, 비식별 관계에서는 dept_id 가 기본 키 밖의 외래 키가 된다. 그래서 비식별·선택 관계의 자식은 부모 없이(NULL) 들어갈 수 있다.

식별자의 분류와 주식별자의 성질

식별자는 대표성(주식별자·보조식별자), 생성 방식(내부식별자·외부식별자), 속성 수(단일·복합), 대체 여부(본질식별자·인조식별자)로 나눈다. 주식별자로 고른 것은 네 가지 성질을 가져야 한다.

  • 유일성: 모든 인스턴스를 서로 구별한다.
  • 최소성: 유일성을 지키는 데 필요한 최소한의 속성만 쓴다.
  • 불변성: 한번 정하면 값이 바뀌지 않는다.
  • 존재성: 값이 반드시 있다(NULL 불가).

인조식별자(자동 증가 번호)는 짧고 바뀌지 않아 편하다. 그러나 인조식별자만 두면 본질적으로 같은 인스턴스가 중복 저장되는 것을 막지 못한다. 아래에서 직접 확인한다.

예제 스키마

부서와 사원은 비식별·선택 관계다(사원의 dept_id 는 NULL 가능). 주문과 주문행은 식별 관계다(order_line 의 기본 키에 order_id 가 들어 있다). SQLite 는 외래 키 검사가 기본으로 꺼져 있어서 스키마 첫 줄에서 켰다.

-- 3장: 부서·사원(비식별 관계)과 주문·주문행(식별 관계). SQLite 는 연결마다 외래 키 검사를 켜야 한다.
PRAGMA foreign_keys = ON;
CREATE TABLE dept (
  dept_id   INTEGER PRIMARY KEY,
  dept_name TEXT NOT NULL
);
CREATE TABLE emp (
  emp_id   INTEGER PRIMARY KEY,
  emp_name TEXT NOT NULL,
  dept_id  INTEGER REFERENCES dept(dept_id)
);
CREATE TABLE orders (
  order_id   INTEGER PRIMARY KEY,
  ordered_on TEXT NOT NULL
);
CREATE TABLE order_line (
  order_id INTEGER NOT NULL REFERENCES orders(order_id) ON DELETE CASCADE,
  line_no  INTEGER NOT NULL,
  item     TEXT    NOT NULL,
  PRIMARY KEY (order_id, line_no)
);
INSERT INTO dept VALUES (1, '총무'), (2, '연구'), (3, '품질');
INSERT INTO emp VALUES (10, '이서', 1), (11, '고은', 2), (12, '나래', NULL);
INSERT INTO orders VALUES (500, '2026-09-10'), (501, '2026-09-11');
INSERT INTO order_line VALUES (500, 1, '모니터'), (500, 2, '키보드'), (501, 1, '마우스');
emp_idemp_namedept_id
10이서1
11고은2
12나래NULL

SQL과 실행 결과

없는 부모를 가리키는 자식은 거부된다

INSERT INTO emp VALUES (13, '다인', 9);

실행 결과:

Runtime error near line 1: FOREIGN KEY constraint failed (19)

선택 관계: 부모 없이 들어가는 자식

외래 키에 NULL 을 넣는 것은 "부모가 없다"는 뜻이라 허용된다. 비식별·선택 관계이기 때문이다.

INSERT INTO emp VALUES (13, '다인', NULL);
SELECT e.emp_name, d.dept_name
FROM emp e LEFT JOIN dept d ON d.dept_id = e.dept_id
ORDER BY e.emp_id;

실행 결과:

emp_name  dept_name
--------  ---------
이서      총무
고은      연구
나래      NULL
다인      NULL

자식이 있는 부모 삭제: 기본 동작은 거부

총무 부서(1)에는 사원 이서가 있다. 외래 키에 삭제 규칙을 따로 적지 않았으므로 삭제가 거부된다.

DELETE FROM dept WHERE dept_id = 1;

실행 결과:

Runtime error near line 1: FOREIGN KEY constraint failed (19)

식별 관계와 CASCADE

주문행은 주문 없이 의미가 없으므로 ON DELETE CASCADE 를 걸었다. 주문 500 을 지우면 그 주문행 두 개도 함께 지워진다.

DELETE FROM orders WHERE order_id = 500;
SELECT order_id, line_no, item FROM order_line;

실행 결과:

order_id  line_no  item
--------  -------  ------
501       1        마우스

식별 관계의 자식은 부모 키가 기본 키의 일부라서 같은 주문 안에서 행 번호가 겹칠 수 없다.

INSERT INTO order_line VALUES (501, 2, '패드');
INSERT INTO order_line VALUES (501, 1, '케이블');

실행 결과:

Runtime error near line 2: UNIQUE constraint failed: order_line.order_id, order_line.line_no (19)

첫 INSERT(501, 2)는 성공했고 두 번째(501, 1)가 기존 행과 겹쳐 거부됐다.

인조식별자만으로는 중복을 못 막는다

CREATE TABLE member_loose (
  member_id INTEGER PRIMARY KEY,
  email     TEXT NOT NULL
);
INSERT INTO member_loose (email) VALUES ('ari@example.com');
INSERT INTO member_loose (email) VALUES ('ari@example.com');
SELECT member_id, email FROM member_loose;

실행 결과:

member_id  email
---------  ---------------
1          ari@example.com
2          ari@example.com

같은 이메일의 회원이 두 명이 됐다. 번호는 유일하지만 "회원"은 유일하지 않다. 본질식별자 후보(이메일)에 UNIQUE 를 함께 걸어야 한다.

CREATE TABLE member_safe (
  member_id INTEGER PRIMARY KEY,
  email     TEXT NOT NULL UNIQUE
);
INSERT INTO member_safe (email) VALUES ('ari@example.com');
INSERT INTO member_safe (email) VALUES ('ari@example.com');

실행 결과:

Runtime error near line 6: UNIQUE constraint failed: member_safe.email (19)

표 · 외래 키·기본 키가 실제로 막은 일과 허용한 일

시도종료 코드실제 결과
없는 부서 9 를 가리키는 사원 추가1Runtime error near line 1: FOREIGN KEY constraint failed (19)
부서 없이(NULL) 사원 추가 — 비식별·선택 관계0성공, 사원 4명 중 부서 NULL 2명
사원이 있는 부서 1 삭제1Runtime error near line 1: FOREIGN KEY constraint failed (19)
주문 500 삭제 — 식별 관계 + CASCADE0남은 주문행 1개 (500 의 행도 함께 삭제)
같은 (주문, 행번호) 다시 추가1Runtime error near line 2: UNIQUE constraint failed: order_line.order_id, order_line.line_no (19)
인조식별자만 두고 같은 이메일 두 번0성공, 2행 — 중복을 못 막는다
이메일에 UNIQUE 를 더하고 같은 시도1Runtime error near line 6: UNIQUE constraint failed: member_safe.email (19)

표준과 구현의 차이

아래 Oracle·SQL Server 동작은 비교 설명이며 이 책에서 실행하지 않았다.

항목SQLite(실행)OracleSQL Server
외래 키 검사연결마다 PRAGMA foreign_keys = ON 필요항상 검사항상 검사
ON DELETE 옵션NO ACTION, RESTRICT, CASCADE, SET NULL, SET DEFAULT기본(거부), CASCADE, SET NULLNO ACTION, CASCADE, SET NULL, SET DEFAULT
자동 증가 키INTEGER PRIMARY KEY시퀀스, IDENTITY 열IDENTITY 속성, 시퀀스
ON UPDATE CASCADE지원지원하지 않음지원

[구현 차이] SQLite 에서 외래 키 검사를 끄면 없는 부서를 가리키는 사원이 들어가고, 주문을 지워도 주문행이 남는다. 모델에서 정한 관계가 DB 에서 지켜지는지는 제품 설정까지 확인해야 안다.

PRAGMA foreign_keys = OFF;
INSERT INTO emp VALUES (13, '다인', 9);
DELETE FROM orders WHERE order_id = 500;
SELECT (SELECT COUNT(*) FROM emp WHERE dept_id = 9)        AS orphan_emp,
       (SELECT COUNT(*) FROM order_line WHERE order_id = 500) AS orphan_lines;

실행 결과:

orphan_emp  orphan_lines
----------  ------------
1           2

시험에서 헷갈리는 지점

판단 1. "비식별 관계에서 자식의 외래 키는 항상 NOT NULL 이다"

틀렸다. 비식별 관계는 필수일 수도 선택일 수도 있다. 선택 관계면 외래 키가 NULL 일 수 있고, 위 예제의 나래·다인이 그 경우다. 식별 관계의 외래 키는 기본 키의 일부이므로 NULL 이 될 수 없다.

판단 2. "식별 관계를 계속 이어 가면 하위 엔터티의 기본 키 속성 수가 늘어난다"

맞다. 주문 → 주문행 → 주문행 옵션처럼 식별 관계가 세 단계면 맨 아래 기본 키는 세 컬럼 이상이 된다. 조인 조건과 SQL 이 길어지는 것이 식별 관계의 대표적 단점이다. 그래서 자식이 부모와 독립적으로 쓰이거나 부모 키가 너무 길면 비식별 관계를 검토한다.

판단 3. "주식별자는 유일하기만 하면 속성을 많이 포함해도 된다"

틀렸다. 최소성을 어긴다. (사원번호, 이름)은 유일하지만 사원번호만으로도 유일하므로 이름은 불필요하다.

판단 4. "M:N 관계는 물리 모델에서 테이블 두 개와 외래 키 하나로 구현한다"

틀렸다. 관계 엔터티(교차 테이블)를 추가해 테이블 세 개로 구현한다.

연습 문제

  1. "학생은 동아리에 가입하지 않아도 되고 여러 동아리에 가입할 수 있다. 동아리에는 회원이 한 명 이상 있어야 한다." 이 문장의 차수와 양쪽 선택성을 쓰고, 테이블 몇 개로 구현하는지 쓰라.
  2. 예제 스키마에서 품질 부서(3)를 지운 뒤 부서 수를 조회하라. 삭제가 성공하는지 먼저 판단하라.
  3. 주민등록번호를 회원의 주식별자로 쓰지 않는 이유를 주식별자의 성질과 관련해 두 가지 쓰라.
  4. 식별 관계인 주문–주문행에서 주문번호를 바꿔야 하는 업무 요구가 생겼다. 어떤 일이 벌어지는가?
  5. 인조식별자 member_id 를 기본 키로 쓰는 회원 테이블에 반드시 함께 둬야 하는 제약은 무엇이고, 왜 필요한가?

정답과 해설

1. 차수 M:N. 학생 쪽 참여는 선택, 동아리 쪽 참여는 필수. 학생·동아리·가입(학생번호, 동아리번호) 세 테이블로 구현한다. "동아리에 한 명 이상"이라는 필수 조건은 외래 키만으로 강제할 수 없어 애플리케이션이나 트리거로 지킨다.

2. 성공한다. 품질 부서에는 사원이 없으므로 외래 키가 막을 것이 없다.

DELETE FROM dept WHERE dept_id = 3;
SELECT COUNT(*) AS depts FROM dept;

실행 결과:

depts
-----
2

3. 존재성: 외국인 등 번호가 없는 회원이 있을 수 있다. 불변성: 정정·변경되는 경우가 있다. 그 밖에 개인정보라 저장과 노출을 최소화해야 한다는 법적 제약도 있다.

4. 주문번호가 주문행 기본 키의 일부이므로 부모와 모든 자식 행의 키를 함께 바꿔야 한다. 주식별자의 불변성을 요구하는 이유가 이것이다. 이런 요구가 예상되면 주문번호를 업무 번호(보조식별자)로 두고, 바뀌지 않는 인조식별자를 주식별자로 쓰는 설계를 검토한다.

5. 본질식별자 후보(예: 이메일, 로그인 아이디)에 대한 UNIQUE 제약. 인조식별자는 행마다 새 번호를 주므로 같은 사람이 두 번 저장되어도 막지 못한다. 위 member_loose 예제가 그 결과다.

다음 장에서는 속성을 어느 엔터티에 둘지 판단하는 기준인 함수 종속과 정규화를 다룬다.

참고 자료

READER FEEDBACK

질문·오탈자·의견

내용에 관한 질문이나 오탈자, 더 나은 설명을 위한 의견을 남겨 주세요. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.