TECH

PostgreSQL 17 성능 튜닝 실전 인덱스 파티셔닝 pgvector로 응답속도 잡기

포스트그레스큐엘 17에서 느린 쿼리를 진단하고 인덱스와 파티셔닝으로 성능을 끌어올리는 방법을 정리했습니다. 실행 계획 분석, 부분 인덱스, pgvector 유사도 검색을 다룹니다.

PostgreSQL 17 성능 튜닝 실전 인덱스 파티셔닝 pgvector로 응답속도 잡기

PostgreSQL 17 성능 튜닝 실전 인덱스 파티셔닝 pgvector로 응답속도 잡기

데이터가 쌓일수록 잘 돌던 쿼리가 어느 날 갑자기 느려진다. 인덱스가 없거나 잘못 걸렸거나, 테이블이 너무 커졌거나, 통계가 낡았을 수 있다. PostgreSQL 17은 성능 면에서 여러 개선을 담았고, 특히 대용량 데이터 처리와 벡터 검색 워크로드에서 강력해졌다. 하지만 도구가 좋아도 튜닝의 기본기를 모르면 소용없다. 이 글은 느린 쿼리를 진단하고, 올바른 인덱스를 설계하고, 파티셔닝으로 대용량 테이블을 다루며, pgvector로 유사도 검색을 최적화하는 실무 흐름을 정리한다.

실행 계획으로 병목을 진단하기

튜닝의 출발점은 언제나 측정이다. 어떤 쿼리가 왜 느린지 추측하지 말고 실행 계획을 직접 확인해야 한다. EXPLAIN ANALYZE는 쿼리 플래너가 세운 계획과 실제 실행 결과를 함께 보여준다. 여기서 가장 먼저 살펴볼 것은 순차 스캔(Sequential Scan)이다. 큰 테이블에서 조건 없이 전체를 훑는 순차 스캔이 보인다면 인덱스가 필요하다는 강한 신호다.

EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 12345 AND status = 'shipped'
ORDER BY created_at DESC
LIMIT 20;

출력에서 예상 행 수와 실제 행 수의 차이가 크면 통계가 낡았다는 뜻이므로 ANALYZE로 통계를 갱신한다. BUFFERS 옵션은 얼마나 많은 데이터 블록을 읽었는지 보여주는데, 디스크에서 읽은 블록이 많으면 캐시 효율이 나쁜 것이다. 실행 계획을 읽는 습관은 튜닝의 가장 중요한 기본기다. 지표 없이 손대는 최적화는 대개 상황을 더 나쁘게 만든다.

인덱스 설계의 원칙

인덱스는 만능이 아니다. 읽기를 빠르게 하는 대신 쓰기를 느리게 하고 저장 공간을 차지한다. 그래서 실제로 자주 쓰이는 쿼리 패턴에 맞춰 선별적으로 만들어야 한다. 여러 컬럼을 함께 조건으로 거는 쿼리에는 복합 인덱스가 효과적인데, 컬럼 순서가 중요하다. 등호 조건에 쓰이는 컬럼을 앞에, 범위 조건이나 정렬에 쓰이는 컬럼을 뒤에 두는 것이 일반적인 원칙이다.

특정 조건의 행만 자주 조회한다면 부분 인덱스가 공간과 성능 모두에서 유리하다. 예를 들어 활성 상태의 주문만 주로 조회한다면, 전체가 아니라 그 조건을 만족하는 행에만 인덱스를 만든다.

-- 배송 완료되지 않은 주문만 인덱싱해 크기를 줄이고 조회를 빠르게
CREATE INDEX idx_orders_active
ON orders (customer_id, created_at DESC)
WHERE status <> 'delivered';

또 인덱스에 조회하려는 컬럼을 모두 포함시키면, 테이블 본체를 읽지 않고 인덱스만으로 결과를 반환하는 인덱스 온리 스캔이 가능해진다. INCLUDE 절로 추가 컬럼을 인덱스에 담아두는 방식이다. 다만 인덱스를 늘릴수록 쓰기 비용이 커지므로, 쓰기가 빈번한 테이블에서는 균형을 신중히 따져야 한다.

파티셔닝과 pgvector 유사도 검색

