본문으로 건너뛰기

"데이터베이스" 태그로 연결된 34개 게시물개의 게시물이 있습니다.

모든 태그 보기

PostgreSQL 18 비동기 I/O

· 약 10분

17년을 동기 I/O로 버텨온 데이터베이스

PostgreSQL은 1996년에 첫 릴리스가 나온 이래로 process-per-connection 모델 + 동기 I/O라는 단순한 조합을 17년 넘게 유지해왔어요. 클라이언트 하나가 붙으면 백엔드 프로세스 하나가 fork되고, 그 프로세스는 디스크에서 페이지를 읽을 때 read() 시스템 콜을 직접 호출해서 결과가 돌아올 때까지 그냥 멈춰서 기다려요.

이 모델은 단순함이 가장 큰 무기였습니다. fork 한 번이면 격리되고 신호 처리도 직관적이며, 잠금이나 컨텍스트 스위치 같은 까다로운 동시성 이슈도 적게 신경 써도 됩니다. MySQL이나 SQL Server가 thread-per-connection 모델로 가면서 동시성 버그와 평생을 싸우는 동안, PostgreSQL은 다른 길을 갔습니다.

문제는 2020년대의 NVMe와 클라우드 스토리지가 그 단순함의 대가를 점점 더 비싸게 만들고 있다는 점입니다.

PostgreSQL 18(2025-09-25 GA)은 17년 만에 처음으로 비동기 I/O 서브시스템을 들고 왔습니다. 이 글에서는 그 구조와 트레이드오프를 살펴봅니다.

동기 I/O의 정확한 병목

PostgreSQL이 디스크에서 데이터를 읽을 때의 기본 단위는 8KB 페이지 한 장입니다. Sequential Scan 한 번에 100만 페이지를 읽어야 한다면, 옛 모델은 이렇게 동작합니다.

Sequential Scan (cold cache):

read(page 1) → 디스크 응답 대기 (lat) → 처리
read(page 2) → 디스크 응답 대기 (lat) → 처리
read(page 3) → 디스크 응답 대기 (lat) → 처리
...

총 시간 ≈ 페이지 수 × 디스크 latency (직렬)

CPU는 매 페이지마다 디스크를 기다리며 놀고, NVMe는 큐가 대부분 비어 있는 채로 능력의 일부만 씁니다. 클라우드 EBS처럼 한 IOP의 latency가 수백 마이크로초인 환경에서는 이 직렬 누적이 잔인하게 드러납니다.

PostgreSQL도 손 놓고 있던 건 아닙니다. 17 버전까지는 OS의 readahead와 posix_fadvise(POSIX_FADV_WILLNEED) 정도로 커널에 "앞으로 이 영역을 읽을 테니 미리 좀 가져와"라고 힌트만 줬습니다. 동작은 하지만, PostgreSQL이 직접 I/O 깊이를 통제하지 못하기 때문에 어떤 페이지가 언제 도착할지를 알 수 없었습니다. 통계 기반 readahead는 패턴이 깨지면 무력해집니다.

"This feature allows backends to queue multiple read requests, which allows for more efficient sequential scans, bitmap heap scans, vacuums, etc."PostgreSQL 18 Release Notes, E.4.3.1.3

PostgreSQL 18의 한 줄 요지는 이렇습니다. "여러 read를 큐에 넣고 동시에 보냅니다." 페이지마다 멈추지 않고 100개를 한꺼번에 던져놓은 뒤, NVMe가 알아서 병렬로 처리하게 두자는 이야기입니다.

세 가지 io_method

PostgreSQL 18은 새 GUC 파라미터 io_method로 비동기 I/O 동작을 고릅니다. 세 가지 옵션이 있습니다.

모드어디서 동작환경 요건장점단점
sync메인 백엔드모든 OSPG17과 동일, 호환성 100%, 추가 프로세스 없음사실상 비동기 X, NVMe/EBS 큐 활용 못 함
workerI/O 워커 프로세스모든 OS (default)어디서든 돌고 sync 대비 1.6배, 운영 안정성 검증된 디폴트워커 프로세스 비용, 컨텍스트 스위치/메모리 카피 오버헤드
io_uring커널과 ring buffer 공유Linux 5.1+sync 대비 2.7배, 워커 없이 syscall 거의 0일부 배포판/컨테이너에서 seccomp으로 차단, ARM/macOS/BSD 불가

기본값은 worker입니다. 모든 운영체제에서 동작하면서도 동기보다는 확연히 빠르기 때문에 안전한 선택입니다. macOS, BSD 또는 io_uring을 못 쓰는 환경에서는 그대로 default로 두면 됩니다.

함께 추가된 GUC들도 알아둘 만합니다(공식 release notes).

io_workersworker 모드의 워커 프로세스 수이며 기본값은 3입니다. io_combine_limitio_max_combine_limit는 인접 read를 한 요청으로 합치는 한도입니다. effective_io_concurrency의 기본값은 1 → 16으로 올랐으며, 이 값은 사실상 "동시에 던질 read 수"입니다. maintenance_io_concurrency는 VACUUM 같은 유지보수 작업의 동시 I/O를 조절합니다.

