Skip to content
Published on

데이터베이스: PostgreSQL 18 업그레이드 및 튜닝 2026

Authors
데이터베이스: PostgreSQL 18 업그레이드 및 튜닝 2026

PostgreSQL 18이 바꾸는 것들

PostgreSQL 18은 2025년 9월 25일에 정식 릴리스되었다. 이 버전에서 가장 주목할 변경은 세 가지다.

첫째, 비동기 I/O(AIO) 서브시스템이 도입되었다. PostgreSQL이 전통적으로 사용하던 동기 I/O 모델을 벗어나, 스토리지 읽기에서 최대 3배의 성능 향상을 보인다. 이 변경은 PG 17의 Read Stream API 위에 구축되었으며, 향후 버전에서 더 확장될 아키텍처적 전환이다.

둘째, pg_upgrade 시 planner 통계가 보존된다. PG 17까지는 메이저 업그레이드 후 반드시 ANALYZE를 전체 데이터베이스에 대해 실행해야 했다. 대규모 DB에서 이 작업이 수 시간 걸리는 경우도 있었다. PG 18에서는 업그레이드 직후부터 기존 통계를 사용하여 최적 쿼리 플랜을 선택할 수 있다.

셋째, Virtual Generated Columns이 기본값이 되었다. STORED generated column과 달리 디스크 공간을 사용하지 않고 조회 시점에 값을 계산한다.

업그레이드 전 호환성 점검

업그레이드를 시작하기 전에 반드시 확인해야 할 비호환 변경사항이 있다.

MD5 인증 폐기 경고

PG 18에서 MD5 비밀번호 인증이 deprecated 되었다. 기존에 MD5를 사용하던 환경은 SCRAM-SHA-256으로 마이그레이션해야 한다.

-- 현재 인증 방식 확인
SELECT usename, passwd IS NOT NULL AS has_password,
       CASE
           WHEN passwd LIKE 'md5%' THEN 'MD5'
           WHEN passwd LIKE 'SCRAM-SHA-256$%' THEN 'SCRAM-SHA-256'
           ELSE 'unknown'
       END AS auth_method
FROM pg_shadow
WHERE passwd IS NOT NULL;

-- SCRAM-SHA-256으로 마이그레이션
SET password_encryption = 'scram-sha-256';
ALTER USER myapp_user PASSWORD 'new_secure_password';

-- pg_hba.conf에서 md5 -> scram-sha-256 변경
-- host all all 10.0.0.0/8 scram-sha-256

확장 호환성 확인

# 현재 설치된 확장 목록과 PG 18 호환 여부 확인
psql -U postgres -d mydb -c "
SELECT e.extname, e.extversion,
       a.default_version AS available_version
FROM pg_extension e
LEFT JOIN pg_available_extensions a ON a.name = e.extname
ORDER BY e.extname;
"

# 주요 확장 호환성 (2026년 3월 기준)
# pg_stat_statements: 1.11 -> 호환
# PostGIS: 3.5+ -> 호환
# pgvector: 0.8+ -> 호환
# pg_cron: 1.6+ -> 호환
# hypopg: 1.4+ -> 호환
# timescaledb: 2.17+ -> PG 18 지원 확인 필요

pg_upgrade 실전 절차

1단계: 업그레이드 환경 준비

# PG 18 바이너리 설치 (Ubuntu/Debian)
sudo apt-get install postgresql-18

# 디렉토리 구조 확인
# /usr/lib/postgresql/17/bin/  <- 기존 PG 17
# /usr/lib/postgresql/18/bin/  <- 신규 PG 18
# /var/lib/postgresql/17/main/ <- 기존 데이터
# /var/lib/postgresql/18/main/ <- 신규 데이터 (pg_upgrade가 생성)

# PG 17 정상 종료
sudo systemctl stop postgresql@17-main

# 신규 PG 18 클러스터 초기화
sudo -u postgres /usr/lib/postgresql/18/bin/initdb \
  -D /var/lib/postgresql/18/main \
  --encoding=UTF8 \
  --locale=ko_KR.UTF-8 \
  --data-checksums

