- Authors
- Name
- 왜 PostgreSQL 17에서 파티셔닝과 병렬 쿼리가 중요한가
- 파티셔닝 유형 개요
- Range vs List vs Hash 파티셔닝 선택 기준 비교
- 파티션 프루닝 원리와 최적화
- 병렬 쿼리 실행 계획 분석
- PostgreSQL 17의 파티셔닝 신규 기능
- 대규모 테이블 파티셔닝 마이그레이션 절차
- 운영 시 주의사항
- 트러블슈팅: 실패 사례와 복구
- 성능 최적화 체크리스트
- 참고자료

왜 PostgreSQL 17에서 파티셔닝과 병렬 쿼리가 중요한가
대규모 운영 환경에서 단일 테이블의 행 수가 수억 건을 넘어가면 인덱스 크기, vacuum 시간, 쿼리 응답 속도 모두 급격히 나빠진다. PostgreSQL 17은 선언적 파티셔닝(Declarative Partitioning)과 병렬 쿼리(Parallel Query)를 한 단계 끌어올렸다. 파티셔닝된 테이블에서 identity column과 exclusion constraint를 직접 지원하고, ALTER TABLE ... MERGE PARTITIONS와 SPLIT PARTITION 구문으로 파티션 경계를 동적으로 변경할 수 있게 되었다. 병렬 쿼리 쪽에서는 FULL OUTER JOIN과 집계 함수에 대한 병렬 처리가 확대되었고, GIN 인덱스의 Parallel Create Index, 상관 서브쿼리의 병렬화 등이 추가되었다.
이 글에서는 파티셔닝 유형별 설계 전략, 파티션 프루닝 원리, 병렬 쿼리 실행 계획 분석, 대규모 테이블 마이그레이션 절차, 그리고 실제 운영에서 만나는 트러블슈팅까지 코드와 함께 다룬다.
파티셔닝 유형 개요
PostgreSQL은 세 가지 기본 파티셔닝 전략과 이를 조합한 복합 파티셔닝을 지원한다.
Range 파티셔닝
시간 기반 데이터나 연속 값 범위에 적합하다. 주문 테이블을 월별로 나누는 것이 대표적이다.
-- Range 파티셔닝: 월별 주문 테이블
CREATE TABLE orders (
order_id BIGSERIAL,
customer_id BIGINT NOT NULL,
order_date DATE NOT NULL,
total_amount NUMERIC(12,2),
status VARCHAR(20) DEFAULT 'pending'
) PARTITION BY RANGE (order_date);
-- 월별 파티션 생성
CREATE TABLE orders_2026_01 PARTITION OF orders
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE orders_2026_02 PARTITION OF orders
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
CREATE TABLE orders_2026_03 PARTITION OF orders
FOR VALUES FROM ('2026-03-01') TO ('2026-04-01');
-- 기본 파티션 (범위에 해당하지 않는 데이터 수용)
CREATE TABLE orders_default PARTITION OF orders DEFAULT;
-- 각 파티션에 로컬 인덱스 자동 생성
CREATE INDEX idx_orders_customer ON orders (customer_id);
CREATE INDEX idx_orders_status ON orders (status, order_date);
List 파티셔닝
이산형 카테고리 값(지역, 상태 코드 등)으로 분리할 때 사용한다.
-- List 파티셔닝: 지역별 사용자 테이블
CREATE TABLE users (
user_id BIGSERIAL,
username VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL,
region VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
) PARTITION BY LIST (region);
CREATE TABLE users_kr PARTITION OF users
FOR VALUES IN ('KR');
CREATE TABLE users_us PARTITION OF users
FOR VALUES IN ('US');
CREATE TABLE users_eu PARTITION OF users
FOR VALUES IN ('DE', 'FR', 'GB', 'IT', 'ES');
CREATE TABLE users_apac PARTITION OF users
FOR VALUES IN ('JP', 'SG', 'AU', 'IN');
CREATE TABLE users_others PARTITION OF users DEFAULT;
Hash 파티셔닝
특정 컬럼의 해시 값으로 균등하게 분배한다. 데이터에 자연스러운 범위나 카테고리가 없을 때 유용하다.
-- Hash 파티셔닝: 세션 테이블을 4개 파티션으로 균등 분배
CREATE TABLE sessions (
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id BIGINT NOT NULL,
payload JSONB,
created_at TIMESTAMPTZ DEFAULT now(),
expires_at TIMESTAMPTZ
) PARTITION BY HASH (session_id);
CREATE TABLE sessions_p0 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_p1 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE sessions_p2 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE sessions_p3 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 3);
복합 파티셔닝 (Sub-Partitioning)
Range와 List를 조합하여 2단계 파티셔닝을 구성할 수 있다.
-- 복합 파티셔닝: 연도별 Range -> 지역별 List
CREATE TABLE events (
event_id BIGSERIAL,
event_type VARCHAR(50),
region VARCHAR(10),
event_date DATE NOT NULL,
payload JSONB
) PARTITION BY RANGE (event_date);
CREATE TABLE events_2026 PARTITION OF events
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01')
PARTITION BY LIST (region);
CREATE TABLE events_2026_kr PARTITION OF events_2026
FOR VALUES IN ('KR');
CREATE TABLE events_2026_us PARTITION OF events_2026
FOR VALUES IN ('US');
CREATE TABLE events_2026_eu PARTITION OF events_2026
FOR VALUES IN ('DE', 'FR', 'GB');
Range vs List vs Hash 파티셔닝 선택 기준 비교
| 항목 | Range | List | Hash |
|---|---|---|---|
| 적합한 데이터 | 시계열, 연속 범위 | 이산 카테고리 | 균등 분배 필요 |
| 파티션 키 예시 | order_date, created_at | region, status | user_id, session_id |
| 파티션 프루닝 | 범위 조건에서 효과적 | 등호 조건에서 효과적 | 해시 키 등호에서만 동작 |
| 데이터 편향 위험 | 특정 기간 집중 가능 | 특정 값 집중 가능 | 낮음 (균등 분배) |
| 신규 파티션 추가 | 쉬움 (미래 범위 추가) | 쉬움 (새 값 추가) | 불가 (재해시 필요) |
| 파티션 제거 | DROP으로 즉시 삭제 | DROP으로 즉시 삭제 | 개별 삭제 불가 |
| 보관/아카이빙 | 우수 (오래된 파티션 분리) | 보통 | 어려움 |
| 복합 파티셔닝 | 지원 | 지원 | 2단계 불가 |
| PostgreSQL 17 개선 | MERGE/SPLIT 지원 | MERGE/SPLIT 지원 | 제한적 |
선택 기준 정리: 시계열 로그나 주문 데이터라면 Range, 지역이나 상태 코드 기반이면 List, 균등 분배가 가장 중요하고 프루닝이 덜 중요하면 Hash를 선택한다. 실무에서는 Range + List 복합 파티셔닝이 가장 흔하다.
파티션 프루닝 원리와 최적화
파티션 프루닝(Partition Pruning)은 쿼리의 WHERE 절을 분석하여 불필요한 파티션을 실행 계획에서 제외하는 메커니즘이다. PostgreSQL 17에서는 컴파일 타임 프루닝과 런타임 프루닝이 모두 동작한다.
컴파일 타임 프루닝
쿼리 계획 단계에서 상수 조건을 평가하여 파티션을 제외한다.
-- 컴파일 타임 프루닝 확인
EXPLAIN (COSTS OFF)
SELECT * FROM orders
WHERE order_date >= '2026-03-01' AND order_date < '2026-04-01';
/*
결과:
Append
-> Seq Scan on orders_2026_03
Filter: ((order_date >= '2026-03-01') AND (order_date < '2026-04-01'))
-- orders_2026_01, orders_2026_02는 프루닝되어 스캔하지 않음
*/
런타임 프루닝
파라미터화된 쿼리나 서브쿼리 결과에 따라 실행 시점에 프루닝이 동작한다.
-- 런타임 프루닝: Prepared Statement에서 동작
PREPARE get_orders(date, date) AS
SELECT * FROM orders WHERE order_date >= $1 AND order_date < $2;
EXPLAIN ANALYZE EXECUTE get_orders('2026-02-01', '2026-03-01');
-- 실행 시점에 orders_2026_02만 스캔
프루닝이 동작하지 않는 경우
다음 상황에서는 프루닝이 작동하지 않으므로 주의해야 한다.
- 함수 적용:
WHERE EXTRACT(MONTH FROM order_date) = 3형태로 파티션 키에 함수를 적용하면 프루닝 불가 - 타입 불일치: 파티션 키가
DATE인데TIMESTAMP값으로 비교하면 암시적 캐스팅으로 프루닝 실패 가능 - OR 조건의 비파티션 키 혼합:
WHERE order_date = '2026-03-01' OR customer_id = 100처럼 파티션 키와 비파티션 키를 OR로 연결하면 모든 파티션 스캔 - enable_partition_pruning = off: 이 GUC 파라미터가 꺼져 있으면 프루닝 비활성화
-- 프루닝 동작 확인: enable_partition_pruning 설정
SHOW enable_partition_pruning; -- 반드시 'on' 확인
-- 안티패턴: 파티션 키에 함수 적용 (프루닝 불가)
EXPLAIN (COSTS OFF)
SELECT * FROM orders
WHERE EXTRACT(YEAR FROM order_date) = 2026
AND EXTRACT(MONTH FROM order_date) = 3;
-- 모든 파티션을 스캔하게 됨
-- 올바른 패턴: 범위 조건 사용 (프루닝 동작)
EXPLAIN (COSTS OFF)
SELECT * FROM orders
WHERE order_date >= '2026-03-01' AND order_date < '2026-04-01';
-- orders_2026_03만 스캔
병렬 쿼리 실행 계획 분석
PostgreSQL 17은 병렬 쿼리 실행을 더욱 적극적으로 활용한다. 파티셔닝된 테이블에서 각 파티션을 병렬 워커가 동시에 스캔할 수 있으며, FULL OUTER JOIN과 집계 연산에 대한 병렬 처리도 확장되었다.
핵심 병렬 쿼리 파라미터
| 파라미터 | 기본값 | 설명 |
|---|---|---|
max_parallel_workers | 8 | 인스턴스 전체 병렬 워커 최대 수 |
max_parallel_workers_per_gather | 2 | Gather 노드당 최대 병렬 워커 수 |
min_parallel_table_scan_size | 8MB | 병렬 Seq Scan 최소 테이블 크기 |
min_parallel_index_scan_size | 512kB | 병렬 Index Scan 최소 인덱스 크기 |
parallel_tuple_cost | 0.1 | 병렬 워커에서 튜플 전달 비용 |
parallel_setup_cost | 1000 | 병렬 워커 시작 비용 |
max_worker_processes | 8 | 전체 백그라운드 워커 프로세스 제한 |
work_mem | 4MB | 워커당 개별 적용되는 작업 메모리 |
병렬 실행 계획 확인
-- 병렬 쿼리 파라미터 설정 (세션 레벨)
SET max_parallel_workers_per_gather = 4;
SET work_mem = '256MB';
-- 대규모 파티셔닝 테이블 병렬 스캔
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT customer_id, SUM(total_amount) AS total_spent
FROM orders
WHERE order_date >= '2026-01-01' AND order_date < '2026-04-01'
GROUP BY customer_id
ORDER BY total_spent DESC
LIMIT 100;
/*
실행 계획 예시:
Limit (cost=... rows=100)
-> Sort (cost=... rows=...)
Sort Key: (sum(total_amount)) DESC
-> Finalize GroupAggregate (cost=... rows=...)
Group Key: customer_id
-> Gather Merge (cost=... rows=...)
Workers Planned: 4
Workers Launched: 4
-> Partial GroupAggregate (cost=... rows=...)
Group Key: customer_id
-> Parallel Append (cost=... rows=...)
-> Parallel Seq Scan on orders_2026_01
Filter: (...)
-> Parallel Seq Scan on orders_2026_02
Filter: (...)
-> Parallel Seq Scan on orders_2026_03
Filter: (...)
Planning Time: 2.1 ms
Execution Time: 1,245 ms
*/
핵심 포인트:
Workers Planned과Workers Launched가 일치하는지 확인한다. 불일치하면max_worker_processes또는 시스템 리소스가 부족한 것이다.Parallel Append는 여러 파티션을 병렬 워커들이 나누어 스캔한다는 뜻이다.Partial GroupAggregate와Finalize GroupAggregate로 집계가 2단계(워커 부분 집계 + 리더 최종 집계)로 나뉜다.
Parallel Hash Join 분석
-- 병렬 Hash Join: 대규모 조인 최적화
EXPLAIN (ANALYZE, BUFFERS)
SELECT o.order_id, o.order_date, c.username, o.total_amount
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2026-03-01' AND o.order_date < '2026-04-01'
AND o.total_amount > 10000;
/*
Gather (cost=... rows=...)
Workers Planned: 4
Workers Launched: 4
-> Parallel Hash Join (cost=... rows=...)
Hash Cond: (o.customer_id = c.customer_id)
-> Parallel Seq Scan on orders_2026_03 o
Filter: (total_amount > 10000)
-> Parallel Hash (cost=... rows=...)
Buckets: 65536 Batches: 1 Memory Usage: 12MB
-> Seq Scan on customers c
*/
Parallel Hash Join에서는 모든 워커가 공유 해시 테이블을 함께 구축하므로 단일 스레드 Hash Join보다 빠르게 빌드된다. work_mem 설정에 따라 in-memory 또는 multi-batch로 동작하는데, 워커 수가 N이면 최대 (N+1) x work_mem의 메모리가 사용된다는 점에 유의한다.
PostgreSQL 17의 파티셔닝 신규 기능
MERGE PARTITIONS
여러 파티션을 하나로 병합할 수 있다. 오래된 데이터를 분기별이나 연도별로 통합할 때 유용하다.
-- PostgreSQL 17: 월별 파티션을 분기별로 병합
ALTER TABLE orders
MERGE PARTITIONS (orders_2026_01, orders_2026_02, orders_2026_03)
INTO orders_2026_q1;
-- 병합 후 새 파티션 확인
SELECT
parent.relname AS parent_table,
child.relname AS partition_name,
pg_get_expr(child.relpartbound, child.oid) AS partition_bound
FROM pg_inherits
JOIN pg_class parent ON pg_inherits.inhparent = parent.oid
JOIN pg_class child ON pg_inherits.inhrelid = child.oid
WHERE parent.relname = 'orders'
ORDER BY child.relname;
SPLIT PARTITION
하나의 파티션을 여러 개로 분리한다. 데이터가 너무 커진 파티션을 세분화할 때 사용한다.
-- PostgreSQL 17: 분기 파티션을 다시 월별로 분할
ALTER TABLE orders
SPLIT PARTITION orders_2026_q1 INTO (
PARTITION orders_2026_01 FOR VALUES FROM ('2026-01-01') TO ('2026-02-01'),
PARTITION orders_2026_02 FOR VALUES FROM ('2026-02-01') TO ('2026-03-01'),
PARTITION orders_2026_03 FOR VALUES FROM ('2026-03-01') TO ('2026-04-01')
);
대규모 테이블 파티셔닝 마이그레이션 절차
기존 단일 테이블(수억 건)을 파티셔닝 테이블로 전환하는 것은 운영 환경에서 가장 까다로운 작업 중 하나다. 다운타임을 최소화하는 실전 절차를 정리한다.
방법 1: pg_partman을 활용한 온라인 마이그레이션
-- 1단계: 확장 설치
CREATE EXTENSION IF NOT EXISTS pg_partman;
-- 2단계: 새로운 파티셔닝 테이블 생성
CREATE TABLE orders_partitioned (LIKE orders INCLUDING ALL)
PARTITION BY RANGE (order_date);
-- 3단계: pg_partman으로 파티션 자동 생성
SELECT partman.create_parent(
p_parent_table := 'public.orders_partitioned',
p_control := 'order_date',
p_type := 'native',
p_interval := '1 month',
p_premake := 3
);
-- 4단계: 데이터 마이그레이션 (배치 처리)
-- 한 번에 전체를 INSERT하면 WAL이 폭증하므로 배치로 나눈다
DO $$
DECLARE
batch_start DATE := '2020-01-01';
batch_end DATE;
BEGIN
WHILE batch_start < '2026-04-01' LOOP
batch_end := batch_start + INTERVAL '1 month';
INSERT INTO orders_partitioned
SELECT * FROM orders
WHERE order_date >= batch_start AND order_date < batch_end;
RAISE NOTICE 'Migrated: % to %', batch_start, batch_end;
batch_start := batch_end;
PERFORM pg_sleep(0.5); -- WAL 부하 분산
END LOOP;
END $$;
-- 5단계: 테이블 교체 (짧은 잠금)
BEGIN;
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_partitioned RENAME TO orders;
COMMIT;
-- 6단계: 검증 후 이전 테이블 제거
-- SELECT count(*) FROM orders;
-- SELECT count(*) FROM orders_old;
-- DROP TABLE orders_old;
방법 2: 논리적 복제(Logical Replication) 활용
다운타임이 허용되지 않는 환경에서는 논리적 복제를 활용한다.
- 새 파티셔닝 테이블을 별도 스키마에 생성
- 논리적 복제 구독(Subscription)으로 실시간 데이터 동기화
- 차이 확인 후 짧은 점검 시간 동안 애플리케이션 전환
- 구독 해제 및 이전 테이블 정리
운영 시 주의사항
인덱스 관리
파티셔닝 테이블에서 인덱스는 각 파티션에 로컬로 생성된다. 부모 테이블에 인덱스를 생성하면 기존 및 미래 파티션에 자동으로 적용된다. 글로벌 고유 인덱스는 파티션 키를 포함해야 한다.
-- 파티셔닝 테이블의 고유 제약조건: 파티션 키 포함 필수
-- 아래는 오류 발생 (파티션 키 미포함)
-- ALTER TABLE orders ADD CONSTRAINT pk_orders PRIMARY KEY (order_id);
-- 올바른 방법: 파티션 키 포함
ALTER TABLE orders ADD CONSTRAINT pk_orders
PRIMARY KEY (order_id, order_date);
Vacuum 전략
파티셔닝 테이블에서 VACUUM은 각 파티션에 개별적으로 실행된다. PostgreSQL 17에서는 VACUUM 명령의 병렬 처리가 가능하다.
-- 특정 파티션만 vacuum
VACUUM (VERBOSE, ANALYZE) orders_2026_03;
-- 전체 파티셔닝 테이블 vacuum (각 파티션 순차)
VACUUM (VERBOSE, ANALYZE) orders;
-- autovacuum 튜닝: 파티션별 설정
ALTER TABLE orders_2026_03 SET (
autovacuum_vacuum_scale_factor = 0.01,
autovacuum_analyze_scale_factor = 0.005,
autovacuum_vacuum_cost_delay = 2
);
제약조건과 트리거
CHECK제약조건은 각 파티션에 독립적으로 정의 가능하다FOREIGN KEY는 파티셔닝 테이블을 참조하는 것은 가능하지만, 파티셔닝 테이블에서 다른 테이블로 FK를 거는 것도 PostgreSQL 12 이후 지원한다- BEFORE ROW 트리거는 각 파티션에 정의해야 한다
트러블슈팅: 실패 사례와 복구
사례 1: 병렬 워커가 0개로 실행되는 문제
증상: EXPLAIN ANALYZE에서 Workers Planned: 4인데 Workers Launched: 0
원인과 해결:
-- 현재 설정 확인
SHOW max_worker_processes; -- 기본 8
SHOW max_parallel_workers; -- 기본 8
SHOW max_parallel_workers_per_gather; -- 기본 2
-- 동시 실행 중인 워커 수 확인
SELECT count(*) FROM pg_stat_activity
WHERE backend_type = 'parallel worker';
-- 해결: max_worker_processes 증가 (재시작 필요)
-- postgresql.conf
-- max_worker_processes = 16
-- max_parallel_workers = 12
-- max_parallel_workers_per_gather = 4
전체 워커 풀이 다른 쿼리에 의해 이미 소진된 상태에서 새 쿼리가 실행되면 워커를 0개로 받을 수 있다. 피크 시간대에 병렬 쿼리가 집중되지 않도록 커넥션 풀 설정과 함께 조율해야 한다.
사례 2: 파티션 프루닝 미동작으로 전체 스캔
증상: 특정 월의 데이터만 조회하는데 모든 파티션을 스캔
원인: 파티션 키에 함수를 적용하거나 타입 불일치
-- 문제 쿼리: 타입 불일치
-- 파티션 키가 DATE인데 TIMESTAMP로 비교
SELECT * FROM orders
WHERE order_date = '2026-03-15 00:00:00'::timestamp;
-- 해결: 정확한 타입으로 비교
SELECT * FROM orders
WHERE order_date = '2026-03-15'::date;
-- 프루닝 동작 검증
EXPLAIN (COSTS OFF)
SELECT * FROM orders WHERE order_date = '2026-03-15'::date;
-- Append -> Seq Scan on orders_2026_03 (프루닝 정상)
사례 3: 파티션 추가 시 LOCK 대기
증상: CREATE TABLE ... PARTITION OF 실행 시 오랫동안 대기
원인: 부모 테이블에 대한 ACCESS EXCLUSIVE LOCK이 필요하며, 동시 트랜잭션이 해당 테이블을 사용 중
해결:
-- lock_timeout 설정으로 빠른 실패
SET lock_timeout = '5s';
-- 비활성 트랜잭션 확인
SELECT pid, state, query_start, query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
OR state = 'idle in transaction';
-- 유지보수 윈도우에서 실행하거나, CONCURRENTLY 옵션 활용
-- (파티션 추가 자체는 CONCURRENTLY 미지원이므로 인덱스만)
CREATE INDEX CONCURRENTLY idx_new_part ON orders_2026_04 (customer_id);
사례 4: work_mem 폭발로 OOM
증상: 병렬 쿼리 실행 중 PostgreSQL 프로세스가 OOM Killer에 의해 종료
원인: work_mem이 워커 수만큼 곱해짐. work_mem = 1GB, 워커 4개면 리더 포함 5 x 1GB = 5GB 사용 가능
-- 안전한 work_mem 계산
-- 총 메모리: 64GB, 최대 동시 연결: 200, 병렬 워커 최대: 4
-- work_mem = 64GB * 0.25 / 200 / 5 = 약 16MB
SET work_mem = '16MB';
-- 특정 쿼리에서만 임시로 높이기
SET LOCAL work_mem = '256MB';
SELECT ... ; -- 대규모 집계 쿼리
RESET work_mem;
성능 최적화 체크리스트
실전 운영을 위한 체크리스트를 정리한다.
파티셔닝 설계:
- 파티션 수를 100개 이하로 유지한다. 수천 개의 파티션은 플래너 오버헤드를 발생시킨다.
- DEFAULT 파티션을 반드시 생성하여 범위 밖 데이터의 INSERT 실패를 방지한다.
- 파티션 키를 PRIMARY KEY와 UNIQUE 제약조건에 포함시킨다.
병렬 쿼리 튜닝:
min_parallel_table_scan_size와min_parallel_index_scan_size를 워크로드에 맞게 조정한다.parallel_tuple_cost와parallel_setup_cost를 낮추면 플래너가 병렬 계획을 더 적극적으로 선택한다.jit = on과 병렬 쿼리를 함께 사용하면 OLAP 워크로드에서 추가 성능 향상을 기대할 수 있다.
모니터링 항목:
pg_stat_user_tables로 각 파티션의n_live_tup,n_dead_tup,last_autovacuum확인pg_stat_activity에서 병렬 워커 사용 현황 모니터링EXPLAIN (ANALYZE, BUFFERS)로 파티션 프루닝과 병렬 실행 검증
참고자료
- PostgreSQL 공식 문서 - Table Partitioning
- PostgreSQL 공식 문서 - How Parallel Query Works
- PostgreSQL 17 릴리스 노트 - 파티셔닝 및 병렬 쿼리 개선
- Crunchy Data - Postgres Parallel Query Troubleshooting
- Mydbops - PostgreSQL 17 Partitioning Best Practices: MERGE/SPLIT Commands
- Microsoft Tech Community - Postgres 17 Query Performance Improvements
- AWS Blog - Improve query performance with parallel queries in PostgreSQL
- pgMustard - Increasing max_parallel_workers_per_gather