fadvise()가 없는 OS에서도 effective_io_concurrency > 0이 의미를 갖게 됐다는 점도 조용히 큰 변화입니다. 이전에는 Linux 외 환경에서 이 파라미터가 사실상 장식이었습니다.

worker 모드

누군가 대신 디스크를 읽는 방식입니다.

worker의 구조는 의외로 깔끔합니다.

백엔드는 read 요청을 큐에 넣고 다른 일을 합니다. 별도의 I/O 워커 프로세스 풀이 큐에서 꺼내 실제 read() syscall을 호출하고, 결과를 공유 버퍼에 채워줍니다. 백엔드는 자기가 요청한 페이지가 필요한 시점에 공유 버퍼만 들여다보면 됩니다.

장점은 호환성입니다. POSIX read만 있으면 어디서든 동작합니다. 단점은 워커 프로세스 자체가 일종의 디스패처라서 컨텍스트 스위치와 메모리 카피 비용이 발생한다는 점입니다.

io_uring 모드

커널에 ring을 심는 방식입니다. io_uring은 Linux 5.1(2019년)에 들어온 커널 비동기 I/O 인터페이스입니다. PostgreSQL과 커널 사이에 공유 메모리 ring 두 개를 두고, 시스템 콜 없이 요청을 주고받습니다.

PostgreSQL 18 구현의 흥미로운 결정 하나는 ring 인스턴스를 backend마다 따로 둔다는 점입니다. 한 인스턴스를 여러 백엔드가 공유하면 lock contention이 생기므로 아예 격리했습니다. 다만 ring 자체는 fork 전에 postmaster가 미리 생성해서 shared memory에 올려둡니다(credativ deep-dive).

워커가 빠지므로 컨텍스트 스위치가 사라지고, syscall도 제출과 대기를 잘 묶으면 거의 0에 수렴합니다. 다만 Linux 5.1+ 전용이며, 일부 배포판은 보안 정책상 io_uring을 비활성화해두기도 합니다(예: 특정 컨테이너 런타임의 seccomp 프로파일).

pg_aios

PostgreSQL의 I/O 큐를 처음으로 들여다볼 수 있게 됐습니다. 운영자 입장에서 더 반가운 변화는 새 시스템 뷰 하나입니다. pg_aios는 진행 중인 비동기 I/O 요청을 그대로 보여줍니다.

SELECT pid, io_method, op, state, target, off, length
FROM pg_aios
ORDER BY pid, off
LIMIT 20;

지금까지 PostgreSQL의 I/O는 거의 블랙박스였습니다. pg_stat_io(PostgreSQL 16에서 들어옴)는 누적 통계를, pg_stat_activity는 wait event 정도를 보여줬습니다. "바로 지금 어떤 read가 큐에 떠 있는가"는 이제야 들여다볼 수 있게 됐습니다.

여기서 잡히는 정보로 특정 파일에 I/O가 몰리는 hot relation을 식별하거나, io_uring이 큐를 정말 깊게 쓰고 있는지를 확인할 수 있습니다.

벤치마크

숫자로 성능 차이를 살펴봅니다.

pganalyze 벤치마크는 AWS c7i.8xlarge에서 3.5GB 테이블의 cold scan을 측정했습니다.

버전 / 모드실행 시간대 PG17대 PG18 sync
PostgreSQL 17 (sync)15,830 ms기준해당 없음
PostgreSQL 18 sync15,071 ms-5%기준
PostgreSQL 18 worker10,051 ms-37%-33%
PostgreSQL 18 io_uring5,723 ms-64%-62%

cold cache Sequential Scan에서 io_uringPostgreSQL 17 대비 2.7배 빠릅니다. worker도 1.6배 빠릅니다. 같은 PostgreSQL 18을 sync로만 켜두면 거의 차이가 없다는 점도 중요한 신호입니다. 모드 선택이 곧 성능입니다.

이 향상이 어디서 오는지 한 줄로 정리하면, 디스크 latency를 여러 번 직렬로 치르던 것을 한 번 병렬로 치르게 된 것뿐입니다. NVMe는 원래 그렇게 쓰는 물건이었는데, PostgreSQL이 17년 만에 그 사용법을 익혔습니다.

한계

아직 비동기가 아닌 작업도 있습니다.

PostgreSQL 18 AIO는 읽기 작업 일부에만 적용됩니다.

작업PG18 AIO 적용?
Sequential Scan적용
Bitmap Heap Scan적용
VACUUM적용
ANALYZE (일부)적용
Index Scan random read아직 동기
WAL write미적용
Checkpoint write미적용

쓰기는 전부 동기 그대로입니다. 인덱스 random read도 들어가지 않았습니다. 즉 OLTP 점 쿼리 워크로드는 이번 변화로 직접 빨라지지는 않고, 이득은 분석 워크로드/대량 스캔/VACUUM에 몰려 있습니다.

PostgreSQL 19(2026-09 예정) 개발 트리에서는 인덱스 prefetch와 일부 쓰기 경로의 비동기화가 논의되고 있습니다. AIO는 PostgreSQL 18에서 끝난 게 아니라 시작된 것에 가깝습니다.

