Quick Comparison
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
nickname TEXT UNIQUE,
name TEXT NOT NULL
);행의 대표 식별자는 PRIMARY KEY, 필수 후보 키는 NOT NULL UNIQUE, 값이 없어도 되는 선택 속성의 중복 금지는 기본 UNIQUE를 씁니다. PostgreSQL의 기본 UNIQUE는 NULL 여러 개를 허용합니다. NULL도 하나만 허용해야 하는 실제 규칙이면 PostgreSQL 15+의 UNIQUE NULLS NOT DISTINCT를 명시합니다.
문법
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
);NULL도 하나만 허용해야 하는 규칙은 NOT NULL로 바꾸는 것과 다릅니다. 값이 없는 행은 허용하되 그 상태는 한 번만 허용하려면 NULLS NOT DISTINCT를 선택합니다. 이 문법은 PostgreSQL 15 이상에서 사용할 수 있습니다.
CREATE TABLE profiles (
primary_contact_email text UNIQUE NULLS NOT DISTINCT
);
INSERT INTO profiles VALUES (NULL); -- 성공
INSERT INTO profiles VALUES (NULL); -- unique_violationNOT 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 위반은 오류를 발생시키고 현재 트랜잭션을 실패 상태로 만듭니다. savepoint가 없다면 이어지는 명령은 ROLLBACK 전까지 실행되지 않습니다. UNIQUE·PRIMARY KEY를 DEFERRABLE로 선언했다면 오류가 SET CONSTRAINTS나 COMMIT에서 발생할 수도 있습니다. 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 |
| NULL을 허용하되 NULL도 하나만 허용할 때 | PostgreSQL 15+ UNIQUE NULLS NOT DISTINCT |
| 다른 테이블에서 이 값을 참조해야 할 때 | 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