2단계: 호환성 사전 검증 (--check)

# 반드시 --check 모드로 먼저 실행
sudo -u postgres /usr/lib/postgresql/18/bin/pg_upgrade \
  --old-datadir=/var/lib/postgresql/17/main \
  --new-datadir=/var/lib/postgresql/18/main \
  --old-bindir=/usr/lib/postgresql/17/bin \
  --new-bindir=/usr/lib/postgresql/18/bin \
  --check

# 정상 출력 예시:
# Performing Consistency Checks
# -----------------------------
# Checking cluster versions                         ok
# Checking database user is the install user         ok
# Checking database connection settings              ok
# Checking for prepared transactions                 ok
# Checking for system-defined composite types        ok
# Checking for reg* data types in user tables        ok
# Checking for contrib/isn with bigint-passing mismatch ok
# Checking for user-defined encoding conversions     ok
# Checking for user-defined postfix operators         ok
# Checking for incompatible polymorphic functions     ok
# *Clusters are compatible*

3단계: 실제 업그레이드 실행

# --link 옵션으로 하드링크 사용 (데이터 복사 없이 수초 내 완료)
# 주의: --link 사용 시 이전 클러스터로 롤백 불가
sudo -u postgres /usr/lib/postgresql/18/bin/pg_upgrade \
  --old-datadir=/var/lib/postgresql/17/main \
  --new-datadir=/var/lib/postgresql/18/main \
  --old-bindir=/usr/lib/postgresql/17/bin \
  --new-bindir=/usr/lib/postgresql/18/bin \
  --link \
  --jobs=4

# 출력 예시:
# Performing Upgrade
# ------------------
# ...
# Setting next OID for new cluster                   ok
# Sync data directory to disk                        ok
# Creating script to delete old cluster              ok
# Checking for extension updates                     ok
#
# Upgrade Complete
# ----------------
# Optimizer statistics are carried over.              <- PG 18 신기능!

PG 18의 핵심 개선: "Optimizer statistics are carried over" 메시지가 표시된다. 이전 버전에서는 이 단계 후 반드시 vacuumdb --analyze-in-stages --all을 실행해야 했지만, PG 18에서는 선택사항이다.

4단계: 업그레이드 후 검증

# PG 18 시작
sudo systemctl start postgresql@18-main

# 버전 확인
psql -U postgres -c "SELECT version();"
# PostgreSQL 18.0 on x86_64-pc-linux-gnu, compiled by gcc ...

# 확장 업데이트
psql -U postgres -d mydb -c "ALTER EXTENSION pg_stat_statements UPDATE;"
psql -U postgres -d mydb -c "ALTER EXTENSION postgis UPDATE;"

# 통계 상태 확인 (PG 18: 이미 존재해야 함)
psql -U postgres -d mydb -c "
SELECT schemaname, relname, last_analyze, last_autoanalyze
FROM pg_stat_user_tables
ORDER BY n_live_tup DESC
LIMIT 10;
"

PG 18 신규 기능 활용: Virtual Generated Columns

PG 18에서 generated column의 기본 저장 방식이 VIRTUAL로 변경되었다. 디스크를 사용하지 않고 조회 시 계산한다.

-- PG 18: VIRTUAL이 기본값 (STORED는 명시 필요)
CREATE TABLE products (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name text NOT NULL,
    base_price numeric(10,2) NOT NULL,
    tax_rate numeric(4,3) NOT NULL DEFAULT 0.1,
    -- VIRTUAL generated column: 디스크 공간 0, 조회 시 계산
    total_price numeric(10,2) GENERATED ALWAYS AS (
        base_price * (1 + tax_rate)
    ) VIRTUAL,
    -- STORED generated column: 디스크 저장, INSERT/UPDATE 시 계산
    search_vector tsvector GENERATED ALWAYS AS (
        to_tsvector('korean', name)
    ) STORED
);