운영 관점

무엇을 켤지 선택하는 기준은 사실 단순합니다.

환경추천
Linux 5.1+ 직접 운영, io_uring 사용 가능io_uring
Linux지만 io_uring 비활성 배포판/컨테이너worker (default)
macOS / BSD / 구형 Linuxworker (default)
호환성 이슈 발생 시 임시 폴백sync

추가로 함께 조정할 만한 설정은 다음과 같습니다.

# postgresql.conf 예시 (분석 워크로드 기준)
io_method = io_uring
effective_io_concurrency = 32 # 기본 16에서 NVMe 깊이에 맞게 조정
maintenance_io_concurrency = 32 # VACUUM 가속
io_combine_limit = 256kB

effective_io_concurrency는 "동시에 던질 read 수"라고 생각하면 됩니다. NVMe 큐 깊이 / 동시 사용자 수에 맞춰 늘립니다. 너무 키우면 다른 백엔드와 디스크 대역을 놓고 다투게 되니, 분석 전용 인스턴스가 아니라면 기본 16에서 천천히 올립니다.

배포 전에는 다음 항목을 확인합니다.

  1. 커널 버전(uname -r): 5.1 미만이면 io_uring 불가
  2. seccomp/AppArmor 프로파일이 io_uring 시스템 콜을 막는지 확인
  3. pg_aios 뷰가 보이는지로 AIO 활성 검증
  4. cold scan 워크로드에서 EXPLAIN (ANALYZE, BUFFERS) 비교 측정

한국 커뮤니티 반응

GeekNews에는 이미 작년에 Postgres 18을 기다리며: 비동기 I/O로 디스크 읽기 속도 향상이 올라왔습니다. 댓글은 많지 않지만 톤은 거의 "드디어"에 가깝고, 관심사는 io_uring 보안 우려와 클라우드 NVMe에서의 실측 향상에 집중돼 있었습니다. 1년이 지난 지금 PostgreSQL 18이 GA된 상태에서 그 기대가 어느 정도 채워졌다고 봐도 됩니다.

정리

PG17 이하PG18
I/O 모델동기 (메인 백엔드 직접 syscall)동기 + worker + io_uring
모드 선택없음io_method GUC
가시성pg_stat_io (누적 통계)+ pg_aios (실시간 큐)
effective_io_concurrencyLinux fadvise 한정모든 OS, 기본 16
Cold scan 성능 (3.5GB)15.8초5.7초 (io_uring)
적용 범위Sequential/Bitmap Scan, VACUUM
미적용Index random read, WAL/Checkpoint write

PostgreSQL이 process-per-connection이라는 17년짜리 아키텍처 결정을 바꾸지 않고서도 비동기 I/O를 들였다는 점이 이번 변화의 진짜 핵심입니다. 백엔드 프로세스 모델은 그대로 두고 syscall 경계만 다시 그었습니다. 그래서 운영자 입장에서 마이그레이션이 거의 무료이며, io_method 한 줄만 바꾸면 됩니다.

다음 PostgreSQL 18 시리즈 글에서는 UUIDv7과 B-tree 인덱스의 관계를 다룰 예정이에요. 같은 "스토리지 효율"이라는 축에서, 이번에는 인덱스 페이지 분할 쪽 이야기를 해보려고 해요.

참고 자료

PG19 파티션 MERGE/SPLIT

· 약 5분

PostgreSQL 파티셔닝의 짧은 역사

PostgreSQL의 파티셔닝은 10 버전에서 선언적(declarative) 문법이 들어온 이후 꾸준히 성숙해왔어요. 13에서 UPDATE로 파티션 간 row 이동이 가능해졌고, 14에서 DETACH PARTITION CONCURRENTLY가 추가됐어요. 그리고 PostgreSQL 19에서 마침내 파티션 자체를 병합하고 분할하는 DDL이 들어와요.

ALTER TABLE ... SPLIT PARTITION ...
ALTER TABLE ... MERGE PARTITIONS ...

익숙한 문법입니다. Oracle을 써본 사람이라면 20년 넘게 봐온 구문과 거의 판박입니다. 다만 자세히 보면 문법과 제약에서 결정적인 차이들이 있습니다.

이전까지 PostgreSQL에서는 어떻게 했나

PostgreSQL 18 이하에서 파티션을 병합하려면 이런 절차를 거쳤습니다.

  1. 새로운 합쳐진 파티션 테이블을 생성
  2. 기존 파티션에서 INSERT SELECT로 데이터 이동
  3. 기존 파티션 DETACH
  4. 새 파티션 ATTACH
  5. 기존 파티션 DROP

한 번에 트랜잭션으로 묶기도 까다롭고, 데이터 이동 중 ACCESS EXCLUSIVE LOCK이 걸려 서비스 가용성에도 영향을 줍니다. 분할도 마찬가지로 귀찮았습니다. Oracle에서는 한 줄이면 끝나는 작업이었습니다.

PostgreSQL 19의 새 문법

SPLIT과 MERGE를 각각 예제로 살펴보겠습니다.

