psql 내부 기본 메타 명령어
- \l: 현재 DB 목록
- \dt: 테이블 목록
- \c db이름: 특정 DB로 이동
- \d 테이블명: 테이블 구조 보기
- \q: 종료
CREATE TABLE
- 컬럼 정의: 타입, DEFAULT, NOT NULL, GENERATED …
- 제약조건(Constraints): PK/UK/FK/CHECK 등 “규칙(무결성)”
- 물리/운영 옵션: TEMP, UNLOGGED, PARTITION, TABLESPACE 등 “저장/운영 방식”
1) 컬럼 정의(Column definition)
column_name data_type [COLLATE collation] [CONSTRAINT ...]
[NOT NULL | NULL]
[DEFAULT expr]
[GENERATED ...]
[IDENTITY ...]
- data_type: 무엇을 저장하나(형식/범위/정렬), 어떤 값인가?
- NULL/NOT NULL: 값이 비어도 되나?
- DEFAULT: 값을 안 주면 무엇이 들어가야 하나?
- GENERATED: DB가 자동으로 계산/생성해야 하나?
- IDENTITY: 숫자 자동 증가(키 생성)
- 계산이면 GENERATED
- 자동 증가면 IDENTITY
- 정렬 규칙이 필요하면 COLLATE
(1) data_type (타입)
컬럼에 저장될 값의 형식/표현/범위를 결정한다.
id bigint
name text
email varchar(100)
age integer
created_at timestamptz
PostgreSQL에서 자주 쓰는 타입
숫자
- smallint (2바이트)
- integer 또는 int (4바이트)
- bigint (8바이트)
- numeric(p,s) (정확한 소수와 10진수, 돈/정산용)
- 팁:
- PK는 보통 bigint가 안정적(서비스 커지면 int 범위 초과 가능)
- 돈은 float/double 쓰지 말고 numeric(정확도 문제)
문자열
- text (길이 제한 없음, 필요하면 CHECK로 길이 강제 가능)
- varchar(n) (최대 길이 제한, 길이 제한이 도메인 규칙일 때 사용)
- Postgresql 특징:
- text와 varchar는 성능 차이가 거의 없고,
- varchar(n)는 “길이 제한을 DB가 강제”하는 의미가 크다.
날짜/시간
- date
- timestamp (timezone 없음)
- timestamptz (timezone 포함: 내부는 UTC로 저장 + 표시할 때 변환)
- created_at/updated_at 같은 “로그성 시간”은 timestamptz가 안전하다.
불리언
- boolean (true/false)
JSON
- jsonb (실무에서 주로 사용: 인덱싱/연산이 좋음)
- json (원문 그대로 보존 목적)
MySQL 차이
- 문자열: varchar(n) 사용 비율이 높음 (text도 있지만)
- 시간: timezone 처리가 Postgres timestamptz처럼 “타입 자체로 강제”되진 않는 느낌이 있음(설계로 통제)
- JSON 타입 존재(버전 의존)
Oracle 차이
- 문자열: VARCHAR2
- 숫자: NUMBER
- 시간: DATE, TIMESTAMP (timezone 변형 타입도 있음)
(2) NULL / NOT NULL
“값이 비어도 되는지”를 DB가 강제한다.
name text NOT NULL
age int NULL -- (기본이 NULL 허용이라 보통 안 씀)
- “비면 안 되는 값”은 DB에서 NOT NULL로 막는 게 안전하다.
- NOT NULL이면 PostgreSQL 내부에서 INSERT/UPDATE 때 NULL 여부를 검사한다.
- PRIMARY KEY는 자동으로 NOT NULL을 포함한다.
(3) DEFAULT
INSERT에서 값을 주지 않았을 때 넣을 기본값을 정의한다. “DEFAULT는 없을 때만 적용”
created_at timestamptz DEFAULT now()
age int DEFAULT 0
status text DEFAULT 'active'
- INSERT에서 컬럼을 생략하면 DEFAULT가 들어감
- INSERT에서 명시적으로 NULL을 넣으면 NULL이 들어감(그리고 NOT NULL이면 에러)
INSERT INTO users(name) VALUES ('a'); -- created_at 기본값 들어감
INSERT INTO users(name, created_at) VALUES ('a', NULL); -- NULL(가능하면)
DEFAULT의 타이밍
- now() / transaction_timestamp():
- 트랜잭션 시작 시각 기준(같은 트랜잭션 내 동일)
- created_at에는 보통 now()가 일관성 좋음.
- clock_timestamp():
- 호출 순간의 실제 시각(행마다 달라질 수 있음)
MySQL 차이
- DEFAULT CURRENT_TIMESTAMP 같은 문법을 자주 씀
- 버전/설정에 따라 timestamp 기본값 동작이 Postgres와 약간 다르게 느껴질 수 있음
Oracle 차이
- 기본값으로 DEFAULT SYSTIMESTAMP 같은 것을 씀
(4) GENERATED (계산 컬럼)
Postgres에서는 “다른 컬럼을 이용해 자동 계산되는 컬럼”을 만들 수 있다.
full_name text
GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED
- full_name 값은 사용자가 직접 입력하지 않고 DB가 계산해서 유지
- 자주 조합되는 표시용 값이나 검색/정렬/인덱스가 필요한 파생 값이 필요할 때 사용
STORED
- 계산 결과를 실제로 저장한다(조회가 빠름)
- 반대로 “계산만 하고 저장은 안 하는” 형태는 DB마다 지원 수준이 다름
MySQL/Oracle 차이
- 둘 다 generated column 개념은 있음
- 표현식/저장 방식/제약은 DB마다 조금씩 차이
(5) IDENTITY (자동 증가)
id bigint GENERATED ALWAYS AS IDENTITY
- INSERT에서 id를 안 주면 자동으로 증가값을 넣어준다.
- Postgres에서 SERIAL 대체.
- 내부적으로 “시퀀스”라는 별도 오브젝트를 만들고, id의 기본값이 “시퀀스 next value”로 연결된다. (“테이블 + 시퀀스”가 같이 생기는 구조)
ALWAYS vs BY DEFAULT
id bigint GENERATED ALWAYS AS IDENTITY
- ALWAYS는 원칙적으로 사용자가 id를 직접 넣는 걸 막음(안전)
id bigint GENERATED BY DEFAULT AS IDENTITY
- DEFAULT는 자동, 필요하면 수동 삽입도 가능(마이그레이션/데이터 이관 때 편함)
SERIAL과 IDENTITY의 차이
- SERIAL: 관례적 문법 설탕(자동으로 여러 작업을 수행)
- IDENTITY: 표준에 가깝고 “이 컬럼은 identity다”가 메타데이터로 남아 관리가 명확
MySQL/Oracle 차이
- MySQL: AUTO_INCREMENT
- Oracle: GENERATED AS IDENTITY 사용
(6) COLLATE (정렬/비교 규칙)
문자열 비교/정렬 규칙을 컬럼 단위로 지정할 수 있다
name text COLLATE "C"
- 정렬 순서를 특정 규칙으로 강제해야 할 때
- 로케일/대소문자 정렬이 민감할 때
- “대부분 기본 collation”을 쓰지만, 검색/정렬 이슈 터질 때 COLLATE가 도움이 된다.
2) 제약조건(Constraints)
제약조건은 한 줄로 말하면, “DB가 데이터가 잘못 들어오는 걸 물리적으로 막는 장치” — 규칙과 무결성
제약조건의 종류 핵심 4개
- PRIMARY KEY (PK)
- UNIQUE (UK)
- FOREIGN KEY (FK)
- CHECK
내부적으로
- PK / UNIQUE는 “인덱스까지 같이 생성됨” → 조회 성능과 직결
- FK / CHECK는 “규칙만 생성됨” → 성능은 별도 인덱스 설계 필요
이 차이가 성능/락/설계까지 이어진다.
(1) PRIMARY KEY (PK)
행(row)을 유일하게 식별하는 기준.
id bigint PRIMARY KEY
또는 테이블 레벨에서 작성:
CONSTRAINT users_pk PRIMARY KEY (id)
PK를 만들면 DB는 3가지를 동시에 보장한다. (PK = 규칙 + 인덱스)
- NOT NULL 강제
- UNIQUE 강제
- 유니크 인덱스 생성
PostgreSQL에서는 B-tree 유니크 인덱스가 자동으로 만들어진다.
- PK는 조회 성능의 기준이 된다
- FK 참조의 기준이 된다
- ORM(JPA 등)에서 엔티티 식별 기준
복합 PK
PRIMARY KEY (tenant_id, user_id)
- 두 컬럼의 조합이 유일해야 함
- 다중 테넌트, 매핑 테이블에 사용됨.
(2) UNIQUE (UK)
“중복 금지” 규칙
- 내부적으로 유니크 인덱스 생성됨
email text UNIQUE
또는:
CONSTRAINT users_email_uk UNIQUE (email)
복합 유니크:
UNIQUE (tenant_id, email)
PostgreSQL에서 중요한 특징 UNIQUE는 NULL을 “값 없음”으로 보며 NULL끼리는 서로 다르다고 판단한다.
email = NULL
email = NULL
→ 여러 개 허용된다.
email text NOT NULL UNIQUE
그래서 실무에서는 NOT NULL + UNIQUE 조합이 일반적이다.
(3) FOREIGN KEY (FK)
“다른 테이블의 값과 연결된다는 규칙”
FOREIGN KEY (user_id)
REFERENCES users(id)
- orders.user_id는 users.id에 존재해야만 한다.
- FK는 인덱스를 자동으로 만들지 않는다. 대신:
- INSERT 시 참조 존재 검사
- UPDATE 시 참조 검사
- DELETE 시 연쇄 규칙 검사
연쇄 규칙
ON DELETE CASCADE
ON UPDATE RESTRICT
옵션 설명:
| CASCADE | 부모 삭제 → 자식도 삭제 (위험: parent 한 번 삭제 → child 수십만 row 삭제 로그/결제/주문 → CASCADE 거의 안 쓴다, 매핑/종속 데이터 → CASCADE 가능) |
| RESTRICT | 참조 중이면 삭제 불가 |
| NO ACTION | 기본값 (RESTRICT와 유사) |
| SET NULL | 부모 삭제 → 자식 FK를 NULL |
| SET DEFAULT | DEFAULT로 변경 |
FK 인덱스
CREATE INDEX orders_user_id_idx ON orders(user_id);
- Postgres는 FK 컬럼에 인덱스를 자동 생성하지 않는다.
- 그래서 이걸 안 하면 부모 삭제 시 → 자식 전체 스캔 발생 → 성능 급락 + lock 증가
DEFERRABLE (고급)
FOREIGN KEY (...) REFERENCES ...
DEFERRABLE INITIALLY DEFERRED
- FK 검사 시점을 “커밋 시점”으로 미룸
- 서로 참조하는 데이터 삽입이나 트랜잭션 내부 순서 제약이 있는 경우 사용
(4) CHECK
값의 범위/형식 규칙 강제.
age int CHECK (age BETWEEN 0 AND 150)
CHECK (status IN ('active', 'inactive'))
- INSERT/UPDATE 때 표현식 평가한다.
- 규칙 위반 시 에러 발생
- CHECK는 DB에서 마지막 방어선 역할을 함.
(5) 제약조건 예제
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY,
tenant_id bigint NOT NULL,
name text NOT NULL,
email text NOT NULL,
age int CHECK (age BETWEEN 0 AND 150),
CONSTRAINT users_pk PRIMARY KEY (id),
CONSTRAINT users_email_uk UNIQUE (tenant_id, email),
CONSTRAINT users_tenant_fk
FOREIGN KEY (tenant_id)
REFERENCES tenants(id)
ON DELETE RESTRICT
);
그리고:
CREATE INDEX users_tenant_idx ON users(tenant_id);
3) 물리 / 운영 옵션 개요
CREATE TABLE은 사실 “데이터 저장 방식”까지 같이 설계하는 명령이다.
“디스크 + 로그 + 복구 + 성능”과 직결되는 대표 옵션:
- TEMP / TEMPORARY
- UNLOGGED
- PARTITION
- TABLESPACE
| TEMP | 세션/트랜잭션 |
| UNLOGGED | WAL / 복구 |
| PARTITION | 성능 / 대용량 |
| TABLESPACE | 디스크 / 운영 |
(1) TEMP TABLE
“세션 또는 트랜잭션 동안만 존재하는 테이블”
- 세션 종료 → 자동 삭제
- 다른 세션에서는 보이지 않음
CREATE TEMP TABLE temp_users (...);
또는
CREATE TEMPORARY TABLE ...
내부 동작
- 일반 테이블처럼 디스크에 쓰일 수도 있음
- WAL 로그는 최소화
- session scope에서만 존재
1) 중간 계산
SELECT ...
INTO TEMP temp_result
FROM ...
2) 배치 처리: 복잡한 JOIN 결과 저장, 집계 중간 단계
3) 성능 튜닝: 반복 계산 방지
운영 시 이슈
- Connection pool 환경에서 문제 발생 가능.
- 예들 들어, Spring / Node, 세션 재사용 시 TEMP TABLE이 예상보다 오래 살아남을 수 있음.
- 그래서 웹 서비스에서는 TEMP TABLE을 신중하게 사용
(2) UNLOGGED TABLE
CREATE UNLOGGED TABLE logs_ingest (...);
- WAL(Write-Ahead Log) 로그를 남기지 않는 테이블
- PostgreSQL는 데이터 쓰기 전에 로그 먼저 기록한다. → 장애 시 복구 가능 / UNLOGGED로 만들면 로그를 기록하지 않음.
- 장점: INSERT 속도 빠름, 디스크 I/O 감소
- 단점: DB crash → 데이터 사라질 수 있음, replication 대상 아님
- 캐시 테이블, 임시 데이터, ETL 중간 결과 등에는 사용가능하지만,
- 주문, 결제, 회원 등에는 절대로 사용해서는 안된다.
(3) PARTITION TABLE
대용량 DB의 핵심이자 거의 필수.
- 내부적으로는 부모 테이블은 “메타”로 실제 데이터는 자식 파티션에 저장됨
CREATE TABLE events (
id bigint,
created_at timestamptz
) PARTITION BY RANGE (created_at);
테이블을 여러 물리 테이블로 나눔.
events
├─ events_2025
├─ events_2026
└─ events_2027
- 대용량 테이블을 사용하면, 수억 row, 인덱스 커짐, 조회 느림, vacuum 부담 등이 발생
- 이를 파티션으로 해결이 가능하다. (조회 성능 향상, 백업/삭제 쉬움, 아카이빙 편함)
- 범위별로 나눠 저장
- 필요한 파티션만 조회
RANGE: 시간 기반.
PARTITION BY RANGE (created_at)
LIST: 카테고리.
PARTITION BY LIST (country)
HASH: 분산.
PARTITION BY HASH (user_id)
(4) TABLESPACE
- 성능 튜닝, 스토리지 분리, 대규모 운영 시 사용됨.
CREATE TABLE users (...) TABLESPACE fast_disk;
- 테이블을 저장할 “디스크 위치” 지정
- 서버에 디스크 여러 개 존재 시 필요하다.
- 테이블을 특정 물리 위치로 보냄.
SSD
HDD
Network Storage
cluster
├─ tablespace A
├─ tablespace B
└─ default
실제 테이블 분석
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE,
age INT,
created_at TIMESTAMP DEFAULT NOW()
);
겉으로는 테이블 하나지만, 실제로는 최소 이것들이 만들어진다:
- 테이블 users
- 컬럼 타입/제약의 메타데이터 (시스템 카탈로그에 기록)
- SERIAL 때문에 시퀀스(sequence) 가 추가로 생성되고, id의 DEFAULT로 연결됨
- PRIMARY KEY 때문에 유니크 인덱스가 생성됨 (Postgres는 PK를 “인덱스”로 구현)
- UNIQUE(email) 때문에 유니크 인덱스가 추가로 생성됨
- DEFAULT NOW() 는 실행 시점마다 평가되는 default expression로 저장됨
즉 “테이블 1개”가 아니라 오브젝트 여러 개가 묶음으로 생긴다.
1) id SERIAL PRIMARY KEY
(1) SERIAL의 정체
- SERIAL은 진짜 타입(type)이 아니다. 문법 설탕(syntactic sugar) 이다.
- SERIAL은 “auto increment” 기능을 만들어주는 패키지 문법이라고 보면 된다.'
- 요즘 Postgres에서는 SERIAL보다 GENERATED ... AS IDENTITY를 권장하는 흐름이 많다. (표준 SQL에 가깝고 관리가 더 명확함)
- 내부적으로 이렇게 바꿔서 처리한다(개념적으로):
- id integer NOT NULL
- CREATE SEQUENCE users_id_seq
- id DEFAULT nextval('users_id_seq')
- 시퀀스 소유 연결 (ALTER SEQUENCE ... OWNED BY users.id)
- PK 인덱스 생성
권장 예시:
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY
또는
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
- ALWAYS: 사용자가 직접 넣는 걸 기본적으로 막음
- BY DEFAULT: 필요하면 직접 값 삽입 가능
(2) PRIMARY KEY의 의미
PK는 2가지를 강제한다:
- NOT NULL
- UNIQUE
그리고 Postgres에서는 이를 구현하기 위해 유니크 B-tree 인덱스가 만들어진다.
2) name VARCHAR(50) NOT NULL
(1) VARCHAR(50)의 의미
- 문자열 길이 제한(최대 50)
- 다만 Postgres에서는 varchar(n)과 text의 성능 차이가 거의 없고, varchar(n)은 “검증(제약)” 목적이 크다.
- 엄격한 길이 제한이 의미 있는 도메인이면 varchar(n) 그렇지 않으면 text + 체크 제약/앱 검증으로도 충분
(2) NOT NULL
- NULL을 허용하지 않는다.
- 인덱스/쿼리 측면에서 NULL은 3-valued logic(참/거짓/Unknown) 때문에 조건이 복잡해질 수 있어, 도메인 상 필수면 NOT NULL이 좋다.
3) email VARCHAR(100) UNIQUE
UNIQUE는 결국 유니크 인덱스 생성이다.
- UNIQUE는 NULL을 “특수하게” 다룬다.
- PostgreSQL에서 UNIQUE는 NULL을 서로 다르다고 취급해서,
- email이 NULL인 행을 여러 개 넣을 수 있다.
- email을 정말 유일하게 강제하려면:
- email NOT NULL UNIQUE 가 가장 흔함
- “활성 사용자만 유니크” 같은 조건부 유니크 Partial unique index(부분 유니크 인덱스)를 쓴다.
CREATE UNIQUE INDEX users_email_unique_active
ON users(email)
WHERE deleted_at IS NULL;
4) age INT
INT는 integer의 별칭(alias). 여기서 깊게 들어가면 “도메인 제약”이 중요하다.
- age는 0~150 같은 범위를 강제하고 싶다면 CHECK가 더 안전하다.
- 이건 앱 검증을 뚫고 들어오는 데이터(배치, 다른 서비스, 운영 실수)를 DB에서 막는다.
age INT CHECK (age BETWEEN 0 AND 150)
5) created_at TIMESTAMP DEFAULT NOW()
(1) TIMESTAMP vs TIMESTAMPTZ
- TIMESTAMP : timezone 정보 없음(그냥 “벽시계 시간”)
- TIMESTAMPTZ : 실제로는 UTC 저장 + 표시할 때 timezone 적용
- 실무에서 생성 시간(created_at)은 거의 항상 timestamptz가 더 안전하다.
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
(2) NOW()가 언제 평가되나
PostgreSQL에서 now()는 보통 “트랜잭션 시작 시각” 기준으로 동작한다(여러 row를 insert해도 동일한 값이 들어갈 수 있음). created_at은 일반적으로 now()가 맞다(트랜잭션 일관성).
반면 “실제 현재 시각”이 필요하면 clock_timestamp()를 쓴다.
- now() / transaction_timestamp() : 트랜잭션 시간
- clock_timestamp() : 호출 순간의 진짜 시간
MySQL
- auto increment는 AUTO_INCREMENT
- DB=스키마 개념으로 묶여 있어서 권한 구조가 Postgres와 다르게 느껴짐
- UNIQUE에서 NULL 처리도 DB/버전에 따라 차이가 체감될 수 있음(대체로 NULL 여러 개 허용)
id BIGINT AUTO_INCREMENT PRIMARY KEY
Oracle
- 전통적으로는 “시퀀스 + 트리거” 조합이 많았음
- 최근 Oracle은 identity 컬럼 지원
- 스키마=사용자 개념이 강함
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
6) 처음 예제를 안전 버전으로 업그레이드
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
age INT CHECK (age BETWEEN 0 AND 150),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
- SERIAL → IDENTITY
- created_at → timestamptz
- email → not null + unique
- age → check로 도메인 강제
문제 1 — 기본 테이블 생성
users 테이블 생성.
컬럼:
- id → 자동 증가 PK
- username → VARCHAR(30), NOT NULL
- email → UNIQUE
- age → 0 이상만 허용
- created_at → 기본값 현재시간
요구사항:
- PK
- UNIQUE
- CHECK
- DEFAULT 포함
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
username VARCHAR(30) NOT NULL,
email TEXT NOT NULL UNIQUE,
age INT CHECK (age >= 0),
created_at TIMESTAMP DEFAULT NOW()
);
문제 2 — FK 포함 테이블
orders 테이블 생성.
컬럼:
- id → 자동 증가 PK
- user_id → users.id 참조
- total_price → 0 이상
- status → 기본값 'PENDING'
- created_at → 현재시간
조건:
- FK
- CHECK
- DEFAULT
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
total_price NUMERIC NOT NULL CHECK (total_price >= 0),
status VARCHAR(30) NOT NULL
CHECK (status IN ('pending', 'processing', 'delivered'))
DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT orders_user_id_fk
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE RESTRICT
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
- 실제로 많이 사용하며, ON DELETE RESTRICT는 기본값이기 때문에 생략 가능하다.
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total_price NUMERIC NOT NULL CHECK (total_price >= 0),
status VARCHAR(30) NOT NULL
CHECK (status IN ('pending', 'processing', 'delivered'))
DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
- 둘 다 가능하지만, 참조 컬럼을 옆에 쓰면 복합 FK는 사용할 수 없다.
문제 3 — 복합 UNIQUE
products 테이블 생성.
컬럼:
- id → 자동 증가 PK
- name → NOT NULL
- category → NOT NULL
- price → 0 이상
- created_at → 현재시간
조건:
- (name, category) 조합 UNIQUE
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
category TEXT NOT NULL,
price NUMERIC NOT NULL CHECK(price >= 0),
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT name_category_uk
UNIQUE(name, category)
)
- 상품 카테고리당 이름 중복 금지를 위해 사용했지만
- UNIQUE 제약을 만들면 → 자동으로 인덱스 생성되므로
- 성능도 향상의 이점도 얻을 수 있다.
- (user_id, product_id) UNIQUE 이런 식으로 한 사용자가 같은 상품에 리뷰 1개만 달 수 있도록 한다.
ALTER TABLE products DROP CONSTRAINT name_category_uk;
- 복함 UNIQUE 삭제 시 사용
문제 4 — INDEX까지 포함
reviews 테이블 생성.
컬럼:
- id → identity PK
- user_id → users FK
- product_id → products FK
- rating → 1~5 범위만 허용
- content → TEXT
- created_at → 현재시간
조건:
- FK 2개
- rating CHECK
- rating 컬럼에 INDEX 생성
- (user_id, product_id) 복합 UNIQUE 추가
(같은 유저가 같은 상품 리뷰 1개만 작성 가능)
CREATE TABLE reviews (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
content TEXT,
created_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT reviews_user_id_fk
FOREIGN KEY (user_id)
REFERENCES users(id)
ON DELETE NO ACTION,
CONSTRAINT reviews_product_id_fk
FOREIGN KEY (product_id)
REFERENCES products(id)
ON DELETE NO ACTION,
CONSTRAINT user_product_uk UNIQUE (user_id, product_id)
);
CREATE INDEX idx_reviews_rating ON reviews(rating);
문제 5 — GENERATED 컬럼
payments 테이블 생성.
컬럼:
- id → 자동 증가 PK
- amount → INT
- tax → INT
- total → amount + tax 자동 계산
- created_at → 현재시간
조건:
- GENERATED ALWAYS AS 사용
CREATE TABLE payments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
amount INTEGER NOT NULL CHECK (amount >= 0),
tax INTEGER NOT NULL CHECK (tax >= 0),
total NUMERIC GENERATED ALWAYS AS (amount + tax) STORED,
created_at TIMESTAMP DEFAULT NOW()
);
- NOT NULL + CHECK 조합을 잊지말 것!
문제 6 — 운영 옵션
logs 테이블 생성.
컬럼:
- id → BIGSERIAL PK
- message → TEXT
- created_at → 현재시간
조건:
- UNLOGGED 테이블
CREATE TABLE events (
id BIGINT GENERATED ALWAYS AS IDENTITY,
type TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
PRIMARY KEY(id, created_at)
)
PARTITION BY RANGE (created_at);
- PK를 바로 쓰지 못하고, partition table의 PK/UNIQUE는 partition key를 포함해야 함.
- PRIMARY KEY (id, created_at)에서 CONSTRAINT events_pk을 생략한 형태
자식 파티션 생성
CREATE TABLE events_2026_01
PARTITION OF events
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE events_2026_02
PARTITION OF events
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
- 1월 데이터 → events_2026_01
- 2월 데이터 → events_2026_02
- 보통 자식은 배포 파이프라인에서 SQL migration으로 만들거나, 또는 스케줄러/크론으로 생성한다.
자동 분산 저장됨. 조회는 부모 테이블만 보면 됨.
INSERT INTO events(type, created_at)
VALUES ('login', '2026-01-15');
INSERT INTO events(type, created_at)
VALUES ('logout', '2026-02-10');
- INSERT를 해보면 만들어지지 않은 이벤트에는 INSERT 되지 않는다.
INSERT INTO events (...) VALUES (...);
UPDATE events SET ... WHERE ...;
DELETE FROM events WHERE ...;
- NSERT/UPDATE/DELETE는 부모 테이블로만 한다
CREATE INDEX ON events (created_at);
- 파티션 테이블에서 이렇게 부모에 만들면, PostgreSQL이 보통 각 파티션에 인덱스를 만들어준다
문제 6 — 운영 옵션
logs 테이블 생성.
컬럼:
- id → BIGSERIAL PK
- message → TEXT
- created_at → 현재시간
조건:
- UNLOGGED 테이블
CREATE UNLOGGED TABLE logs (
id BIGSERIAL PRIMARY KEY,
message TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);'Database > SQL' 카테고리의 다른 글
| INSERT, UPDATE, DELETE (0) | 2026.02.10 |
|---|---|
| ALTER, DROP, TRUNCATE (0) | 2026.02.10 |
| String, Date (0) | 2026.02.09 |
| GROUP BY (0) | 2026.02.09 |
| JOIN (0) | 2026.02.08 |