숏컷 코드
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);문법
자료형 선택은 저장 효율과 제약 표현력을 동시에 결정한다
열의 자료형은 단순히 "어떤 값을 담는가"가 아니라 허용 값 범위, 저장 크기, 비교 연산자까지 정의합니다. 정수 ID에는 BIGSERIAL(자동 증가 8바이트 정수), 가변 길이 문자열에는 TEXT, 시간 정보에는 TIMESTAMPTZ를 선택하는 것이 PostgreSQL 관용입니다. VARCHAR(n)처럼 길이를 제한하는 타입은 실제로 성능상 이점이 없고 스키마 변경 부담만 늘어나므로, 특별한 이유가 없다면 TEXT를 쓰는 편이 낫습니다.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- 8바이트 자동 증가 정수
email TEXT NOT NULL UNIQUE, -- 길이 제한 없는 문자열
score NUMERIC(10, 2), -- 소수점 정밀도가 필요할 때
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- 타임존 포함 시각
);CREATE TABLE은 열 이름, 타입, 기본값, 제약을 한 번에 선언한다
입문 단계에서는 열 이름 -> 자료형 -> NOT NULL/DEFAULT -> PRIMARY KEY/UNIQUE 흐름으로 읽으면 됩니다. PostgreSQL은 테이블 정의 안에서 데이터 구조와 무결성 규칙을 함께 선언하는 쪽이 기본입니다. 그래서 "애플리케이션이 알아서 막겠지"보다 "DB가 직접 막게 만들자"가 기본 방향입니다.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
nickname TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);애플리케이션 검증과 DB 제약은 대체 관계가 아니라 중복 방어선이다
폼 검증이나 API 레벨 검증은 사용자 경험을 위해 필요하지만, 최종 무결성은 DB 제약이 막아야 합니다. 배치 작업, 관리자 콘솔, 다른 서비스, 수동 SQL 실행처럼 애플리케이션 경로를 우회하는 쓰기 경로가 항상 생기기 때문입니다. "프론트에서 이미 검사했으니 DB 제약은 생략"은 운영 단계에서 가장 먼저 무너지는 가정입니다.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);NOT NULL과 DEFAULT는 애플리케이션 코드가 아닌 DB가 규칙을 강제하게 만든다
NOT NULL이 없으면 해당 열에 NULL이 삽입될 수 있고, 애플리케이션이 값을 보내지 않으면 조용히 NULL이 저장됩니다. DEFAULT는 INSERT 시 값을 생략했을 때 자동으로 채워지는 값입니다. 이 두 제약을 DB 레벨에서 선언하면 애플리케이션 버그, 마이그레이션 실수, 직접 SQL 조작 등 어떤 경로로 데이터가 들어와도 기본 규칙이 지켜집니다. 특히 published BOOLEAN NOT NULL DEFAULT false처럼 기본 상태를 명시하면 INSERT 시 컬럼을 생략해도 안전합니다.
-- published를 생략해도 DEFAULT false가 적용된다
INSERT INTO posts (title) VALUES ('초안');
-- NOT NULL 위반은 DB가 즉시 오류로 거부한다
INSERT INTO posts (title, published) VALUES (NULL, true);
-- ERROR: null value in column "title" violates not-null constraintPRIMARY KEY는 행의 유일성을 보장하고 외래 키 참조의 기반이 된다
PRIMARY KEY는 내부적으로 UNIQUE + NOT NULL 제약과 B-tree 인덱스를 자동 생성합니다. 이 인덱스 덕분에 WHERE id = ? 조회는 Seq Scan 없이 Index Scan으로 처리됩니다. 또한 다른 테이블의 외래 키가 이 열을 참조하려면 반드시 PRIMARY KEY 또는 UNIQUE 제약이 있어야 합니다. BIGSERIAL을 사용하면 PostgreSQL이 시퀀스를 자동 관리해 INSERT마다 값을 할당합니다.
-- BIGSERIAL은 내부적으로 시퀀스를 생성하고 DEFAULT nextval('...')을 붙인다
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY
-- 아래와 같다:
-- id BIGINT NOT NULL DEFAULT nextval('posts_id_seq') PRIMARY KEY
);BIGSERIAL과 IDENTITY는 둘 다 자동 증가 키지만 새 설계는 IDENTITY도 고려할 수 있다
많은 PostgreSQL 스키마는 여전히 BIGSERIAL PRIMARY KEY를 많이 씁니다. 최근 표준 SQL 쪽에 더 가까운 표현은 GENERATED ... AS IDENTITY입니다. 둘 다 자동 증가 키를 만들지만, 기존 코드베이스와 일관성을 맞추는 게 더 중요할 때가 많습니다.
CREATE TABLE posts (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title TEXT NOT NULL
);UNIQUE 제약은 중복 방지와 함께 자동으로 인덱스를 만든다
UNIQUE 제약을 추가하면 PostgreSQL은 해당 열(또는 열 조합)에 자동으로 B-tree 인덱스를 생성합니다. 즉 email TEXT NOT NULL UNIQUE는 중복 이메일을 막으면서 동시에 WHERE email = ? 조회 성능도 확보합니다. 복합 UNIQUE 제약은 CONSTRAINT uq_user_slug UNIQUE (user_id, slug) 형태로 선언하며, 두 열의 조합이 유일해야 할 때 사용합니다.
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
slug TEXT NOT NULL,
title TEXT NOT NULL,
CONSTRAINT uq_post_user_slug UNIQUE (user_id, slug)
);체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 자동 증가 정수 ID가 필요할 때 | BIGSERIAL PRIMARY KEY |
| 가변 길이 문자열을 저장할 때 | TEXT (VARCHAR보다 관용적) |
| 값이 없을 때 기본값을 자동으로 채우려면 | DEFAULT 절 선언 |
| NULL 삽입 자체를 DB 수준에서 막으려면 | NOT NULL 제약 |
| 열 값의 중복을 금지하면서 조회도 빠르게 | UNIQUE 제약 |
주의할 점
테이블을 빠르게 만들기 위해 NOT NULL과 DEFAULT를 생략하면, 나중에 데이터가 쌓인 뒤
제약을 추가하기가 훨씬 어려워집니다. 컬럼을 NOT NULL로 바꾸려면 기존 NULL 행을 먼저 모두
채워야 하며, 대용량 테이블에서는 잠금이 발생할 수 있습니다. 스키마는 한 번 배포 후 변경 비용이
높으므로, 처음 설계할 때 제약을 충분히 선언하는 습관이 중요합니다.
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255)
);길이 제한이 특별한 비즈니스 규칙이 아닌데도 습관적으로 VARCHAR(255)를 쓰면, 나중에 길이를 늘릴 때 스키마 변경만 추가로 생깁니다. PostgreSQL에서는 제한 의미가 없으면 TEXT가 더 자연스럽습니다.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT
);애플리케이션에서만 이메일 필수 여부를 검사하고 DB에서 NOT NULL UNIQUE를 빼면, 배치나 수동 INSERT 경로로 NULL, 중복 이메일이 그대로 들어갈 수 있습니다. 핵심 식별 컬럼은 DB가 직접 막게 두는 편이 안전합니다.
참고 링크
1 sources