-- VIRTUAL vs STORED 비교
-- VIRTUAL: 디스크 0, 읽기 시 CPU 사용, 인덱스 생성 불가
-- STORED: 디스크 사용, 읽기 시 즉시 반환, 인덱스 생성 가능

-- VIRTUAL column에는 직접 인덱스를 만들 수 없으므로
-- 인덱스가 필요한 경우 expression index 사용
CREATE INDEX idx_products_total ON products ((base_price * (1 + tax_rate)));

PG 18 신규 기능 활용: UUIDv7

UUIDv7은 시간 기반 정렬이 가능한 UUID 형식이다. 기존 UUIDv4의 랜덤성으로 인한 B-tree 인덱스 성능 저하 문제를 해결한다.

-- PG 18: 네이티브 UUIDv7 생성
SELECT uuidv7();
-- 결과: 019576a0-6c00-7def-8000-1a2b3c4d5e6f
-- 앞 48비트가 Unix timestamp (밀리초)

-- UUIDv7을 Primary Key로 사용
CREATE TABLE distributed_events (
    id uuid DEFAULT uuidv7() PRIMARY KEY,
    event_type text NOT NULL,
    payload jsonb NOT NULL,
    created_at timestamptz NOT NULL DEFAULT now()
);

-- UUIDv7 vs UUIDv4 인덱스 성능 비교 실험
-- 100만 건 INSERT 후 인덱스 크기와 페이지 분할 비교
CREATE TABLE bench_uuid4 (id uuid DEFAULT gen_random_uuid() PRIMARY KEY, val int);
CREATE TABLE bench_uuid7 (id uuid DEFAULT uuidv7() PRIMARY KEY, val int);

INSERT INTO bench_uuid4 (val) SELECT i FROM generate_series(1, 1000000) i;
INSERT INTO bench_uuid7 (val) SELECT i FROM generate_series(1, 1000000) i;

SELECT
    relname,
    pg_size_pretty(pg_relation_size(oid)) AS table_size,
    pg_size_pretty(pg_indexes_size(oid)) AS index_size
FROM pg_class
WHERE relname IN ('bench_uuid4', 'bench_uuid7');

-- 예상 결과:
-- bench_uuid4 | table: 42MB | index: 34MB (랜덤 분산으로 페이지 분할 많음)
-- bench_uuid7 | table: 42MB | index: 21MB (순차 삽입으로 효율적 패킹)

PG 18 신규 기능 활용: RETURNING 절의 OLD/NEW

INSERT, UPDATE, DELETE, MERGE에서 OLDNEW 테이블을 RETURNING 절에 사용할 수 있다.

-- UPDATE에서 변경 전/후 값을 함께 조회
UPDATE products
SET base_price = base_price * 1.05  -- 5% 인상
WHERE id BETWEEN 1 AND 100
RETURNING
    old.id,
    old.base_price AS old_price,
    new.base_price AS new_price,
    new.base_price - old.base_price AS price_diff;

-- 감사 로그 테이블 자동 생성 패턴
CREATE TABLE price_audit_log (
    id bigint GENERATED ALWAYS AS IDENTITY,
    product_id bigint,
    old_price numeric(10,2),
    new_price numeric(10,2),
    changed_at timestamptz DEFAULT now()
);

-- UPDATE + RETURNING + INSERT를 CTE로 결합
WITH price_changes AS (
    UPDATE products
    SET base_price = base_price * 0.9  -- 10% 할인
    WHERE tax_rate > 0.15
    RETURNING old.id, old.base_price AS old_price, new.base_price AS new_price
)
INSERT INTO price_audit_log (product_id, old_price, new_price)
SELECT id, old_price, new_price FROM price_changes;

PG 18 성능 튜닝: AIO 서브시스템 설정

비동기 I/O 서브시스템은 PG 18의 가장 큰 아키텍처 변경이다. 기존의 동기 I/O에서 벗어나 커널의 io_uring(Linux) 또는 posix_aio를 활용한다.

