Skip to content

필사 모드: PostgreSQL 파티셔닝 완벽 가이드: Range, List, Hash 전략과 성능 최적화

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

파티셔닝이 필요한 시점

테이블이 커지면 성능 문제가 발생합니다:

- 인덱스 크기 증가로 INSERT 성능 저하

- 풀 테이블 스캔 비용 증가

- VACUUM 작업 시간 증가

- 데이터 보관/삭제 비용 증가

일반적으로 **테이블 크기가 수십 GB 이상**이거나, **시계열 데이터로 일정 기간 후 삭제**가 필요한 경우 파티셔닝을 고려합니다.

Range 파티셔닝: 시계열 데이터

가장 많이 사용되는 전략으로, 날짜나 ID 범위로 데이터를 분할합니다.

월별 파티셔닝 생성

-- 부모 테이블 생성

CREATE TABLE events (

id BIGSERIAL,

event_type VARCHAR(50) NOT NULL,

payload JSONB,

created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

PRIMARY KEY (id, created_at)

) PARTITION BY RANGE (created_at);

-- 월별 파티션 생성

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');

CREATE TABLE events_2026_03 PARTITION OF events

FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');

-- 기본 파티션 (범위에 맞는 파티션이 없을 때)

CREATE TABLE events_default PARTITION OF events DEFAULT;

파티션별 인덱스

-- 각 파티션에 자동으로 생성되는 글로벌 인덱스

CREATE INDEX idx_events_type ON events (event_type);

CREATE INDEX idx_events_payload ON events USING GIN (payload);

-- 파티션별 로컬 인덱스

CREATE INDEX idx_events_2026_03_type

ON events_2026_03 (event_type, created_at DESC);

파티션 프루닝 확인

-- 파티션 프루닝이 작동하는지 EXPLAIN으로 확인

EXPLAIN (ANALYZE, BUFFERS)

SELECT * FROM events

WHERE created_at >= '2026-03-01'

AND created_at < '2026-03-15'

AND event_type = 'purchase';

-- 결과: events_2026_03 파티션만 스캔

-- Append

-- -> Index Scan using events_2026_03_type on events_2026_03

-- Index Cond: (event_type = 'purchase')

-- Filter: (created_at >= '2026-03-01' AND created_at < '2026-03-15')

List 파티셔닝: 카테고리별 분할

특정 값 목록으로 데이터를 분할합니다:

-- 지역별 파티셔닝

CREATE TABLE orders (

id BIGSERIAL,

customer_id BIGINT NOT NULL,

amount DECIMAL(12,2) NOT NULL,

region VARCHAR(10) NOT NULL,

status VARCHAR(20) NOT NULL,

ordered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

PRIMARY KEY (id, region)

) PARTITION BY LIST (region);

CREATE TABLE orders_kr PARTITION OF orders

FOR VALUES IN ('KR');

CREATE TABLE orders_jp PARTITION OF orders

FOR VALUES IN ('JP');

CREATE TABLE orders_us PARTITION OF orders

FOR VALUES IN ('US');

CREATE TABLE orders_eu PARTITION OF orders

FOR VALUES IN ('DE', 'FR', 'GB', 'IT', 'ES');

CREATE TABLE orders_other PARTITION OF orders DEFAULT;

Hash 파티셔닝: 균등 분배

특정 컬럼의 해시값으로 데이터를 균등하게 분배합니다:

-- 사용자 ID 기반 해시 파티셔닝 (4개 파티션)

CREATE TABLE user_activities (

id BIGSERIAL,

user_id BIGINT NOT NULL,

activity VARCHAR(100) NOT NULL,

metadata JSONB,

created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

PRIMARY KEY (id, user_id)

) PARTITION BY HASH (user_id);

CREATE TABLE user_activities_0 PARTITION OF user_activities

FOR VALUES WITH (MODULUS 4, REMAINDER 0);

CREATE TABLE user_activities_1 PARTITION OF user_activities

FOR VALUES WITH (MODULUS 4, REMAINDER 1);

CREATE TABLE user_activities_2 PARTITION OF user_activities