테이블이 수억 행으로 커지면 인덱스만으로는 한계가 온다. 이때 선언적 파티셔닝으로 하나의 큰 테이블을 여러 물리적 조각으로 나눈다. 시계열 데이터라면 날짜 범위로 파티셔닝하는 것이 자연스럽다. 최근 데이터만 조회하는 쿼리는 오래된 파티션을 아예 건드리지 않으므로 스캔 대상이 극적으로 줄고, 오래된 파티션을 통째로 떼어내는 데이터 정리도 간단해진다.

CREATE TABLE events (
    id BIGINT,
    occurred_at DATE NOT NULL,
    payload JSONB
) PARTITION BY RANGE (occurred_at);

CREATE TABLE events_2026_q3 PARTITION OF events
    FOR VALUES FROM ('2026-07-01') TO ('2026-10-01');

한편 AI 애플리케이션이 늘면서 벡터 유사도 검색 수요가 폭발했다. pgvector 확장은 PostgreSQL을 벡터 데이터베이스로 만들어, 임베딩을 저장하고 코사인 거리나 유클리드 거리로 가장 비슷한 항목을 찾게 해준다. 수백만 개 벡터에서 정확한 최근접 탐색은 느리므로, HNSW 인덱스로 근사 최근접 탐색을 쓴다. 약간의 정확도를 내주는 대신 검색 속도가 비약적으로 빨라진다. HNSW는 그래프 기반 인덱스로 빌드 시간과 메모리를 더 쓰지만 조회 성능이 뛰어나, 실시간 추천이나 검색 증강 생성 같은 워크로드에 적합하다. 인덱스 빌드 시 이웃 수와 탐색 폭 파라미터를 워크로드에 맞게 조정하면 정확도와 속도의 균형을 세밀하게 맞출 수 있다. 결국 PostgreSQL 튜닝의 핵심은 하나다. 측정으로 병목을 찾고, 워크로드에 맞는 도구를 골라 적용하고, 다시 측정해 검증하는 순환을 꾸준히 돌리는 것이다.

VACUUM과 통계, 그리고 서버 파라미터

PostgreSQL의 성능 문제는 쿼리와 인덱스만으로 설명되지 않는다. 그 아래에는 MVCC라는 동시성 제어 방식이 있다. 행을 갱신하거나 삭제해도 즉시 사라지지 않고 죽은 튜플로 남는데, 이것이 쌓이면 테이블이 부풀어 스캔이 느려진다. 이를 정리하는 것이 VACUUM이다. 오토배큠이 기본으로 돌지만, 갱신이 잦은 대형 테이블에서는 기본 설정이 따라가지 못해 부풀음이 심해지기도 한다. 이럴 때는 해당 테이블의 오토배큠 임계값을 낮춰 더 자주 청소하도록 조정한다. 죽은 튜플 비율과 마지막 배큠 시각을 시스템 뷰로 모니터링하는 습관이 중요하다.

플래너가 좋은 계획을 세우려면 정확한 통계가 필수다. 데이터 분포가 크게 바뀌었는데 통계가 낡으면 플래너가 잘못된 판단을 내려 엉뚱한 실행 계획을 고른다. 대량 적재나 대량 삭제 직후에는 반드시 통계를 갱신해야 한다. 특정 컬럼의 값 분포가 매우 치우쳐 있다면, 그 컬럼의 통계 수집 정밀도를 높여 플래너가 더 정확히 추정하게 만들 수 있다.

서버 파라미터 튜닝도 빼놓을 수 없다. 데이터를 캐싱하는 공유 버퍼는 보통 시스템 메모리의 사분의 일 정도로 잡고, 정렬과 해시에 쓰이는 작업 메모리는 동시 접속 수와 쿼리 특성을 고려해 설정한다. 작업 메모리가 너무 작으면 정렬이 디스크로 넘쳐 느려지고, 너무 크면 동시 쿼리가 많을 때 메모리가 고갈된다. 또 플래너가 디스크와 메모리 접근 비용을 어떻게 추정할지 정하는 비용 파라미터를, 실제 스토리지가 SSD라면 그에 맞게 낮춰 인덱스 활용을 유도한다. 이런 파라미터들은 하드웨어와 워크로드에 따라 최적값이 다르므로, 문서의 권장값에서 출발해 실측하며 조금씩 조정하는 것이 정석이다. 결국 데이터베이스 튜닝은 쿼리, 인덱스, 물리 구조, 서버 설정이라는 여러 층위를 함께 보는 종합적 안목을 요구한다.