관계 모델 — 왜 테이블인가
데이터를 어떻게 담을까? 1970년 *에드거 코드(E. F. Codd)*는 한 가지 답을 내놨다 — 모든 것을 표(릴레이션) 하나로 표현하자. 이 단순한 발상이 관계 모델이고, 지금 우리가 쓰는 거의 모든 DB(PostgreSQL·MySQL·Oracle)의 토대다. 핵심은 표가 수학(집합과 논리) 위에 서 있다는 점이다 — 그래서 DB는 우리가 쓴 질의를 증명 가능하게 변형하고 최적화할 수 있다. 우리는 *"무엇을 원하는지"(선언적 SQL)*만 적고, *"어떻게 꺼낼지"(실행 계획)*는 DB가 알아서 정한다. 이 분리가 가능한 이유가 바로 관계 모델의 수학적 토대다. (출처: Silberschatz, Database System Concepts 7e, Ch 2 Relational Model.)
릴레이션 — 표 한 장의 정확한 정의
말은 "표"지만, 관계 모델에는 엄밀한 용어가 있다. 이 용어를 정확히 잡아야 정규화·키·조인이 흔들리지 않는다.
| 이론 용어 | 실무(SQL) | 뜻 |
|---|---|---|
| 릴레이션(relation) | 테이블 | 행의 모음 |
| 튜플(tuple) | 행(row) | 한 개체의 값들 |
| 속성(attribute) | 열(column) | 항목 이름 |
| 도메인(domain) | 타입+제약 | 그 열이 가질 수 있는 값의 집합 |
| 차수(degree) | 열 개수 | 속성 수 |
| 카디널리티(cardinality) | 행 개수 | 튜플 수 |
가장 중요한 한 문장 — **릴레이션은 튜플의 집합(set)이다. 집합이므로 이론상 (1) 완전히 같은 행이 둘 있을 수 없고, (2) 행과 열의 순서에 의미가 없다. 다만 *실제 SQL 테이블은 멀티셋(multiset)*이라 — PK·UNIQUE가 없으면 중복 행이 허용된다. 이 "이론은 집합, SQL은 멀티셋" 차이가 뒤에서 DISTINCT·UNION vs UNION ALL의 동작을 가른다.
또 하나 구분 — 스키마(schema)는 구조, 인스턴스(instance)는 그 순간의 데이터다. member(id, email, created_at)은 스키마고, 지금 그 안에 든 1,200개 행은 인스턴스다. 스키마는 거의 안 변하고, 인스턴스는 매 순간 변한다.
키 — 행을 가리키는 정확한 이름표
수백만 행 중 하나를 콕 집으려면 그 행을 유일하게 식별하는 값이 필요하다. 그게 키다. 키는 종류가 여럿이라 헷갈리기 쉬운데, 포함 관계로 외우면 깔끔하다.
- 슈퍼키(super key) — 행을 유일하게 식별하는 속성 집합 전부.
{id},{id, email},{email, created_at}… 군더더기가 붙어도 유일하기만 하면 슈퍼키. - 후보키(candidate key) — 슈퍼키 중 더 줄이면 유일성이 깨지는 최소 슈퍼키.
{id},{email}처럼 군더더기 없는 키. - 기본키(primary key) — 후보키 중 설계자가 대표로 고른 하나.
NOT NULL+UNIQUE가 자동 보장된다. - 대체키(alternate key) — 후보키인데 PK로 안 뽑힌 나머지(
email). - 외래키(foreign key) — 다른 테이블의 PK를 가리키는 속성. 테이블을 잇는 끈.
CREATE TABLE member (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 대리키
email text NOT NULL UNIQUE, -- 후보키(대체키)
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
member_id bigint NOT NULL REFERENCES member(id), -- 외래키
total_amount numeric(12,2) NOT NULL CHECK (total_amount >= 0)
);
기본키를 고를 때 자연키 vs 대리키 논쟁이 늘 나온다. **자연키(natural key)**는 업무상 의미가 있는 값(주민번호·사업자번호·email)이고, **대리키(surrogate key)**는 의미 없는 자동 증가 값(IDENTITY/SERIAL/UUID)이다. 실무는 거의 대리키를 PK로 쓴다 — 자연키는 바뀔 수 있고(email 변경), 길고, 외부에 노출하면 곤란(주민번호)하기 때문이다. PostgreSQL에서는 *표준 SQL인 GENERATED ALWAYS AS IDENTITY*가 옛 serial보다 권장된다. (출처: PostgreSQL Docs — Identity Columns.)
무결성 제약 — DB가 스스로 지키는 규칙
관계 모델의 강점은 데이터의 규칙을 DB 안에 못 박을 수 있다는 것이다. 애플리케이션 코드가 깜빡해도 DB가 마지막 방어선에서 막는다. 무결성은 세 층이다.
- 도메인 무결성 — 값 자체가 규칙을 지키는가. 타입(
numeric),NOT NULL,CHECK (total_amount >= 0). - 개체 무결성(entity integrity) — 기본키는 NULL일 수 없고 유일해야 한다. "식별 못 하는 행"을 금지.
- 참조 무결성(referential integrity) — 외래키 값은 반드시 부모 테이블에 존재하거나 NULL이어야 한다. 없는 회원의 주문을 막는다.
참조 무결성은 부모가 사라질 때 어떻게 할지를 같이 정한다.
member_id bigint NOT NULL
REFERENCES member(id)
ON DELETE RESTRICT -- 자식(주문)이 있으면 부모(회원) 삭제 거부 (기본값)
ON UPDATE CASCADE; -- 부모 PK가 바뀌면 자식도 따라 바꿈
ON DELETE 옵션은 *RESTRICT(거부)·CASCADE(같이 삭제)·SET NULL·SET DEFAULT·NO ACTION*가 있다. CASCADE는 편하지만 연쇄 삭제가 의도보다 크게 번질 수 있어 신중해야 한다(회원 하나 지웠는데 주문·결제·로그가 줄줄이 사라짐).
NULL — "모름"이라는 세 번째 진리값
NULL은 *0도 빈 문자열도 아닌, "값이 없음/모름"*이다. 그래서 SQL은 *참·거짓·UNKNOWN의 3치 논리(three-valued logic)*를 쓴다. 이게 초보가 가장 자주 데이는 지점이다.
SELECT * FROM member WHERE age = NULL; -- ❌ 항상 0행 (NULL = NULL 은 UNKNOWN)
SELECT * FROM member WHERE age IS NULL; -- ✅ NULL 판별은 IS NULL 로만
NULL = NULL조차 참이 아니라 UNKNOWN이고, WHERE는 TRUE인 행만 통과시키므로 결과가 0행이 된다. 집계도 다르다 — COUNT(*)는 NULL 포함 전체를 세지만 COUNT(age)는 age가 NULL인 행을 빼고 센다. (출처: PostgreSQL Docs — Comparison Functions, NULL.)
집합대수(관계대수) — SQL의 수학적 뿌리
우리가 쓰는 SQL은 사실 *관계대수(relational algebra)*라는 연산들의 옷을 갈아입은 것이다. 옵티마이저는 SQL을 관계대수 트리로 바꿔 *순서를 재배치(예: 조인보다 먼저 필터)*하며 최적화한다. 그래서 연산 6개만 알면 SQL이 왜 그렇게 도는지가 보인다.
| 관계대수 | 기호 | SQL | 뜻 |
|---|---|---|---|
| 선택(selection) | σ | WHERE | 조건에 맞는 행만 |
| 투영(projection) | π | SELECT 컬럼들 | 원하는 열만 |
| 합집합 | ∪ | UNION | 두 결과를 합침(중복 제거) |
| 차집합 | − | EXCEPT | A에 있고 B엔 없는 |
| 교집합 | ∩ | INTERSECT | 둘 다에 있는 |
| 곱(Cartesian) | × | CROSS JOIN | 모든 조합 |
| 조인(join) | ⋈ | JOIN ... ON | 곱 + 선택의 합성 |
예를 들어 *"서울에 사는 회원의 email"*은 관계대수로 π_email( σ_city='서울'(member) ) 이고, 곧 SELECT email FROM member WHERE city = '서울'이다. 조인(⋈)은 곱(×) 뒤에 조건 선택(σ)을 한 것과 같다 — 그래서 JOIN ... ON을 빼먹으면 카테시안 곱이 되어 행 수가 폭발한다(흔한 사고). 나눗셈(÷)은 SQL에 직접 연산자가 없어 "모든 ~을 가진" 질의(예: 모든 과목을 수강한 학생)를 NOT EXISTS 이중 부정으로 표현한다.
정리 — 토대가 위층을 떠받친다
관계 모델은 표 + 키 + 제약 + 집합 연산이라는 작은 약속이다. 그런데 이 약속이 — 정규화(중복 제거)·인덱스(키 탐색)·트랜잭션(무결성 보존)·옵티마이저(관계대수 재작성) 모두의 출발점이다. "왜 PK가 필요한가", "왜 FK가 사고를 막는가", "왜 = NULL이 안 되는가"를 모델 차원에서 이해하면, 위층의 실무 사고도 바닥부터 설명된다.
한 줄 요약 — 릴레이션은 튜플의 집합, 키는 유일 식별자, 무결성은 DB가 못 박은 규칙, SQL은 관계대수의 선언적 표현이다. 이 네 가지가 관계 DB 전부의 씨앗이다.
(출처: Silberschatz, Database System Concepts 7e Ch 2·4 / 김연희 데이터베이스 개론 2·3장 / PostgreSQL 17 Documentation.)