PostgreSQL 17 성능 혁신

PostgreSQL 17이 대규모 성능 개선과 함께 출시되었다. 메모리 관리 재설계, B-Tree 인덱스 최적화, 새로운 SQL/JSON 기능이 핵심이다. 이 글에서는 실무에서 바로 적용할 수 있는 최적화 기법을 다룬다.

1. B-Tree 인덱스 IN 절 최적화

PostgreSQL 17에서 IN 절과 ANY 조건의 B-Tree 인덱스 스캔이 획기적으로 개선되었다. 기존에는 IN 절의 각 값에 대해 개별 인덱스 룩업을 수행했지만, 이제 한 번의 스캔으로 처리한다.

-- 테스트 테이블 생성
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id INTEGER NOT NULL,
    status VARCHAR(20) NOT NULL,
    amount DECIMAL(10,2),
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_orders_status ON orders(status);
CREATE INDEX idx_orders_user_status 
    ON orders(user_id, status);

-- PostgreSQL 16: 각 값마다 별도 인덱스 스캔
-- PostgreSQL 17: 단일 스캔으로 처리
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE status IN ('pending', 'processing', 'shipped', 
                  'delivered', 'cancelled');

-- PG17 실행 계획
-- Index Scan using idx_orders_status on orders
--   Index Cond: status = ANY('{pending,processing,...}')
--   Actual Time: 0.05..12.3ms (vs PG16: 0.05..45.8ms)

복합 인덱스에서의 개선

-- 복합 인덱스도 동일하게 최적화
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 1234
  AND status IN ('pending', 'processing');

-- 복합 인덱스를 효율적으로 활용
-- 버퍼 페이지 접근 횟수 감소 -> CPU 사용량 절감

2. 상관 서브쿼리 자동 조인 변환

PostgreSQL 17은 상관 IN 서브쿼리를 자동으로 조인으로 변환한다. 직접 조인으로 다시 작성할 필요가 없어졌다.

-- 기존: 서브쿼리 (외부 행마다 반복 실행)
SELECT u.name, u.email
FROM users u
WHERE u.id IN (
    SELECT o.user_id FROM orders o
    WHERE o.amount > 100
      AND o.created_at > NOW() - INTERVAL '30 days'
);

-- PostgreSQL 17: 자동으로 Semi Join으로 변환
-- Hash Semi Join
--   Hash Cond: (u.id = o.user_id)
--   -> Seq Scan on users u
--   -> Hash
--       -> Bitmap Heap Scan on orders o
--            Recheck Cond: (created_at > ...)
--            Filter: (amount > 100)

-- 수동 변환할 필요 없음 (아래처럼 쓰지 않아도 됨)
SELECT DISTINCT u.name, u.email
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.amount > 100
  AND o.created_at > NOW() - INTERVAL '30 days';

3. JSON_TABLE 함수

SQL/JSON 표준의 JSON_TABLE이 정식 지원된다. JSON 데이터를 관계형 테이블로 변환하는 강력한 기능이다.

-- JSON 데이터를 테이블로 변환
SELECT jt.*
FROM orders o,
    JSON_TABLE(
        o.metadata,
        '$.items[*]'
        COLUMNS (
            item_name   TEXT    PATH '$.name',
            quantity    INTEGER PATH '$.qty',
            unit_price  NUMERIC PATH '$.price',
            in_stock    BOOLEAN PATH '$.available'
                DEFAULT true ON EMPTY
        )
    ) AS jt
WHERE o.id = 1001;

-- 결과:
-- item_name | quantity | unit_price | in_stock
-- ----------+----------+------------+---------
-- Laptop    |        1 |    1299.99 | true
-- Mouse     |        2 |      29.99 | true
-- Cable     |        3 |       9.99 | false

중첩 JSON 처리