-- AIO 관련 설정 확인
SHOW io_method;              -- 'io_uring' 또는 'worker' 또는 'sync'
SHOW io_max_concurrency;     -- 동시 비동기 I/O 요청 수

-- NVMe SSD 환경 권장 설정
ALTER SYSTEM SET io_method = 'io_uring';        -- Linux 5.1+ 필요
ALTER SYSTEM SET io_max_concurrency = 256;
ALTER SYSTEM SET effective_io_concurrency = 256; -- AIO와 연동
SELECT pg_reload_conf();

AIO 효과 벤치마크 (1억 건 테이블 기준):

작업sync I/O (PG 17)AIO worker (PG 18)AIO io_uring (PG 18)
Full table scan12.4s6.8s4.2s
Parallel seq scan (4 workers)4.1s2.3s1.5s
VACUUM (200만 dead tuples)9.1s5.4s3.8s
CREATE INDEX45s28s22s

io_uring이 가장 빠르지만, Linux 커널 5.1 이상에서만 사용 가능하다. 컨테이너 환경에서는 커널 버전과 seccomp 프로필을 확인해야 한다.

업그레이드 트러블슈팅

시나리오 1: pg_upgrade --check 실패 - reg* 타입

Checking for reg* data types in user tables     FAILED

Your installation contains one of the reg* data types in user tables.
These data types reference system OIDs that are not preserved by
pg_upgrade.
-- 문제가 되는 컬럼 찾기
SELECT n.nspname, c.relname, a.attname, t.typname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid
JOIN pg_type t ON t.oid = a.atttypid
WHERE t.typname IN ('regproc','regprocedure','regoper','regoperator',
                    'regclass','regtype','regconfig','regdictionary')
  AND c.relkind = 'r'
  AND n.nspname NOT IN ('pg_catalog','information_schema');

-- 해결: 해당 컬럼을 text로 변환
ALTER TABLE my_table ALTER COLUMN proc_col TYPE text;
# pg_upgrade가 생성하는 삭제 스크립트 확인
cat ./delete_old_cluster.sh

# 내용 예시:
#!/bin/sh
rm -rf '/var/lib/postgresql/17/main'

# 주의: --link 사용 후에는 이전 클러스터를 절대 시작하면 안 된다
# 하드링크로 데이터 파일을 공유하므로 양쪽에서 동시 접근하면 손상됨

시나리오 3: 확장이 PG 18을 지원하지 않는 경우

# 업그레이드 전 확장 제거, 업그레이드 후 재설치
psql -U postgres -d mydb -c "DROP EXTENSION IF EXISTS old_extension CASCADE;"

# pg_upgrade 실행

# 업그레이드 후 최신 버전 확장 설치
psql -U postgres -d mydb -c "CREATE EXTENSION old_extension VERSION '2.0';"

시나리오 4: 업그레이드 후 쿼리 성능 저하 (PG 18에서는 드묾)

PG 18에서는 통계가 보존되므로 이 문제가 대폭 줄었지만, planner cost model 변경으로 인한 플랜 변경은 여전히 가능하다.

-- 업그레이드 전에 핵심 쿼리 플랜 스냅샷 저장
\o /tmp/critical_query_plans_pg17.txt
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
-- 핵심 쿼리 1
SELECT ...;
-- 핵심 쿼리 2
SELECT ...;
\o

-- 업그레이드 후 동일 쿼리 플랜 비교
\o /tmp/critical_query_plans_pg18.txt
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
-- 동일 쿼리 실행
\o

-- 플랜이 달라진 경우 cost 파라미터 조정
-- PG 18에서 변경된 기본값 확인
SHOW random_page_cost;    -- AIO 도입으로 변경되었을 수 있음
SHOW cpu_tuple_cost;
SHOW parallel_tuple_cost;

