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 ns | 38% 감소 |
| RDTSCP | 16.94 ns | 10% 감소 |
200만 번 호출 기준으로 환산하면:
| 클럭 소스 | 총 오버헤드 |
|---|---|
| System clock | ~37.6 ms |
| RDTSC | ~23.4 ms |
| 절감량 | ~14.2 ms |
단일 쿼리에서 14ms라면 크지 않아 보이지만, 중첩 노드가 있거나 행 수가 더 많으면 오버헤드는 기하급수적으로 늘어납니다.
RDTSC vs RDTSCP
두 명령어의 차이를 알아두면 좋습니다.
| RDTSC | RDTSCP | |
|---|---|---|
| 속도 | 더 빠름 (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부터는 측정 때문에 발생하는 노이즈가 줄어들어, 더 정확한 성능 분석이 가능해져요.