파일 포맷과 성능

파일 포맷과 성능 (File Formats - file-formats)

Parquet 파일을 직접 쿼리할지, 아니면 먼저 DuckDB에 로드할지 선택할 때 성능을 고민하게 돼요. CSV는 압축 상태로 바로 읽는 게 빠른 경우도 있고요. 이 페이지에서는 Parquet와 CSV라는 두 주요 파일 포맷이 성능에 미치는 영향을 구체적인 벤치마크와 함께 설명해 드릴게요.

출처: DuckDB 공식 문서 — file-formats

Parquet 파일 다루기 (Handling Parquet Files)

DuckDB는 Parquet 파일에 대한 고급 지원을 해요. 여기에는 Parquet 파일 직접 쿼리가 포함돼요. 이 파일들을 직접 쿼리할지 아니면 먼저 데이터베이스로 로드할지 결정할 때는 여러 요소를 고려해야 해요.

Parquet 파일을 직접 쿼리하는 이유 (Reasons for Querying Parquet Files)

기본 통계의 가용성. Parquet 파일은 컬럼형(columnar) 저장 포맷이고 zonemap 같은 기본 통계를 포함해요. 이런 기능 덕분에 DuckDB는 Parquet 파일에 대해 프로젝션과 필터 푸시다운 같은 최적화를 활용할 수 있어요. 따라서 프로젝션·필터·집계를 조합한 워크로드는 Parquet 파일에서 꽤 좋은 성능을 내요.

저장 공간 고려. Parquet 파일의 데이터를 로드하면 DuckDB 데이터베이스 파일에 대략 같은 정도의 공간이 필요해요. 따라서 사용 가능한 디스크 공간이 제한돼 있다면 Parquet 파일에 직접 쿼리를 실행하는 게 유리해요.

Parquet 파일을 직접 쿼리하지 말아야 하는 이유 (Reasons against Querying Parquet Files)

고급 통계의 부족. DuckDB 데이터베이스 포맷에는 Parquet 파일에는 없는 hyperloglog 통계가 있어요. 이 통계는 카디널리티 추정의 정확도를 높여주며, 특히 쿼리에 조인 연산자가 많이 포함된 경우 중요해요.

Tip. DuckDB가 Parquet 파일에서 차선의 조인 순서를 만들어내는 걸 발견했다면, Parquet 파일을 DuckDB 테이블로 로드해 보세요. 개선된 통계 덕분에 더 나은 조인 순서를 얻을 가능성이 높아요.

반복 쿼리. 같은 데이터셋에 여러 쿼리를 실행할 계획이라면 데이터를 DuckDB에 로드하는 게 좋아요. 쿼리는 항상 다소 더 빨라지고, 시간이 지나면서 초기 로드 시간을 상쇄해요.

높은 압축 해제 시간. 일부 Parquet 파일은 gzip 같은 무거운 압축 알고리즘으로 압축돼 있어요. 이런 경우 파일에 접근할 때마다 비싼 압축 해제가 필요해져요. 반면 Snappy, LZ4, zstd 같은 가벼운 압축 방법은 해제가 더 빨라요. parquet_metadata 함수로 사용된 압축 알고리즘을 알아낼 수 있어요.

마이크로벤치마크: DuckDB vs. Parquet에서 TPC-H 실행 (Microbenchmark)

TPC-H 벤치마크 쿼리는 Parquet 파일에서 실행하면 DuckDB 데이터베이스보다 약 1.1-5.0배 느려요.

Best practice 저장 공간이 충분하고 조인 중심의 워크로드이거나 같은 데이터셋에 많은 쿼리를 실행할 계획이라면, Parquet 파일을 먼저 데이터베이스로 로드하세요. Parquet 파일의 압축 알고리즘과 row group 크기가 성능에 큰 영향을 미쳐요. parquet_metadata 함수로 이것들을 연구해 보세요.

Row Group 크기의 영향 (The Effect of Row Group Sizes)

DuckDB는 각각 100K~1M 행의 row group을 가진 Parquet 파일에서 가장 잘 작동해요. 이유는 DuckDB가 row group 단위로만 병렬화할 수 있기 때문이에요. 그래서 Parquet 파일에 하나의 거대한 row group만 있으면 단일 스레드로만 처리될 수 있죠. parquet_metadata 함수로 Parquet 파일에 row group이 몇 개인지 알 수 있어요. Parquet 파일을 쓸 때는 row_group_size 옵션을 사용하세요.