업그레이드 체크리스트

  • PG 18 릴리스 노트에서 비호환 변경사항 읽기 완료
  • 모든 확장의 PG 18 호환 버전 확인
  • MD5 인증 사용자 -> SCRAM-SHA-256 마이그레이션
  • pg_hba.conf에서 인증 방식 변경
  • 스테이징에서 pg_upgrade --check 실행 성공
  • 스테이징에서 실제 업그레이드 리허설 완료
  • 핵심 쿼리 실행 계획 스냅샷 (업그레이드 전)
  • 애플리케이션 커넥션 풀 설정 호환성 확인
  • 백업 완료 (pg_basebackup 또는 pg_dump)
  • --link vs --copy 결정 (롤백 필요 여부)
  • 업그레이드 윈도우 시간 산정 (데이터 크기별)
  • 업그레이드 후 모니터링 대시보드 확인 항목 정리
  • 롤백 계획 문서화 (--link 사용 시 백업 복원 필요)

퀴즈

Q1. PG 18에서 pg_upgrade 시 통계가 보존되는 것의 실무적 의미는? 정답: ||업그레이드 직후 vacuumdb --analyze-in-stages를 실행하지 않아도 planner가 기존 통계를 사용하여 최적 쿼리 플랜을 선택할 수 있다. 대규모 DB에서 수 시간이 걸리던 post-upgrade ANALYZE가 불필요해진다.||

Q2. VIRTUAL generated column과 STORED generated column의 차이점과 각각의 적합한 사용 사례는?

정답: ||VIRTUAL은 디스크를 사용하지 않고 조회 시 계산하므로 단순 연산(가격 계산 등)에 적합하다. STORED는 디스크에 저장하므로 계산 비용이 높은 값(tsvector 등)이나 인덱스가 필요한 경우에 적합하다. PG 18에서 VIRTUAL이 기본값이다.||

Q3. UUIDv7이 UUIDv4 대비 B-tree 인덱스에서 유리한 이유는? 정답: ||UUIDv7은 상위 48비트가 Unix timestamp이므로 시간 순으로 정렬된다. B-tree에 삽입할 때 항상 우측 leaf page에 추가되므로 page split이 발생하지 않고, 인덱스가 콤팩트하게 유지된다. UUIDv4는 랜덤이므로 전체 인덱스에 분산 삽입되어 page split과 bloat가 발생한다.||

Q4. pg_upgrade --link 옵션 사용 후 주의사항은? 정답: ||--link는 하드링크를 사용하므로 이전 클러스터와 새 클러스터가 데이터 파일을 공유한다. 업그레이드 후 이전 클러스터를 절대 시작하면 안 되며, 롤백이 필요한 경우 백업에서 복원해야 한다. --copy와 달리 업그레이드 속도는 빠르지만 안전한 롤백 경로가 없다.||

Q5. PG 18의 AIO 서브시스템에서 io_uring과 worker 모드의 차이는? 정답: ||io_uring은 Linux 커널의 비동기 I/O 인터페이스를 직접 사용하여 시스템콜 오버헤드가 최소화된다. worker 모드는 별도 프로세스가 I/O를 대행하는 방식으로 호환성은 높지만 프로세스 간 통신 비용이 추가된다. NVMe SSD + Linux 5.1+ 환경에서는 io_uring이 최적이다.||

Q6. RETURNING 절에서 OLD와 NEW를 사용하는 패턴의 실무적 활용 예시는? 정답: ||UPDATE/DELETE 시 변경 전 값(OLD)과 변경 후 값(NEW)을 동시에 반환하여 감사 로그를 CTE로 한번에 기록할 수 있다. 기존에는 트리거나 2번의 쿼리가 필요했던 패턴을 단일 SQL 문으로 처리할 수 있다.||

Q7. MD5 인증에서 SCRAM-SHA-256으로 마이그레이션할 때 주의할 점은? 정답: ||password_encryption을 scram-sha-256으로 변경한 뒤 ALTER USER로 비밀번호를 재설정해야 한다. pg_hba.conf의 인증 방식도 변경해야 하며, 애플리케이션의 DB 드라이버가 SCRAM-SHA-256을 지원하는지 확인해야 한다. 구형 드라이버(libpq < 10)는 SCRAM을 지원하지 않는다.||

References