SPLIT PARTITION

-- 원본 파티션 테이블
CREATE TABLE sales (
id bigint generated always as identity,
region text not null,
amount numeric
) PARTITION BY LIST (region);

CREATE TABLE sales_all PARTITION OF sales
FOR VALUES IN ('KR', 'JP', 'CN', 'US', 'UK');

-- 하나의 파티션을 셋으로 분할
ALTER TABLE sales SPLIT PARTITION sales_all INTO (
PARTITION sales_asia FOR VALUES IN ('KR', 'JP', 'CN'),
PARTITION sales_us FOR VALUES IN ('US'),
PARTITION sales_uk FOR VALUES IN ('UK')
);

MERGE PARTITIONS

-- 세 파티션을 하나로 합치기
ALTER TABLE sales MERGE PARTITIONS
(sales_asia, sales_us, sales_uk)
INTO sales_all;

문법에서 눈여겨볼 점은 다음과 같습니다.

  • 지원 타입: RANGE, LIST (HASH는 지원하지 않음)
  • RANGE 파티션은 인접(adjacent) 해야 병합 가능
  • LIST는 인접 제약 없음
  • 소스 파티션들은 괄호 (...)로 감싼다
  • 대상 파티션에 PARTITION 키워드를 붙이지 않는다

Oracle의 문법

같은 일을 Oracle에서 하면 이렇게 됩니다. Oracle 쪽은 8i 시절부터 존재해온 문법이라 변주가 많습니다.

SPLIT PARTITION

-- RANGE 파티션 분할 (AT 값 기준)
ALTER TABLE sales SPLIT PARTITION sales_2026 AT (DATE '2026-07-01')
INTO (
PARTITION sales_2026_h1 TABLESPACE ts1,
PARTITION sales_2026_h2 TABLESPACE ts2
);

-- LIST 파티션 분할 (VALUES 기준)
ALTER TABLE sales SPLIT PARTITION sales_all
VALUES ('KR', 'JP', 'CN')
INTO (
PARTITION sales_asia,
PARTITION sales_rest
);

MERGE PARTITIONS

-- 기본 문법
ALTER TABLE sales MERGE PARTITIONS
sales_q1_2026, sales_q2_2026, sales_q3_2026, sales_q4_2026
INTO PARTITION sales_2026;

-- RANGE 전용 TO 단축 문법
ALTER TABLE sales MERGE PARTITIONS
sales_q1_2026 TO sales_q4_2026
INTO PARTITION sales_2026;

나란히 비교

문법 차이

항목PostgreSQL 19Oracle
MERGE 소스 지정MERGE PARTITIONS (p1, p2) 괄호MERGE PARTITIONS p1, p2 나열
MERGE 대상 지정INTO p_newINTO PARTITION p_new
SPLIT 경계 지정FOR VALUES IN (...) / FOR VALUES FROM ... TO ...AT (value) (RANGE) / VALUES (...) (LIST)
RANGE 범위 병합 단축없음 (일일이 나열)p1 TO p4 단축 문법
TABLESPACE 지정각 대상 파티션마다 개별동일

기능/제약 차이

항목PostgreSQL 19Oracle
지원 파티션 타입RANGE, LISTRANGE, LIST, SYSTEM
HASH 파티션지원 안 함지원 안 함
RANGE 인접 조건필요필요
LIST 인접 조건불필요불필요
락 수준ACCESS EXCLUSIVE (전 구간)EXCLUSIVE + ONLINE 옵션 (EE 12.2+)
실행 방식단일 프로세스병렬 실행 가능

실전에서 갈리는 락과 온라인 실행

문법은 거의 맞춰졌습니다. 하지만 운영에서 진짜 갈리는 것은 락과 온라인 실행 여부입니다.

PostgreSQL 19

공식 커밋 메시지와 depesz의 벤치(1천만 행 LIST 파티션 기준)에서 확인된 내용은 다음과 같습니다.

  • 작업 전체 구간에서 ACCESS EXCLUSIVE LOCK 유지
  • 단일 프로세스에서 순차 실행
  • 대규모 파티션에서는 실질적 다운타임이 발생할 수 있음

쓸모 있는 평가는 이렇습니다. "편의성은 크게 좋아졌지만, 온라인성은 Oracle을 따라잡지 못했습니다." 한 줄 SQL로 간단히 표현할 수 있게 된 것만으로도 의미가 크지만, 수백 GB 파티션을 무중단으로 합칠 수 있는 수준은 아닙니다.

Oracle

Oracle Enterprise Edition 12.2부터 ONLINE 키워드로 DML 블로킹 없이 파티션을 합치거나 쪼갤 수 있습니다.

ALTER TABLE sales MERGE PARTITIONS
sales_q1_2026, sales_q2_2026
INTO PARTITION sales_h1_2026
ONLINE;

내부적으로는 shadow 구조를 만들고 점진적으로 데이터를 이관하는 방식이라, 배타 락 보유 시간이 거의 없습니다. Enterprise Edition 라이선스가 전제라는 게 큰 단서지만, 대형 운영 환경에서 체감되는 차이는 큽니다.