FOR VALUES WITH (MODULUS 4, REMAINDER 2);

CREATE TABLE user_activities_3 PARTITION OF user_activities

FOR VALUES WITH (MODULUS 4, REMAINDER 3);

다중 레벨 파티셔닝

Range와 List를 조합한 다중 레벨 파티셔닝:

-- 1차: 날짜(Range), 2차: 지역(List)

CREATE TABLE sales (

id BIGSERIAL,

product_id BIGINT NOT NULL,

region VARCHAR(10) NOT NULL,

amount DECIMAL(12,2) NOT NULL,

sold_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

PRIMARY KEY (id, sold_at, region)

) PARTITION BY RANGE (sold_at);

-- 월별 서브 파티션

CREATE TABLE sales_2026_03 PARTITION OF sales

FOR VALUES FROM ('2026-03-01') TO ('2026-04-01')

PARTITION BY LIST (region);

CREATE TABLE sales_2026_03_kr PARTITION OF sales_2026_03

FOR VALUES IN ('KR');

CREATE TABLE sales_2026_03_jp PARTITION OF sales_2026_03

FOR VALUES IN ('JP');

CREATE TABLE sales_2026_03_other PARTITION OF sales_2026_03 DEFAULT;

자동 파티션 관리

pg_partman 확장 사용

-- pg_partman 설치

CREATE EXTENSION pg_partman;

-- 자동 파티션 관리 설정

SELECT partman.create_parent(

p_parent_table := 'public.events',

p_control := 'created_at',

p_type := 'native',

p_interval := '1 month',

p_premake := 3, -- 3개월 미리 생성

p_start_partition := '2026-01-01'

);

-- 자동 유지보수 (cron으로 실행)

-- 새 파티션 생성 + 오래된 파티션 관리

SELECT partman.run_maintenance();

쉘 스크립트로 자동 생성

#!/bin/bash

create_monthly_partitions.sh

PGHOST="localhost"

PGDB="mydb"

PGUSER="admin"

향후 3개월 파티션 생성

for i in 0 1 2 3; do

MONTH=$(date -d "+${i} months" +%Y-%m-01)

NEXT_MONTH=$(date -d "+$((i+1)) months" +%Y-%m-01)

TABLE_NAME="events_$(date -d "+${i} months" +%Y_%m)"

psql -h $PGHOST -d $PGDB -U $PGUSER -c "

CREATE TABLE IF NOT EXISTS ${TABLE_NAME}

PARTITION OF events

FOR VALUES FROM ('${MONTH}') TO ('${NEXT_MONTH}');

" 2>/dev/null

echo "Created partition: ${TABLE_NAME}"

done

오래된 파티션 삭제/보관

-- 파티션 분리 (데이터 보존, 쿼리에서 제외)

ALTER TABLE events DETACH PARTITION events_2025_01;

-- 분리된 파티션을 압축 테이블스페이스로 이동

ALTER TABLE events_2025_01 SET TABLESPACE archive_tablespace;

-- 또는 완전 삭제 (DROP이 DELETE보다 훨씬 빠름!)

DROP TABLE events_2025_01;

-- vs.

-- DELETE FROM events WHERE created_at < '2025-02-01';

-- ↑ 이 방식은 수백만 행 삭제 시 수십 분 소요

성능 비교: 파티셔닝 전 vs 후

테스트 환경

-- 1억 행 테이블 생성 (파티셔닝 없음)

CREATE TABLE events_no_part (

id BIGSERIAL PRIMARY KEY,

event_type VARCHAR(50),

created_at TIMESTAMPTZ DEFAULT NOW()

);

-- 동일 데이터로 파티셔닝 테이블 생성 (월별)

-- ... (위의 events 테이블 사용)

쿼리 성능 비교

-- 1개월 데이터 조회

-- 파티셔닝 없음: 15.2초 (Full Table Scan)

-- 파티셔닝 있음: 0.8초 (Partition Pruning → 단일 파티션 스캔)

-- 인덱스 크기

-- 파티셔닝 없음: 2.1 GB (단일 인덱스)

-- 파티셔닝 있음: 175 MB/파티션 × 12 = 2.1 GB (총합은 같지만 개별 인덱스 효율적)

