병렬 쿼리가 왜 문제가 되는가

PostgreSQL은 큰 스캔·조인·집계를 여러 워커로 나눠 처리하는 병렬 실행을 지원한다. 하지만 기본값은 보수적이라, 대용량 테이블 스캔인데도 워커가 하나도 붙지 않거나, 반대로 OLTP 환경에서 병렬 워커가 커넥션당 여러 개씩 튀어 CPU와 max_worker_processes 슬롯을 고갈시키는 일이 흔하다. 핵심은 두 가지다. 플래너가 병렬 계획을 고를지 결정하는 비용 모델, 그리고 실행 시 워커를 실제로 몇 개 확보하는지다.

워커 슬롯 구조부터 이해하기

병렬 워커는 세 단계 상한을 거친다. 전체 백그라운드 슬롯(max_worker_processes), 병렬용 전역 풀(max_parallel_workers), 그리고 노드 하나당 상한(max_parallel_workers_per_gather)이다. 실행 중 슬롯이 부족하면 조용히 적은 수로 실행되므로, EXPLAIN ANALYZE의 Workers Planned와 Workers Launched가 다르면 슬롯 고갈 신호다.

파라미터의미실무 기준
max_worker_processes전체 백그라운드 프로세스vCPU 수 근처
max_parallel_workers병렬 전용 풀max_worker_processes의 절반~동일
max_parallel_workers_per_gatherGather 노드당 상한2~4 (OLTP는 낮게)

비용 모델: 언제 병렬을 고르는가

플래너는 병렬 스타트업·워커 전송 비용을 감안해, 그 오버헤드를 상쇄할 만큼 작업량이 커야 병렬 계획을 선택한다. 관련 노브는 다음과 같다.

-- 병렬 진입 문턱을 낮춘다
SET parallel_setup_cost = 500;      -- 기본 1000, 워커 기동 고정비
SET parallel_tuple_cost = 0.05;     -- 기본 0.1, 워커→리더 튜플 전송 단가
SET min_parallel_table_scan_size = '8MB';  -- 기본 8MB, 이 미만은 병렬 제외

-- 대상 테이블에 워커 상한을 직접 지정 (플래너 힌트 역할)
ALTER TABLE events SET (parallel_workers = 4);

워커 수는 대략 테이블 크기가 min_parallel_table_scan_size의 3배가 될 때마다 하나씩 늘어나는 로그 스케일로 산정된다. 그래서 수백 MB 테이블도 워커가 2개에 그치는 경우가 많다. 특정 대형 테이블은 위처럼 parallel_workers를 명시하는 편이 예측 가능하다.

실측으로 확인하기

추측 대신 계획을 본다. Gather 아래 노드가 병렬이고, 두 워커 수치가 일치하는지가 관건이다.

EXPLAIN (ANALYZE, VERBOSE, BUFFERS)
SELECT date_trunc('hour', ts), count(*)
FROM events
WHERE ts >= now() - interval '7 days'
GROUP BY 1;

-- 출력 예:
-- Gather  (cost=... rows=...)
--   Workers Planned: 4
--   Workers Launched: 2   <-- 불일치: 전역 풀 고갈

Launched가 계획보다 작으면 동시 실행 쿼리들이 max_parallel_workers를 나눠 쓰는 중이다. 부하 시간대에 반복 측정해야 실제 병목이 보인다.

워크로드별 설정 전략

OLTP와 분석계는 정반대로 튜닝한다. 하나의 인스턴스가 둘을 겸하면 세션 단위로 분리하는 게 안전하다.

# postgresql.conf — 16 vCPU 분석 위주 예시
max_worker_processes = 16
max_parallel_workers = 12
max_parallel_workers_per_gather = 4
max_parallel_maintenance_workers = 4   # CREATE INDEX 병렬화

# OLTP 세션은 롤/애플리케이션 단에서 병렬 억제
ALTER ROLE app_oltp SET max_parallel_workers_per_gather = 0;

배치·리포트 전용 롤에는 반대로 상한을 올려주면, 같은 클러스터에서 성격이 다른 트래픽이 서로의 워커 슬롯을 잠식하지 않는다.

흔한 함정과 주의점

  • 병렬 제약 쿼리: 커서, 일부 CTE, FOR UPDATE, 병렬 비지원 함수(PARALLEL UNSAFE)가 있으면 계획 전체가 직렬화된다. 사용자 정의 함수는 안전하면 PARALLEL SAFE로 명시하자.
  • CPU 오버서브스크립션: 워커 하나당 실제 CPU 하나를 먹는다. 동시 쿼리 × 워커 수가 vCPU를 넘으면 컨텍스트 스위칭으로 오히려 느려진다.
  • 메모리 배수: work_mem은 워커마다 개별 적용된다. 워커 4개면 정렬·해시가 최대 5배(리더 포함)까지 메모리를 쓸 수 있어 OOM 위험이 커진다.

정리

병렬 튜닝의 순서는 명확하다. 먼저 슬롯 3단 상한을 워크로드와 vCPU에 맞게 잡고, 비용 노브로 진입 문턱을 조정한 뒤, Workers Planned vs Launched로 실측 검증한다. OLTP는 롤 단위로 병렬을 끄고 분석계만 열어두는 분리가 가장 안정적이다. 값 하나를 크게 키우기보다, work_mem·CPU 여유와 함께 균형을 맞추는 것이 실무의 핵심이다.