어디까지 따라왔고, 어디까지 아직인가

따라잡은 것

  • SPLIT/MERGE DDL 자체의 존재: SQL 한 줄로 끝납니다
  • 지원 파티션 타입 (RANGE, LIST)
  • 인접 조건 정책 (RANGE는 인접 필요, LIST는 무관)
  • 다중 분할(한 파티션 → N개)과 다중 병합(N개 → 하나)

아직 못 따라온 것

  • 온라인(ONLINE) 실행: 대형 파티션은 실질적 다운타임이 남음
  • 병렬 실행: 단일 프로세스로만 처리
  • RANGE 범위 병합 단축 문법(p1 TO p4) 없음

PostgreSQL의 역사적 약점 중 하나가 "큰 파티션 테이블의 유지보수가 DDL 한 줄로 안 끝난다"였습니다. PostgreSQL 19는 그 거리를 눈에 띄게 좁혔습니다. 다만 운영에서 가장 아픈 "락을 오래 붙잡는다"는 문제는 남아 있어서, 대형 테이블은 여전히 DETACH CONCURRENTLY + 수동 작업 전략을 병용해야 할 수 있습니다.

Oracle에서 PostgreSQL로 넘어올 때 "제가 쓰던 그 문법, PostgreSQL에도 있어요?"라는 질문의 목록이 하나씩 채워지고 있어요. 이번은 파티션 병합/분할 차례였어요.


참고

PG19 EXPLAIN RDTSC

· 약 5분

EXPLAIN ANALYZE의 불편한 진실

EXPLAIN ANALYZE는 PostgreSQL에서 쿼리 성능을 분석할 때 가장 먼저 쓰는 도구예요. 실제로 쿼리를 실행하면서 각 노드의 실행 시간, 행 수, 루프 횟수를 보여줘요.

그런데 한 가지 문제가 있습니다. EXPLAIN ANALYZE를 붙이는 것 자체가 쿼리를 느리게 만듭니다.

이건 버그가 아니라 구조적인 문제입니다. 공식 문서에서도 이렇게 경고합니다.

"The measurement overhead added by EXPLAIN ANALYZE can be significant, especially on machines with slow gettimeofday() operating-system calls."

왜 느려지나

EXPLAIN ANALYZE는 실행 계획의 각 노드를 통과할 때마다 시간을 측정합니다. 구체적으로는 InstrStartNode(시작)과 InstrStopNode(종료)에서 시스템 시계를 읽습니다.

핵심은 이 시계를 읽는 함수가 clock_gettime()이라는 것입니다.

쿼리 실행 흐름:

SeqScan (100만 행)
├─ 행 1: clock_gettime() → 처리 → clock_gettime()
├─ 행 2: clock_gettime() → 처리 → clock_gettime()
├─ 행 3: clock_gettime() → 처리 → clock_gettime()
│ ...
└─ 행 1,000,000: clock_gettime() → 처리 → clock_gettime()

→ clock_gettime() 호출 횟수: 200만 번

100만 행을 스캔하면 clock_gettime()200만 번 호출됩니다. 중첩된 노드가 있으면 더 늘어납니다. 각 호출이 약 20ns라고 해도, 200만 번이면 40ms의 순수 오버헤드가 발생합니다.

프로파일링을 해보면 InstrStartNode/InstrStopNode의 실행 시간 대부분이 clock_gettime()에서 소비됩니다. 실제 쿼리 로직이 아니라 시간 측정에 시간을 쓰고 있는 것입니다.

clock_gettime()이 뭔데

clock_gettime()은 Linux에서 고해상도 시간을 가져오는 시스템 콜입니다. 정확하고 안정적이지만, 호출할 때마다 비용이 듭니다.

현대 Linux에서는 VDSO (Virtual Dynamic Shared Object)를 통해 커널 진입 없이 호출할 수 있도록 최적화되어 있습니다. 그래도 호출당 약 18~20ns의 오버헤드가 있습니다. 플랫폼에 따라 20~100ns까지 달라질 수 있습니다.

clock_gettime() 호출 경로:

사용자 공간 → VDSO → TSC 레지스터 읽기 → 보정 → 반환

커널 진입 없이 실행되지만
보정 로직 때문에 여전히 비용 발생

매번 이 경로를 거치는 게 문제입니다. 쿼리 노드 처리 시간이 수십 ns 수준이면, 시간 측정 비용이 실제 처리 비용보다 클 수도 있습니다.

TIMING FALSE라는 우회로

사실 PostgreSQL 9.4부터 이 문제의 우회 방법은 있었습니다.

EXPLAIN (ANALYZE, TIMING FALSE) SELECT * FROM large_table;

TIMING FALSE를 쓰면 시간 측정을 건너뜁니다. 실제 행 수와 루프 횟수만 보여줍니다. 오버헤드가 거의 사라집니다.

하지만 타이밍이 없으면 어느 노드가 병목인지 알 수 없습니다. 그래서 실무에서는 결국 TIMING TRUE(기본값)를 쓰게 됩니다.