마이크로벤치마크: 서로 다른 Row Group 크기에서 집계 쿼리 실행

960에서 1,966,080 사이의 서로 다른 row group 크기로 Parquet 파일에 대해 간단한 집계 쿼리를 실행했어요. 결과는 다음과 같아요.

Row group size Execution time
960 8.77 s
1920 8.95 s
3840 4.33 s
7680 2.35 s
15360 1.58 s
30720 1.17 s
61440 0.94 s
122880 0.87 s
245760 0.93 s
491520 0.95 s
983040 0.97 s
1966080 0.88 s

결과를 보면 5,000 미만의 row group 크기는 매우 해롭고, 이상적인 크기의 row group보다 런타임이 5-10배 이상 커져요. 5,000~20,000 사이의 row group 크기는 여전히 최적 성능보다 1.5-2.5배 느려요. 100,000 이상의 row group 크기부터는 차이가 작아져서, 최고와 최저 런타임 사이 간격이 약 10% 정도예요.

Parquet 파일 크기 (Parquet File Sizes)

DuckDB는 여러 Parquet 파일에 걸쳐서도 병렬화할 수 있어요. 모든 파일의 row group 총 개수가 CPU 스레드 수만큼은 되는 것이 좋아요. 예를 들어 10개 스레드 머신이라면, 1개 row group씩 가진 파일 10개 또는 10개 row group을 가진 파일 1개 모두 완전한 병렬 처리를 달성해요. 또한 개별 Parquet 파일의 크기는 적당히 유지하는 게 좋아요.

Best practice 개별 Parquet 파일당 이상적인 범위는 100 MB에서 10 GB 사이예요.

필터 푸시다운을 위한 Hive 파티셔닝 (Hive Partitioning for Filter Pushdown)

필터 조건이 있는 많은 파일을 쿼리할 때, 필터 조건에 쓰이는 컬럼을 따라 데이터를 파티셔닝하는 Hive 포맷 폴더 구조를 사용하면 성능을 개선할 수 있어요. DuckDB는 필터 기준을 충족하는 폴더와 파일만 읽으면 되기 때문이에요. 특히 원격 파일을 쿼리할 때 유용해요.

Parquet 읽기·쓰기에 대한 추가 팁

Parquet 파일 읽기·쓰기에 대한 팁은 Parquet Tips 페이지를 참고해요.

CSV 파일 로드 (Loading CSV Files)

CSV 파일은 종종 GZIP 아카이브(.csv.gz) 같은 압축 포맷으로 배포돼요. DuckDB는 이 파일들을 그 자리에서 압축 해제할 수 있어요. 사실 IO가 줄어들기 때문에 파일을 먼저 해제하고 로드하는 것보다 보통 더 빠르답니다.

스키마 로드 시간
GZIP 압축 CSV 파일(.csv.gz)에서 로드 107.1 s
(병렬 gunzip으로) 해제 후 비압축 CSV 파일에서 로드 121.3 s

많은 작은 CSV 파일 로드 (Loading Many Small CSV Files)

CSV 리더는 모든 파일에 CSV 스니퍼를 실행해요. 파일이 많고 작으면 불필요하게 높은 오버헤드가 발생할 수 있어요. 이를 줄이려면 스니퍼를 끄는 최적화가 가능해요. 모든 파일이 같은 CSV 다이얼렉트와 컬럼 이름/타입을 가진다고 가정하고, 스니퍼 옵션을 다음과 같이 얻어요.

.mode line
SELECT Prompt FROM sniff_csv('part-0001.csv');
Prompt = FROM read_csv('file_path.csv', auto_detect=false, delim=',', quote='"', escape='"', new_line='\n', skip=0, header=true, columns={'hello': 'BIGINT', 'world': 'VARCHAR'});

그다음 read_csv 명령에 파일명 확장(글로빙)을 적용하는 등 조정하면서, 스니퍼가 감지한 나머지 옵션과 함께 실행하면 돼요.

FROM read_csv('part-*.csv', auto_detect=false, delim=',', quote='"', escape='"', new_line='\n', skip=0, header=true, columns={'hello': 'BIGINT', 'world': 'VARCHAR'});

더 알아보기 (Learn more)