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


정리

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

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


참고 자료