pg_test_timing 유틸리티로 현재 시스템의 타이밍 오버헤드를 확인할 수 있습니다.

$ pg_test_timing
Testing timing overhead for 3 seconds.
Per loop time including overhead: 18.80 ns

PostgreSQL 19의 해결책: RDTSC

PostgreSQL 19에서는 x86-64 CPU의 RDTSC (Read Time-Stamp Counter) 명령어를 활용합니다.

RDTSC란

CPU에는 TSC(Time Stamp Counter)라는 카운터가 있습니다. CPU 클럭마다 1씩 증가하는 레지스터입니다. RDTSC 명령어는 이 카운터 값을 직접 읽습니다.

clock_gettime(): 사용자 공간 → VDSO → 보정 로직 → 반환
RDTSC: CPU 레지스터 직접 읽기 → 반환

시스템 콜도, VDSO 경유도, 보정 로직도 없습니다. CPU 명령어 하나로 끝납니다.

성능 비교

pg_test_timing으로 측정한 결과:

클럭 소스평균 루프 시간비교
System clock (기존)18.80 ns기준
RDTSC (PG19)11.69 ns38% 감소
RDTSCP16.94 ns10% 감소

200만 번 호출 기준으로 환산하면:

클럭 소스총 오버헤드
System clock~37.6 ms
RDTSC~23.4 ms
절감량~14.2 ms

단일 쿼리에서 14ms라면 크지 않아 보이지만, 중첩 노드가 있거나 행 수가 더 많으면 오버헤드는 기하급수적으로 늘어납니다.

RDTSC vs RDTSCP

두 명령어의 차이를 알아두면 좋습니다.

RDTSCRDTSCP
속도더 빠름 (11.69 ns)약간 느림 (16.94 ns)
순서 보장비순차 실행 가능순서 보장됨
정밀도약간 낮음높음
용도EXPLAIN ANALYZE (상대적 시간 측정)절대적 시간 측정이 필요한 경우

RDTSC는 CPU의 비순차 실행(out-of-order execution)으로 인해 측정 순서가 살짝 뒤바뀔 수 있습니다. 하지만 EXPLAIN ANALYZE에서는 상대적인 시간 차이만 보면 되므로, 약간의 부정확함은 문제가 되지 않습니다.

PostgreSQL 19는 EXPLAIN ANALYZE에는 빠른 RDTSC를, 높은 정밀도가 필요한 다른 경우에는 RDTSCP를 사용합니다.

timing_clock_source 설정

새로운 timing_clock_source 파라미터로 클럭 소스를 제어할 수 있습니다.

-- 현재 설정 확인
SHOW timing_clock_source;

-- 변경 (postgresql.conf 또는 SET)
SET timing_clock_source = 'rdtsc'; -- 빠름, 약간 낮은 정밀도
SET timing_clock_source = 'rdtscp'; -- 높은 정밀도
SET timing_clock_source = 'system'; -- 기존 clock_gettime()

x86-64 CPU에서 해당 명령어를 지원하면 자동으로 RDTSC를 사용합니다. ARM 등 다른 아키텍처에서는 기존 방식이 유지됩니다.

주의사항

x86-64 전용

이번 최적화는 x86-64 아키텍처 전용입니다. ARM 기반 서버(AWS Graviton 등)에서는 적용되지 않습니다. 향후 ARM용 최적화도 추가될 수 있지만, 초기 릴리즈에는 포함되지 않았습니다.

TSC 신뢰성

모든 x86-64 CPU에서 TSC가 동일하게 동작하지는 않습니다. 오래된 CPU나 가상화 환경에서는 TSC가 불안정할 수 있습니다. PostgreSQL은 TSC 지원 여부를 확인한 후 자동으로 적절한 클럭 소스를 선택합니다.

변화의 체감

일상적인 EXPLAIN ANALYZE 사용에서 이 변화를 극적으로 체감하기는 어려울 수 있습니다. 하지만 대량 행을 처리하는 복잡한 쿼리에서는 측정 오버헤드 감소가 실행 시간 측정의 정확도를 높여준다는 점이 핵심입니다.

역사: 6년간의 논의

이 아이디어가 처음 나온 건 2020년입니다. Andres Freund가 PostgreSQL Hackers 메일링 리스트에 "Reduce timing overhead of EXPLAIN ANALYZE using rdtsc?" 라는 제목으로 제안했습니다.

6년 동안 논의와 구현이 이어진 끝에 PostgreSQL 19에 드디어 포함되었습니다. 아이디어 자체는 단순하지만, TSC 안정성 검증, 다양한 CPU/VM 환경 호환성, 정밀도 트레이드오프 같은 세부 사항을 해결하는 데 시간이 걸렸습니다.

정리

기존 (PG18 이하)PG19
클럭 소스clock_gettime()RDTSC
호출당 비용~18.80 ns~11.69 ns
개선율-38% 감소
대상 아키텍처모든 플랫폼x86-64 (자동 감지)
설정없음timing_clock_source

EXPLAIN ANALYZE를 프로덕션에서 쓰는 건 여전히 주의가 필요해요. 하지만 PostgreSQL 19부터는 측정 때문에 발생하는 노이즈가 줄어들어, 더 정확한 성능 분석이 가능해져요.