-- 중첩된 JSON 배열 처리
SELECT dept.*, emp.*
FROM company_data cd,
    JSON_TABLE(
        cd.org_chart,
        '$.departments[*]'
        COLUMNS (
            dept_name TEXT PATH '$.name',
            budget    NUMERIC PATH '$.budget',
            NESTED PATH '$.employees[*]' COLUMNS (
                emp_name  TEXT PATH '$.name',
                role      TEXT PATH '$.role',
                salary    NUMERIC PATH '$.salary'
            )
        )
    ) AS dept;

-- 결과:
-- dept_name   | budget   | emp_name | role      | salary
-- ------------+----------+----------+-----------+--------
-- Engineering | 500000   | Kim      | Lead      | 95000
-- Engineering | 500000   | Lee      | Developer | 80000
-- Marketing   | 300000   | Park     | Manager   | 85000

4. VACUUM 메모리 관리 최적화

PostgreSQL 17에서 VACUUM의 메모리 관리가 완전히 재설계되었다. 대용량 테이블의 VACUUM 성능이 크게 향상되었다.

-- VACUUM 관련 설정 최적화
ALTER SYSTEM SET maintenance_work_mem = '1GB';
ALTER SYSTEM SET autovacuum_vacuum_cost_limit = 800;
ALTER SYSTEM SET autovacuum_vacuum_cost_delay = '2ms';

-- 테이블별 autovacuum 튜닝
ALTER TABLE orders SET (
    autovacuum_vacuum_threshold = 1000,
    autovacuum_vacuum_scale_factor = 0.02,
    autovacuum_analyze_threshold = 500,
    autovacuum_analyze_scale_factor = 0.01
);

-- VACUUM 진행 상황 모니터링
SELECT
    relname,
    phase,
    heap_blks_total,
    heap_blks_scanned,
    ROUND(100.0 * heap_blks_scanned / 
          NULLIF(heap_blks_total, 0), 1) AS pct_done
FROM pg_stat_progress_vacuum;

5. NOT NULL 제약조건 최적화

쿼리 플래너가 NOT NULL 제약조건을 활용하여 불필요한 IS NOT NULL 검사를 제거한다.

-- user_id에 NOT NULL 제약이 있으면
-- PostgreSQL 17이 자동으로 최적화
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id IS NOT NULL  -- 플래너가 자동 제거
  AND status = 'pending';

-- CTE(WITH 절)도 최적화 개선
WITH recent_orders AS (
    SELECT user_id, SUM(amount) as total
    FROM orders
    WHERE created_at > NOW() - INTERVAL '7 days'
    GROUP BY user_id
)
SELECT u.name, ro.total
FROM users u
JOIN recent_orders ro ON u.id = ro.user_id
WHERE ro.total > 500;
-- PG17: CTE가 인라인되어 전체 쿼리 최적화 가능

6. 증분 백업

PostgreSQL 17에서 증분 백업이 공식 지원된다.

# 전체 백업 (기준점)
pg_basebackup -D /backup/full \
  --checkpoint=fast \
  --manifest-checksums=SHA256

# 증분 백업 (변경분만)
pg_basebackup -D /backup/incr1 \
  --incremental=/backup/full/backup_manifest

# 증분 백업 결합하여 복원
pg_combinebackup \
  /backup/full \
  /backup/incr1 \
  -o /backup/restored

# 복원
pg_restore -D /var/lib/postgresql/data /backup/restored

마이그레이션 체크리스트

  • pg_upgrade로 업그레이드 전 pg_upgrade --check로 호환성 확인
  • 확장 모듈(PostGIS, pgvector 등) 호환 버전 확인
  • VACUUM ANALYZE 전체 실행으로 통계 재수집
  • 쿼리 실행 계획 변경 검토 (EXPLAIN ANALYZE로 핵심 쿼리 확인)
  • pg_stat_statements로 성능 회귀 모니터링

PostgreSQL 17의 쿼리 최적화 개선은 별도의 코드 변경 없이도 기존 쿼리의 성능을 향상시킨다. 특히 IN 절이 많은 OLTP 워크로드와 JSON 데이터를 다루는 애플리케이션에서 체감 효과가 크다.