-- 데이터 삭제 (1개월분)

-- 파티셔닝 없음: DELETE → 45분 + VACUUM 30분

-- 파티셔닝 있음: DROP TABLE → 0.01초

주의사항과 제약

PRIMARY KEY 제약

파티션 키는 반드시 PRIMARY KEY에 포함되어야 합니다:

-- 오류! 파티션 키(created_at)가 PK에 없음

CREATE TABLE events (

id BIGSERIAL PRIMARY KEY, -- ERROR

created_at TIMESTAMPTZ NOT NULL

) PARTITION BY RANGE (created_at);

-- 올바른 방법: 복합 PK

CREATE TABLE events (

id BIGSERIAL,

created_at TIMESTAMPTZ NOT NULL,

PRIMARY KEY (id, created_at)

) PARTITION BY RANGE (created_at);

UNIQUE 제약

-- UNIQUE 제약도 파티션 키를 포함해야 함

CREATE UNIQUE INDEX idx_events_unique

ON events (event_type, created_at); -- OK

-- 파티션 키 없는 UNIQUE는 불가

-- CREATE UNIQUE INDEX ON events (event_type); -- ERROR

크로스 파티션 조인 성능

-- 파티션 키로 필터링하지 않으면 모든 파티션을 스캔

-- 반드시 WHERE 절에 파티션 키 포함!

SELECT * FROM events

WHERE created_at >= '2026-03-01' -- 파티션 프루닝 작동

AND event_type = 'purchase';

-- enable_partition_pruning 설정 확인

SHOW enable_partition_pruning; -- 'on' 이어야 함

모니터링

-- 파티션별 크기 확인

SELECT

schemaname,

tablename,

pg_size_pretty(pg_total_relation_size(schemaname || '.' || tablename)) as total_size,

pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) as table_size

FROM pg_tables

WHERE tablename LIKE 'events_%'

ORDER BY pg_total_relation_size(schemaname || '.' || tablename) DESC;

-- 파티션별 행 수 확인

SELECT

relname as partition_name,

n_live_tup as row_count

FROM pg_stat_user_tables

WHERE relname LIKE 'events_%'

ORDER BY relname;

-- 파티션 프루닝 효과 확인

EXPLAIN (ANALYZE, COSTS, BUFFERS, FORMAT TEXT)

SELECT count(*) FROM events

WHERE created_at >= '2026-03-01' AND created_at < '2026-04-01';

**Q1. PostgreSQL에서 지원하는 세 가지 파티셔닝 전략은?**

Range, List, Hash 파티셔닝

**Q2. 파티션 프루닝(Partition Pruning)이란?**

쿼리의 WHERE 조건에 따라 불필요한 파티션을 스캔하지 않고 건너뛰는 최적화 기법입니다.

**Q3. 파티션 키가 PRIMARY KEY에 포함되어야 하는 이유는?**

PostgreSQL의 선언적 파티셔닝에서 각 파티션은 독립적인 테이블이므로, 전체 테이블에 걸친 유니크 보장을 위해 파티션 키가 PK에 포함되어야 합니다.

**Q4. 오래된 데이터 삭제 시 DELETE 대신 DROP TABLE을 사용하는 이점은?**

DELETE는 행 단위로 삭제하고 VACUUM이 필요하지만, DROP TABLE은 파티션 전체를 즉시 제거하므로 수십 분 → 0.01초로 단축됩니다.

**Q5. DETACH PARTITION의 용도는?**

파티션을 부모 테이블에서 분리하여 쿼리 대상에서 제외하되 데이터는 보존합니다. 아카이브나 백업에 유용합니다.

**Q6. Hash 파티셔닝은 어떤 경우에 적합한가요?**

특정 범위나 카테고리 없이 데이터를 균등하게 분배해야 할 때 적합합니다. 특히 핫스팟을 방지하고 병렬 처리 성능을 높이는 데 유용합니다.

현재 단락 (1/208)

테이블이 커지면 성능 문제가 발생합니다:

작성 글자: 0원문 글자: 6,332작성 단락: 0/208