성능 프로파일링
EXPLAIN, EXPLAIN ANALYZE, profiling output으로 DuckDB 쿼리 병목을 읽는 기준을 정리합니다.
핵심 요약
- 성능 튜닝은 memory_limit부터 만지기 전에 plan을 읽는 일입니다. EXPLAIN(미실행 물리 계획), EXPLAIN ANALYZE(실행 operator runtime), profiling setting, duckdb_settings()를 씁니다.
- EXPLAIN ANALYZE는 multi-thread에서 operator 시간 합이 전체 시간보다 클 수 있다는 점을 감안해 읽습니다.
- scan 이후 filter는 pushdown 실패, nested loop join·cardinality 폭발은 join key/통계 문제, 과도한 file scan은 partition filter 부재 신호입니다.
- 느린 쿼리는 EXPLAIN → EXPLAIN ANALYZE → 병목 구분(file layout / join·statistics / memory·blocking) → rewrite·materialize 루프로 다룹니다.
- 벤치마크는 쿼리 시간만이 아니라 DuckDB·extension version, input files, threads/memory/temp, cold/warm cache를 함께 기록합니다.
2.0 성능·설정 변경
2.0의 asynchronous I/O는 원격 파일 읽기와 쿼리 처리의 병렬성을 분리합니다. 이전 버전의 threads 튜닝 결과를 그대로 적용하지 말고 같은 파일 manifest·필터·cold/warm 조건에서 다시 측정합니다. 특정 블로그의 속도 배수를 팀 SLA로 가져오지 않습니다.
custom_profiling_settings는 제거됐고 tracked_metrics로 옮깁니다. profiling_mode는 deprecated이며 값을 바꿔도 효과가 없습니다. HTTP 로그는 CALL enable_logging('HTTP'), CLI에서 조회 가능한 메모리 로그는 CALL enable_logging(storage = 'memory') 등 목적별로 설정합니다.
DuckDB 성능 튜닝은 추측 대신 plan을 읽는 일입니다. 파일 scan, filter pushdown, join order, blocking operator, thread 사용을 확인하지 않고
memory_limit만 바꾸면 문제를 뒤늦게 발견하게 됩니다.
기본 도구
| 도구 | 실행 여부 | 용도 |
|---|---|---|
EXPLAIN | 실행하지 않음 | 물리 계획 확인 |
EXPLAIN ANALYZE | 실행함 | operator별 runtime 확인 |
| profiling pragma/settings | 실행함 | JSON/파일 기반 profile 저장 |
duckdb_settings() | 실행함 | 세션 설정 확인 |
EXPLAIN
SELECT customer_id, sum(amount) AS revenue
FROM read_parquet('curated/orders/*.parquet')
WHERE order_date >= DATE '2026-01-01'
GROUP BY customer_id;EXPLAIN ANALYZE
SELECT customer_id, sum(amount) AS revenue
FROM read_parquet('curated/orders/*.parquet')
WHERE order_date >= DATE '2026-01-01'
GROUP BY customer_id;EXPLAIN ANALYZE는 실제 실행하며 operator별 wall-clock time을 보여줍니다. multi-thread 실행에서는 operator 시간 합이 전체 시간보다 클 수 있습니다.
Plan에서 보는 것
| 신호 | 해석 | 대응 |
|---|---|---|
| filter가 scan 이후에 적용 | pushdown 실패 가능 | expression 단순화, type cast 위치 조정 |
| nested loop join | 작은 side가 맞는지 확인 | join condition, 통계, materialization 점검 |
| cardinality 폭발 | join key 중복 또는 조건 누락 | key uniqueness 검사 |
| file scan이 너무 많음 | partition filter 부재 | hive partition, manifest, path 제한 |
| sort/window 시간이 큼 | blocking operator | pre-aggregate, partition 축소 |
Profile 루프
Benchmark 습관
SET threads = 4;
SET memory_limit = '8GB';
SET temp_directory = '/fast-temp/duckdb.tmp';
SELECT current_setting('threads'), current_setting('memory_limit');쿼리 시간만 기록하지 말고 다음을 함께 기록합니다.
| 항목 | 이유 |
|---|---|
| DuckDB version | optimizer/reader 변경 영향 |
| extension version | httpfs/lakehouse 동작 차이 |
| input files | glob 결과 변화 방지 |
| threads/memory/temp | 환경 차이 분리 |
| cold/warm cache | OS cache 영향 분리 |
비동기 I/O의 적용 범위와 관측
2.0 비동기 읽기는 Parquet, 압축되지 않은 seekable UTF-8 CSV, native DuckDB 저장 포맷에 자동 적용됩니다. JSON 등은 동기 경로이므로 모든 파일이 같은 개선을 얻는다고 보지 않습니다.
SELECT name, value FROM duckdb_settings()
WHERE name IN ('threads', 'async_threads', 'read_ahead_depth', 'memory_limit')
ORDER BY name;threads는 쿼리 처리, async_threads는 별도 I/O pool입니다. 여러 인스턴스를 함께 운영하면 두 pool을 합산해 자원 사용을 봅니다. read_ahead_depth=-1은 메모리 예산에 따라 제어하고, 양수는 앞서 읽는 job 수를 제한하지만 같은 메모리 예산 방식이 아닙니다. 0은 선행 읽기를 끕니다. 기본값부터 측정하고 CPU·네트워크·peak RSS·spill을 함께 비교합니다. I/O 문서.
2.0 EXPLAIN (SQL)
EXPLAIN (SQL) SELECT 1 + 2 AS answer;최적화된 논리 계획을 실행 가능한 SQL로 확인합니다. 실제 쿼리를 실행하지 않으며, 출력의 별칭·문자열 포맷은 안정적인 API가 아닙니다. golden test에서 SQL 문자열 전체를 고정하기보다 결과 스키마와 의미를 검증합니다. ANALYZE·FORMAT과 함께 사용할 수 없습니다. Profiling Queries.