백엔드 데이터 인프라 9편. CREATE TABLE로 PostgreSQL의 첫 테이블을 만드는 표준 패턴과 데이터 타입·제약·기본 키 설계 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 9편이에요. 8편 SQL 기초 까지 "동사 5개" 를 다뤘으니, 이번 9편은 그 동사들이 작동할 그릇 — 테이블 을 직접 만들어요.
CREATE TABLE — DDL의 핵심
8편 의 5대 동사가 DML (Data Manipulation Language — 데이터 조작) 이라면, CREATE TABLE 은 DDL (Data Definition Language — 데이터 구조 정의). "이 시스템에 어떤 데이터가 어떤 형태로 박힐 것인가" 의 첫 선언이에요.
기본 형태는 이래요.
CREATE TABLE 테이블이름 (
컬럼1 타입 제약,
컬럼2 타입 제약,
...
);
가장 단순한 예
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE,
created TIMESTAMPTZ DEFAULT NOW()
);
이 한 문장이 표현하는 것:
- 테이블 이름 = users
- 컬럼 4개 — id·name·email·created
- 각 컬럼의 타입·제약 명시
PG 자주 쓰는 타입
32편 데이터 타입 에서 깊이 다루지만, 입문 단계에서 자주 쓰는 6가지만 추려요.
| 타입 | 의미 | 예 |
|---|---|---|
BIGSERIAL |
자동 증가 정수 (64bit) | id |
INTEGER / INT |
32bit 정수 | 카운트·금액 |
BIGINT |
64bit 정수 | 외래 키 (BIGSERIAL 가리킬 때) |
TEXT |
가변 길이 문자열 (사실상 무제한) | 이름·이메일·내용 |
VARCHAR(N) |
길이 제한 문자열 | 사용 안 권장 — TEXT 우세 |
BOOLEAN |
true/false | 활성 여부 |
TIMESTAMPTZ |
시간대 포함 시각 | created·updated |
DATE |
날짜만 | 생일·시작일 |
NUMERIC(p,s) |
정확한 소수 | 금액 |
JSONB |
인덱스 가능 JSON | 동적 메타데이터 |
TEXT vs VARCHAR — 한국에서 자주 헷갈리는
MySQL 출신자가 VARCHAR(255) 자주 박는데, PG는 TEXT 표준이에요. 길이 제한이 필요하면 CHECK 제약으로 따로 박는 게 PG 스타일.
CREATE TABLE posts (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL CHECK (char_length(title) <= 200)
);
PG 내부적으로는 TEXT·VARCHAR·CHAR가 거의 같은 성능이라, TEXT가 가장 단순해요.
제약 (Constraint) — 데이터 무결성
NOT NULL
name TEXT NOT NULL
값이 반드시 박혀야 해요. NULL 박으면 거부.
UNIQUE
email TEXT UNIQUE
다른 행과 같은 값 X. 자동으로 인덱스 생성.
PRIMARY KEY
id BIGSERIAL PRIMARY KEY
= NOT NULL + UNIQUE + 자동 인덱스 + "이 컬럼이 행 식별자" 명시. 테이블당 1개.
DEFAULT
created TIMESTAMPTZ DEFAULT NOW()
status TEXT DEFAULT 'PENDING'
INSERT 시 값 누락이면 기본값이 박혀요.
CHECK
amount INTEGER CHECK (amount >= 0)
age INTEGER CHECK (age BETWEEN 0 AND 150)
조건 위반하는 값은 거부.
FOREIGN KEY
user_id BIGINT NOT NULL REFERENCES users(id)
다른 테이블 기본 키 참조. 7편 관계형 모델 의 참조 무결성 보장.
다중 컬럼 제약
CREATE TABLE user_tags (
user_id BIGINT,
tag_id BIGINT,
PRIMARY KEY (user_id, tag_id), -- 복합 키
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
여러 컬럼을 합쳐서 키·외래 키를 만들어요.
실전 예 — orders 테이블
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
amount INTEGER NOT NULL CHECK (amount >= 0),
status TEXT NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'PAID', 'CANCELED', 'SHIPPED', 'DELIVERED')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
이 한 테이블 안에 기본 키·외래 키·NOT NULL·CHECK·DEFAULT 가 다 들어가 있어요. 한국 회사 백엔드의 표준 패턴.
IF NOT EXISTS — 멱등성
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
...
);
같은 이름 테이블이 이미 있으면 "무시". CI·마이그레이션 스크립트에 표준이에요.
테이블 수정 — ALTER TABLE
컬럼 추가
ALTER TABLE users ADD COLUMN nickname TEXT;
-- 기본값과 함께
ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'ACTIVE';
컬럼 수정
-- 타입 변경
ALTER TABLE users ALTER COLUMN name TYPE VARCHAR(100);
-- NOT NULL 추가·제거
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users ALTER COLUMN email DROP NOT NULL;
-- 기본값 변경
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'PENDING';
컬럼 삭제
ALTER TABLE users DROP COLUMN nickname;
컬럼 이름 변경
ALTER TABLE users RENAME COLUMN created TO created_at;
제약 추가·삭제
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
ALTER TABLE users DROP CONSTRAINT users_email_unique;
운영 환경 ALTER은 "테이블 락 위험" 이라 신중해야 해요. 큰 테이블은 트랜잭션·다운타임을 같이 고려.
DROP TABLE
DROP TABLE users;
-- 안전
DROP TABLE IF EXISTS users;
-- 외래 키로 참조하는 테이블도 함께
DROP TABLE users CASCADE;
CASCADE = 의존하는 객체 함께 삭제. 신중하게 써요.
테이블 구조 확인 — psql 메타
\dt -- 테이블 목록
\d users -- users 구조
\d+ users -- 더 자세히 (통계·인덱스 포함)
\d users 출력:
Table "public.users"
Column | Type | Collation | Nullable | Default
--------+--------------------------+-----------+----------+-------------------------
id | bigint | | not null | nextval('users_id_seq')
name | text | | not null |
email | text | | |
created| timestamp with time zone | | | now()
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
"users_email_key" UNIQUE CONSTRAINT, btree (email)
CREATE TABLE 시 자동으로 생긴 시퀀스·인덱스·제약이 한눈에 보여요.
Spring JPA 매핑
자바 백엔드 입문 45편 @Entity 의 자바 코드가 이 CREATE TABLE을 어떻게 만드는지 봐요.
@Entity
@Table(name = "users")
public class User {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(unique = true)
private String email;
@CreationTimestamp
private LocalDateTime created;
}
→ Hibernate가 자동 생성하는 SQL (ddl-auto: update 모드):
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE,
created TIMESTAMP
);
@Entity ↔ CREATE TABLE 1:1 매핑이 명확해야 운영 단계에서 "DB랑 코드 어긋남" 을 잡을 수 있어요.
함정 5가지
(1) VARCHAR(255) 습관
MySQL 출신자의 습관이에요. PG는 TEXT 우세, 길이 제한은 CHECK로.
(2) id 컬럼 누락
기본 키 없는 테이블 = "임시 데이터 또는 안티패턴". 거의 모든 테이블에 id 박아요.
(3) created_at·updated_at 누락
운영 디버깅에 필수인 컬럼. 모든 테이블에 박는 게 표준.
(4) DEFAULT NOW() vs @CreationTimestamp
DB 레벨 DEFAULT가 안전해요. 앱이 박지 않아도 자동으로 들어가요. 단, JPA가 INSERT 시 NULL을 박지 않도록 @PrePersist 또는 Hibernate 어노테이션으로 매핑해줘야 해요.
(5) ALTER TABLE 락
큰 테이블에 ALTER COLUMN ... TYPE 등을 박으면 테이블 전체 락이 걸려요 (수십 분~ 시간). 운영 환경은 온라인 DDL (pg_repack — PG 테이블 무중단 재구성 도구·Sleek·Liquibase 등) 또는 점진적 마이그레이션으로 풀어요.
id BIGSERIAL PRIMARY KEY + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(). 이 세 컬럼이 운영 디버깅 + 감사 추적의 기본.
한 줄 정리 — CREATE TABLE = DDL 핵심. BIGSERIAL·TEXT·TIMESTAMPTZ + NOT NULL·UNIQUE·PRIMARY KEY·DEFAULT·CHECK·REFERENCES 가 표준 제약 6개. id·created_at·updated_at 3 컬럼이 거의 모든 테이블 표준. ALTER는 운영에서 락 주의.
시험 직전 한 번 더 — CREATE TABLE 입문자가 매번 헷갈리는 것
CREATE TABLE name (col type constraint, ...)- BIGSERIAL = 자동 증가 64bit 정수 (기본 키 표준)
- TEXT = PG 문자열 표준 (VARCHAR 거의 안 씀)
- TIMESTAMPTZ = 시간대 포함 시각
- NUMERIC(p,s) = 정확한 소수 (금액)
- 제약 6개 = NOT NULL·UNIQUE·PRIMARY KEY·DEFAULT·CHECK·REFERENCES
- PRIMARY KEY = NOT NULL + UNIQUE + 자동 인덱스
- UNIQUE = 자동 인덱스
- 외래 키 =
REFERENCES users(id) - 다중 컬럼 키 =
PRIMARY KEY (a, b) - DEFAULT NOW() = INSERT 시 자동
- IF NOT EXISTS = 멱등성
- ALTER ADD COLUMN·ALTER COLUMN·DROP COLUMN·RENAME
- DROP TABLE [IF EXISTS] [CASCADE]
- 표준 컬럼 3개 = id · created_at · updated_at
\dt테이블 목록,\d table구조- @Entity ↔ CREATE TABLE 1:1 매핑
- VARCHAR(255) = MySQL 습관, PG는 TEXT
- 운영 ALTER = 테이블 락 주의
- 큰 테이블 = 온라인 DDL 도구 (pg_repack 등)
- 시퀀스 자동 생성 =
users_id_seq - CASCADE = 의존 객체 함께 (위험)
- CHECK 조건 = 데이터 무결성 강화
- 한국 회사 표준 = TEXT + CHECK 조합
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 4편 — psql 첫 접속과 기본 명령
- 5편 — 데이터베이스 만들기 CREATE DATABASE
- 6편 — PostgreSQL 데이터베이스 접속·전환
- 7편 — PostgreSQL 관계형 모델 핵심 개념
- 8편 — SQL 기초 5가지 동사
다음 글: