
- PostgreSQL 18이 바꾸는 것들
- 업그레이드 전 호환성 점검
- pg_upgrade 실전 절차
- PG 18 신규 기능 활용: Virtual Generated Columns
- PG 18 신규 기능 활용: UUIDv7
- PG 18 신규 기능 활용: RETURNING 절의 OLD/NEW
- PG 18 성능 튜닝: AIO 서브시스템 설정
- 업그레이드 트러블슈팅
- 업그레이드 체크리스트
- 퀴즈
- References
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에서 OLD와 NEW 테이블을 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 scan | 12.4s | 6.8s | 4.2s |
| Parallel seq scan (4 workers) | 4.1s | 2.3s | 1.5s |
| VACUUM (200만 dead tuples) | 9.1s | 5.4s | 3.8s |
| CREATE INDEX | 45s | 28s | 22s |
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;
시나리오 2: --link 모드에서 남은 이전 클러스터 처리
# 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
현재 단락 (1/250)
PostgreSQL 18은 2025년 9월 25일에 정식 릴리스되었다. 이 버전에서 가장 주목할 변경은 세 가지다.