참고 자료

PostgreSQL 19 새 기능 총정리

· 약 6분

PostgreSQL 19 개요

PostgreSQL 19는 2026년 9월 출시 예정이에요. 기능 동결(Feature Freeze)은 4월 8일에 완료되었고, 5월에 첫 번째 베타가 나올 예정이에요.

PostgreSQL 18이 비동기 I/O, UUIDv7, 가상 생성 컬럼 같은 굵직한 기능을 가져왔다면, PostgreSQL 19는 기존 기능의 실질적 개선과 성능 최적화에 집중하는 릴리즈입니다. 화려하진 않지만 실무에서 체감할 수 있는 변화가 많습니다.

이 글은 2026년 4월 기준으로 작성되었으며, 정식 출시 전까지 일부 내용이 변경될 수 있습니다.

쿼리 플래너 & 실행

GROUP BY ALL

SELECT department, role, COUNT(*)
FROM employees
GROUP BY ALL;

GROUP BY ALL을 쓰면 집계 함수가 아닌 SELECT 표현식을 자동으로 GROUP BY에 포함합니다. 컬럼 5개를 일일이 나열할 필요가 없어졌습니다.

Eager Aggregation

대용량 테이블을 조인하기 전에 먼저 집계하는 최적화입니다. enable_eager_aggregate 파라미터로 제어합니다.

-- 플래너가 자동으로 orders를 먼저 집계한 후 customers와 조인
SELECT c.name, SUM(o.amount)
FROM customers c JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;

조인 대상 행 수가 줄어들기 때문에 대용량 분석 쿼리에서 성능이 크게 개선될 수 있습니다.

COUNT() 자동 최적화

COUNT(1), COUNT(not_null_col)을 자동으로 COUNT(*)로 변환합니다. 습관적으로 COUNT(1)을 쓰던 사람들에게는 반가운 변화입니다.

병렬 TID Range Scan

TID Range Scan이 병렬 실행을 지원합니다. 대규모 테이블에서 특정 범위의 물리적 행을 빠르게 스캔할 수 있습니다.

Incremental Sort 확장

Append/MergeAppend 노드 내에서도 Incremental Sort가 가능해졌습니다. 파티션 테이블의 정렬 성능이 개선됩니다.

EXPLAIN ANALYZE 타이밍 오버헤드 감소

x86-64 CPU에서 RDTSC 명령어를 활용해 시간 측정 오버헤드를 약 38% 줄였습니다. 이 주제는 별도 포스트에서 상세히 다룹니다.

윈도우 함수 개선

IGNORE NULLS / RESPECT NULLS

lag, lead, first_value, last_value, nth_value에서 NULL 처리 방식을 지정할 수 있습니다.

SELECT
ts,
value,
last_value(value) IGNORE NULLS OVER (ORDER BY ts) AS last_non_null
FROM sensor_data;

시계열 데이터에서 NULL 값을 건너뛰고 마지막 유효 값을 가져오는 패턴이 훨씬 간결해졌습니다. 이전에는 서브쿼리나 래터럴 조인이 필요했습니다.

파티셔닝

MERGE / SPLIT PARTITIONS

-- 파티션 병합
ALTER TABLE bookings MERGE PARTITIONS (p_2024_q1, p_2024_q2) INTO p_2024_h1;

-- 파티션 분할
ALTER TABLE bookings SPLIT PARTITION p_2024 INTO
(PARTITION p_2024_h1 FOR VALUES FROM ('2024-01-01') TO ('2024-07-01'),
PARTITION p_2024_h2 FOR VALUES FROM ('2024-07-01') TO ('2025-01-01'));

파티션을 병합하거나 분할하는 DDL이 추가되었습니다. 기존에는 새 파티션을 만들고 데이터를 이동한 뒤 기존 파티션을 삭제해야 했습니다.

COPY TO with Partitioned Tables

파티션 테이블에서 직접 COPY TO가 가능해졌습니다. 이전에는 각 파티션을 개별적으로 복사해야 했습니다.

논리 복제 (Logical Replication)

WAL 레벨 동적 조정

서버 재시작 없이 WAL 레벨을 변경할 수 있습니다. effective_wal_level 읽기 전용 파라미터로 현재 상태를 확인합니다.

이전에는 wal_level = logical 설정 변경 후 반드시 재시작이 필요했습니다. 운영 중인 시스템에서 논리 복제를 활성화할 때 다운타임을 줄일 수 있습니다.

시퀀스 복제

논리 복제에서 시퀀스 값을 동기화할 수 있습니다.

ALTER SUBSCRIPTION my_sub REFRESH SEQUENCES;

WAIT FOR LSN

레플리카가 특정 LSN까지 동기화될 때까지 대기하는 메커니즘입니다. 읽기 일관성이 필요한 경우에 유용합니다.

동기화 모니터링

pg_replication_slotsslotsync_skip_reason 컬럼이 추가되어 슬롯 동기화 지연 원인을 파악할 수 있습니다.

성능 최적화

