콘텐츠로 이동
Study NotePostgreSQL

테이블·타입·제약으로 데이터 설계하기

결론부터
테이블은 사실을 나누어 저장하고, 제약은 어떤 경로로 쓰더라도 지켜야 할 규칙을 강제한다.

프로젝트 이름을 모든 작업 행에 복사하면 이름을 바꿀 때 여러 행을 함께 고쳐야 한다. 공통 예제가 projects와 tasks를 나누는 이유다. 프로젝트 이름은 프로젝트에 한 번 저장하고 작업은 그 프로젝트의 ID를 참조한다.

이 장에서 처음 나오는 말3개
기본 키Primary key
각 행을 중복과 NULL 없이 식별하는 컬럼 또는 컬럼 묶음.
외래 키Foreign key
다른 행을 참조할 때 그 대상이 존재하는지 강제하는 제약.
NULL
값이 없거나 아직 모름을 나타낸다. 빈 문자열·0·false와 다르다.

projects.id = 1인 프로젝트에 작업 두 개가 속한다. 각 작업의 project_id가 1이므로 이것은 프로젝트 하나 대 작업 여러 개의 관계다. 프로젝트 이름을 바꿔도 작업 행의 ID 참조는 유지된다. 한 작업에 담당자 여러 명이 필요해지면 쉼표 문자열에 넣기보다 별도 연결 테이블을 고려한다.

규칙공통 예제의 표현막는 오류
행을 식별한다PRIMARY KEY같은 ID의 중복
이름이 필요하다NOT NULL값 없는 프로젝트 이름
프로젝트 이름은 중복되지 않는다UNIQUE이 예제의 이름 중복
제목은 빈 문자열이 아니다CHECK (char_length(title) > 0)빈 제목
프로젝트가 존재해야 한다REFERENCES public.projects(id)없는 프로젝트를 가리키는 작업

프로젝트 이름의 유일성은 이 예제의 업무 규칙이다. 모든 앱에서 이름을 유일하게 할 필요는 없다. CHECK는 결과가 NULL이면 통과할 수 있으므로 필수 값에는 NOT NULL도 필요하다. 외래 키를 만들면 참조 대상의 유효성을 검사하지만, 참조하는 쪽의 tasks.project_id 인덱스까지 자동 생성하지는 않는다. (제약 조건)

타입은 데이터의 의미로 고른다

섹션 제목: “타입은 데이터의 의미로 고른다”
저장할 값출발점판단할 점
이름·제목text길이 제한이 업무 규칙이면 별도 제약
내부 식별자bigint identity자동 번호에 빈틈이 생겨도 정상
여러 곳에서 생성할 IDuuidID가 추측하기 어렵더라도 접근 권한 검사는 필요
사건이 발생한 시점timestamptz원래 입력한 지역 시간대 이름은 따로 보관해야 함
생일·정산일date특정 순간이 아니라 달력상의 날짜
정확한 소수 금액numeric통화·자릿수·반올림 규칙도 결정
유동적인 부가 속성jsonb자주 JOIN·제약·검색할 핵심 필드는 일반 컬럼부터 검토

timestamptz는 시점을 저장하고 세션의 시간대에 맞춰 표시한다. “서울 시간대”라는 이름 자체를 값에 보존하는 타입은 아니다. 예약 장소의 시간대가 필요하면 별도 컬럼을 둔다. (날짜와 시간)

jsonb도 인덱스를 사용할 수 있지만 모든 관계를 JSON 안에 숨기면 필수 값과 참조 무결성을 관리하기 어려워진다. 스키마가 없다는 뜻으로 쓰지 않는다. (JSON 타입)

제약이 실제로 막는지 확인하기

섹션 제목: “제약이 실제로 막는지 확인하기”

공통 예제의 psql에서 아래 명령은 각각 따로 실행한다. 모두 실패해야 정상이다. 자동 커밋 상태에서 실패한 문장은 데이터를 남기지 않는다.

INSERT INTO public.projects (name) VALUES ('플랫폼');
INSERT INTO public.tasks (project_id, title) VALUES (999, '없는 프로젝트');
INSERT INTO public.tasks (project_id, title) VALUES (1, '');

순서대로 unique·foreign key·check 제약 오류가 나온다. 앱에서만 검사했다면 관리자 SQL이나 다른 서비스의 쓰기가 규칙을 우회할 수 있었지만, DB 제약은 그 경로에서도 적용된다. 실패한 INSERT도 identity 번호를 소비할 수 있다. 번호가 연속인지로 데이터 유실을 판단하지 않는다.

공통 예제는 작업이 있는 프로젝트를 바로 지우지 못하게 한다. ON DELETE CASCADE를 선택하면 관련 작업도 자동 삭제할 수 있지만, 실수의 영향 범위가 커진다. 프로젝트 보관 처리, 명시적 하위 삭제, 연쇄 삭제 중 업무 의미에 맞는 것을 고른다.

이해 확인: assignee IS NULL인 작업은 데이터 오류일까? 이 예제에서는 미배정 상태이므로 정상이다. 반면 project_id는 모든 작업이 프로젝트에 속해야 해서 NULL을 허용하지 않는다.