데이터베이스 용어 사전
설계·정규화엔터티 · 식별 관계 · 비식별 관계 · 연결 테이블

ERD

엔터티와 관계를 그림으로 표현한 데이터 모델. 개념 → 논리 → 물리 순으로 구체화한다.

엔터티(Entity)와 관계(Relationship)를 그림으로 표현한 데이터 모델.

세 단계로 구체화한다

개념 모델   업무를 그대로 옮긴다. 주요 엔터티와 관계만
            "회원이 주문을 한다"
   ↓
논리 모델   속성 · 키 · 정규화. DB 제품과 무관하다
            회원(회원ID, 이름, 이메일) — 주문(주문ID, 회원ID, 금액)
   ↓
물리 모델   타입 · 인덱스 · 파티션. 제품에 맞춘다
            member(id BIGINT, email VARCHAR(255) ...) + INDEX(email)

논리에서 정규화를 마치고, 물리에서 성능을 고민한다는 순서가 핵심이다. 처음부터 성능을 걱정해 넓은 테이블을 만드는 것이 흔한 실수다.

관계의 차수(카디널리티)

1:1회원 — 회원상세(자주 안 쓴다. 대개 한 테이블로 합친다)
1:N회원 — 주문(가장 흔하다. N 쪽이 FK 를 가진다)
N:M학생 — 과목(직접 표현할 수 없다 → 연결 테이블로 푼다)

N:M은 반드시 연결 테이블로

-- 학생과 과목은 서로 여럿 → 중간 테이블을 만든다
CREATE TABLE enrollment (
  student_id BIGINT REFERENCES student(id),
  subject_id BIGINT REFERENCES subject(id),
  enrolled_at TIMESTAMP NOT NULL DEFAULT now(),   -- 관계 자체의 속성이 붙는다
  PRIMARY KEY (student_id, subject_id)
);

연결 테이블은 단순한 다리가 아니다 — 관계 자체가 가지는 속성(수강 신청 시각·성적)이 여기 들어간다.

식별 관계 vs 비식별 관계

  • 식별 관계 — 부모의 키가 자식의 기본키 '일부' 가 된다

    • enrollment(student_id, subject_id) ← 부모 키가 PK 에 포함
    • 부모 없이는 존재할 수 없다(약한 엔터티)
  • 비식별 관계 — 부모의 키가 일반 속성(FK)일 뿐이다

    • orders(id, member_id) ← id 가 독립적인 PK
    • 부모가 없어도 논리적으로 존재 가능하다

식별 관계를 남발하면 키가 계속 길어진다

  • 주문(주문ID) → 주문상품(주문ID, 상품ID) → 주문상품이력(주문ID, 상품ID, 순번)
  • 손자 테이블의 PK 가 3개 칼럼. 조인과 인덱스가 무거워진다

실무 기본값은 비식별 + 대리키다

흔한 모델링 안티패턴

  • ① EAV (Entity-Attribute-Value)

    • 속성을 행으로 저장한다 → 조회마다 피벗. 타입 검증 불가. 대개 후회한다
    • (정말 필요하면 jsonb 가 낫다)
  • ② 만능 코드 테이블

    • 모든 코드를 한 테이블에 몰아넣는다 → FK 를 걸 수 없다
  • ③ 나중에 쓸까 봐 만든 칼럼

    • col1 · col2 · reserved1 ... → 의미를 아무도 모르게 된다

면접 함정

  • "1:1이면 테이블을 나눈다" → 대개 합치는 게 낫다. 접근 빈도나 보안 등급이 확연히 다를 때만 나눈다.
  • "N:M은 FK 두 개면 된다" → 연결 테이블이 필요하다. 관계에도 속성이 붙는다.

물리 설계로 내려올 때 정하는 것

-- 논리: '회원은 이메일을 가진다'
-- 물리: 타입 · 길이 · NULL 여부 · 인덱스 · 기본값을 전부 정한다
CREATE TABLE member (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  email      VARCHAR(255) NOT NULL,
  status     VARCHAR(20)  NOT NULL DEFAULT 'ACTIVE',
  created_at TIMESTAMPTZ  NOT NULL DEFAULT now(),   -- TIMESTAMP 가 아니라 TZ 를 쓴다
  UNIQUE (email)
);
CREATE INDEX idx_member_created ON member (created_at DESC);

TIMESTAMPTZ 는 실무에서 자주 틀리는 선택이다. 시간대 없는 TIMESTAMP로 저장하면 서버 시간대가 바뀌거나 해외 사용자가 생길 때 값의 의미가 깨진다.

타입 선택이 성능이 된다

  • BIGINT vs UUID — InnoDB 에서는 PK 크기가 모든 보조 인덱스에 전파된다
  • VARCHAR(255) 남발 — 길이는 저장 공간이 아니라 '검증' 이다. 의미에 맞게 정한다
  • NUMERIC vs FLOAT — 금액은 반드시 NUMERIC/DECIMAL (부동소수점 오차 금지)
  • TEXT vs VARCHAR — PostgreSQL 에서는 성능 차이가 없다. 제약이 필요할 때만 길이 지정

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 데이터 모델링·ERD — 개념·논리·물리 설계