jsonb_agg 성능 향상

jsonb_agg 함수의 성능이 50% 이상 개선되었습니다. JSON 집계가 많은 API 백엔드에는 반가운 변화입니다.

LISTEN/NOTIFY 최적화

채널별 해시 테이블 기반으로 처리 방식이 변경되어, 많은 채널을 사용하는 환경에서 성능이 개선됩니다.

ICU 문자 변환 최적화

UTF-8 데이터베이스에서 ICU 문자 변환 함수의 성능이 향상되었습니다.

Buffer Cache 알고리즘 변경

Free buffer list를 Clock-sweep 알고리즘으로 교체했습니다.

Temporary Table 트렁케이션 고속화

임시 테이블 truncation 처리가 빨라졌습니다.

모니터링 & 관찰성

pg_stat_statements 확장

  • Generic/Custom 플랜별 호출 횟수 추적
  • FETCH 명령어 정규화
  • IN 절 파라미터 리스트 정규화

VACUUM / ANALYZE 진행 상황

  • mode, started_by 컬럼 추가로 수동/자동 실행 구분 가능
  • VACUUM VERBOSE에서 메모리 사용량 표시
  • vacuumdb --dry-run 옵션 추가
  • log_autoanalyze_min_duration 신규 파라미터로 ANALYZE 로깅 분리

pg_buffercache 확장

pg_buffercache_os_pages로 OS 페이지 분포를 확인할 수 있습니다.

WAL 모니터링

wal_fpi_bytes로 Full Page Image 바이트를 추적합니다.

멀티트랜잭션 통계

pg_get_multixact_stats() 함수로 멀티트랜잭션 사용 현황을 확인할 수 있습니다.

SQL & 함수

random() 날짜/시간 생성

SELECT random('2024-01-01'::date, '2024-12-31'::date);

지정 범위 내 랜덤 날짜/시간을 생성할 수 있습니다. 테스트 데이터 생성이 간편해졌습니다.

base64url 인코딩

encode/decode 함수에서 URL-safe한 base64url 포맷을 지원합니다.

error_on_null()

NULL 값 검증 함수가 추가되었습니다. NULL이 들어오면 에러를 발생시킵니다.

SRF(Set-Returning Function) 인라인

Set-returning 함수의 인라인 최적화가 지원됩니다.

관리 & 도구

regdatabase 타입

데이터베이스 이름과 OID를 상호 변환하는 새 객체 식별자 타입입니다.

CHECKPOINT 개선

FLUSH_UNLOGGED, MODE 파라미터가 추가되었습니다.

pg_upgrade 대용량 객체 최적화

대용량 객체(Large Object) 마이그레이션이 빨라졌습니다.

psql 개선

  • %S 프롬프트 옵션으로 검색 경로 표시
  • boolean 값 표시 커스터마이징 (\pset display_true)

pgbench

--continue-on-error 옵션으로 에러 발생 시에도 벤치마크를 계속 진행할 수 있습니다.

그래프 쿼리 (SQL/PGQ)

PostgreSQL 19는 SQL:2023 Part 16(SQL/PGQ) 을 코어에 들였습니다. 관계형 테이블 위에 프로퍼티 그래프 뷰를 얹고, GRAPH_TABLEMATCH 구문으로 그래프 패턴 매칭을 표준 SQL 안에서 풉니다. 별도 그래프 DB나 확장 없이 PostgreSQL만으로 일부 그래프 워크로드를 받아낼 수 있게 되었습니다.

첫 구현은 고정 깊이 패턴까지입니다. 가변 길이 경로는 다음 릴리스의 1차 후보입니다. 이 주제는 분량이 커서 별도 포스트에서 상세히 다룹니다.

호환성 주의

standard_conforming_strings 비활성화 불가

이제 standard_conforming_stringsoff로 설정할 수 없습니다. 비표준 문자열 리터럴(\' 등)은 에러를 발생시킵니다. escape_string_warning 파라미터도 제거되었습니다.

기존에 E'...' 없이 역슬래시 이스케이프를 쓰던 레거시 코드가 있다면 확인이 필요합니다.

log_lock_waits 기본값 변경

log_lock_waits가 기본적으로 활성화됩니다. 잠금 대기 로그가 자동으로 남습니다.

마무리

PostgreSQL 19는 눈에 띄는 신기능보다는 기존 기능의 완성도를 높이는 릴리즈입니다.

제가 개인적으로 주목하는 기능은 매일 쓰게 될 편의 기능인 GROUP BY ALL, 분석 쿼리 성능에 직접 영향을 주는 Eager Aggregation, 운영 중 논리 복제 활성화를 편하게 해주는 WAL 레벨 동적 조정이에요. IGNORE NULLS는 시계열 처리를 깔끔하게 해주고, 표준 그래프 쿼리를 코어에 들인 SQL/PGQ도 눈에 들어와요. EXPLAIN ANALYZE RDTSC다음 글에서, SQL/PGQ후속편에서 상세히 다룹니다.

2026년 9월 정식 출시가 기대되고, 5월 베타부터 미리 테스트해볼 수 있어요.

참고 자료