숏컷 코드
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
nickname TEXT UNIQUE,
name TEXT NOT NULL
);문법
PRIMARY KEY 는 행 식별과 외래 키 참조의 기준점이다
PRIMARY KEY 는 해당 컬럼의 값이 유일(UNIQUE)하고 비어 있지 않아야(NOT NULL) 한다는 두 제약을 동시에 선언합니다. 테이블당 하나만 설정할 수 있으며, 다른 테이블의 FOREIGN KEY 가 이 값을 참조합니다. PostgreSQL 은 PRIMARY KEY 선언 시 자동으로 B-Tree 인덱스를 생성하므로 기본 키 조회는 인덱스 스캔으로 처리됩니다. BIGSERIAL (내부적으로 BIGINT + 시퀀스)은 단조 증가하는 대리 키로 가장 일반적으로 쓰입니다.
-- 복합 기본 키: 두 컬럼의 조합이 유일성을 보장
CREATE TABLE order_items (
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id)
);PRIMARY KEY, UNIQUE, NOT NULL은 막는 대상이 다르다
PRIMARY KEY는 "행을 대표하는 값", UNIQUE는 "중복 금지", NOT NULL은 "빈 값 금지"를 담당합니다. 비슷해 보여도 역할이 다르므로 먼저 "식별자 문제인지, 중복 문제인지, 값 누락 문제인지"를 구분해서 제약을 고르는 편이 안전합니다.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, -- 행 식별
email TEXT NOT NULL UNIQUE, -- 값 필수 + 중복 금지
nickname TEXT UNIQUE -- 중복 금지, NULL은 허용
);PRIMARY KEY 와 UNIQUE NOT NULL 은 비슷해 보여도 의도가 다르다
둘 다 유일성과 비어 있지 않음을 보장할 수 있지만, PRIMARY KEY는 "이 테이블을 대표하는 기본 식별자"라는 의미까지 포함합니다. UNIQUE NOT NULL은 로그인 이메일, 외부 시스템 키처럼 후보 키를 만들 때 적합합니다. 다른 테이블이 참조할 수는 있어도, 무엇을 주 식별자로 삼는지까지 드러내려면 PRIMARY KEY가 더 직접적입니다.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE sessions (
id BIGSERIAL PRIMARY KEY,
user_email TEXT NOT NULL REFERENCES users(email)
);UNIQUE 는 중복 금지이지만 NULL 은 중복으로 보지 않는다
UNIQUE 제약은 해당 컬럼(또는 컬럼 조합)에 같은 값이 두 번 이상 들어오는 것을 막습니다. PostgreSQL 은 UNIQUE 선언 시에도 B-Tree 인덱스를 자동으로 생성하며, 이 인덱스는 유일성 검사와 쿼리 최적화 두 가지 목적에 모두 활용됩니다. 중요한 예외가 하나 있습니다. UNIQUE 컬럼에 NULL 이 여러 행에 들어가더라도 PostgreSQL 은 이를 중복으로 처리하지 않습니다. SQL 표준의 3값 논리에서 NULL = NULL 은 UNKNOWN 이기 때문입니다. "NULL 없이 유일"을 보장하려면 NOT NULL UNIQUE 를 함께 써야 합니다.
-- NULL 허용 UNIQUE: NULL 여러 개 허용
CREATE TABLE profiles (
user_id BIGINT PRIMARY KEY,
github_handle TEXT UNIQUE -- NULL 이 여러 행에 공존 가능
);
-- NULL 불허 UNIQUE: 진짜 완전한 유일성 보장
CREATE TABLE users (
email TEXT NOT NULL UNIQUE
);NOT NULL 은 값이 없는 상태 자체를 금지하는 데이터 품질 선언이다
NOT NULL 은 해당 컬럼에 NULL 이 저장되는 것을 금지합니다. 생성 시각, 상태 코드, 핵심 식별자처럼 행의 의미를 완성하는 데 반드시 필요한 값에 적용합니다. 애플리케이션 레이어에서도 유효성 검사를 하더라도, 데이터베이스 차원의 NOT NULL 이 있어야 직접 SQL 실행이나 다른 서비스 경로로 들어오는 데이터도 막을 수 있습니다. NOT NULL 은 별도 인덱스를 만들지 않으므로 성능 비용 없이 추가할 수 있습니다.
-- 컬럼 수준 제약
ALTER TABLE posts
ALTER COLUMN title SET NOT NULL;
-- DEFAULT 와 함께: 값 없이 INSERT 해도 기본값이 채워짐
CREATE TABLE events (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);제약 위반은 트랜잭션 전체를 롤백시킨다
PRIMARY KEY, UNIQUE, NOT NULL 위반은 즉시 오류를 발생시키고 현재 트랜잭션을 중단합니다. ON CONFLICT 절을 사용하면 UNIQUE / PRIMARY KEY 충돌 시 오류 대신 무시(DO NOTHING)하거나 갱신(DO UPDATE)하는 방식으로 처리할 수 있습니다. 제약 이름을 명시적으로 지정해 두면 오류 메시지에서 어떤 제약이 위반됐는지 바로 확인할 수 있습니다.
-- 제약에 이름 부여 (오류 메시지에서 원인 파악 용이)
ALTER TABLE users
ADD CONSTRAINT users_email_unique UNIQUE (email);
-- 충돌 시 무시
INSERT INTO users (email, name)
VALUES ('ada@example.com', 'Ada')
ON CONFLICT (email) DO NOTHING;선택 기준
| 상황 | 적합한 선택 |
|---|---|
| 행을 고유하게 식별해야 할 때 | PRIMARY KEY |
| NULL 허용하면서 중복만 막을 때 | UNIQUE |
| 반드시 값이 있어야 하고 중복도 안 될 때 | NOT NULL UNIQUE |
| 다른 테이블에서 이 값을 참조해야 할 때 | PRIMARY KEY (또는 UNIQUE NOT NULL) |
| 조합 유일성이 필요할 때 | 복합 UNIQUE (col1, col2) |
주의할 점
UNIQUE 만 있다고 해서 그 컬럼이 반드시 채워지지는 않습니다. PostgreSQL 은 UNIQUE 컬럼의 NULL 여러 개를 중복으로 처리하지 않습니다.
"반드시 있어야 하고 중복도 안 된다"면 NOT NULL 과 UNIQUE 를 반드시 함께 선언해야 합니다.
PRIMARY KEY 는 두 제약을 자동으로 포함하므로 대리 키 컬럼은 PRIMARY KEY 선언만으로 충분합니다.
CREATE TABLE users (
nickname TEXT UNIQUE
);
INSERT INTO users (nickname) VALUES (NULL);
INSERT INTO users (nickname) VALUES (NULL);위 두 INSERT는 둘 다 성공합니다. "NULL도 하나만 허용되겠지"라고 기대하면 설계 의도와 달라질 수 있습니다.
-- 애플리케이션에서만 중복 검사를 했다고 가정
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada');
INSERT INTO users (email, name) VALUES ('ada@example.com', 'Ada 2');중복 검사 로직을 애플리케이션에서만 처리하면 동시 요청 두 개가 같은 값을 통과시킬 수 있습니다. 최종 중복 방지는 항상 UNIQUE 또는 PRIMARY KEY 제약이 맡아야 합니다.
참고 링크
1 sources