Parquet 팁

Parquet 팁 (Parquet Tips)

Parquet 파일을 다룰 때 도움이 되는 팁을 모아봤습니다. 읽기와 쓰기 양쪽에서 성능이나 편의성을 올리는 방법을 하나씩 살펴보겠습니다.

출처: 공식문서

Parquet 파일 읽기 팁

스키마가 서로 다른 파일은 union_by_name 사용하기

union_by_name 옵션은 컬럼이 다르거나 누락된 파일들의 스키마를 하나로 통합합니다. 특정 컬럼이 없는 파일에는 NULL 값이 채워집니다.

SELECT *
FROM read_parquet('flights*.parquet', union_by_name = true);

Parquet 파일 쓰기 팁

읽을 때 glob 패턴을 쓰거나 Hive 파티셔닝 구조를 사용하는 것이 여러 파일을 투명하게 다루는 좋은 방법입니다.

PER_THREAD_OUTPUT 활성화하기

최종 Parquet 파일의 개수가 중요하지 않다면, 스레드당 파일 하나씩 쓰도록 해서 성능을 크게 향상시킬 수 있습니다.

COPY
    (FROM generate_series(10_000_000))
    TO 'test.parquet'
    (FORMAT parquet, PER_THREAD_OUTPUT);

ROW_GROUP_SIZE 선택하기

ROW_GROUP_SIZE 파라미터는 Parquet row group의 최소 행 수를 지정합니다. 최솟값은 DuckDB의 벡터 크기인 2,048이고, 기본값은 122,880입니다. Parquet row group은 데이터셋의 각 컬럼에 대한 column chunk로 구성된 행 분할 단위입니다.

압축 알고리즘은 row group 단위로만 적용되므로, row group 크기가 클수록 데이터를 압축할 기회가 많아집니다. 반면 row group 크기가 크면 결과를 스트리밍할 때 각 스레드가 flush 전까지 더 많은 데이터를 메모리에 보관하게 됩니다. row group 크기를 작게 하면 좋은 또 다른 이유는, DuckDB가 같은 파일 내에서도 Parquet row group을 병렬로 읽을 수 있고, predicate pushdown을 사용해 메타데이터 범위가 쿼리의 WHERE 절과 일치하는 row group만 스캔하기 때문입니다. 다만 각 그룹의 메타데이터를 읽는 데는 약간의 오버헤드가 있습니다.

좋은 경험칙은, 파일당 row group 수가 그 파일을 조회할 때 사용하는 CPU 스레드 수보다 최소한 크게 나오도록 하는 것입니다. 스레드 수보다 많은 row group이 있으면 선택성(highly selective)이 높은 쿼리는 빨라지지만, 집계처럼 파일 전체를 스캔해야 하는 쿼리는 느려집니다.

row group 크기를 다르게 하여 Parquet 파일로 쿼리를 쓰려면 다음을 실행하세요.

COPY
    (FROM generate_series(100_000))
    TO 'row-groups.parquet'
    (FORMAT parquet, ROW_GROUP_SIZE 100_000);

ROW_GROUPS_PER_FILE 옵션

ROW_GROUPS_PER_FILE 파라미터는 현재 파일이 지정된 수만큼의 row group을 가지면 새 Parquet 파일을 만듭니다.

COPY
    (FROM generate_series(100_000))
    TO 'output-directory'
    (FORMAT parquet, ROW_GROUP_SIZE 20_000, ROW_GROUPS_PER_FILE 2);

여러 스레드가 활성화되어 있으면, FILE_SIZE_BYTES와 유사하게, 잠금(locking)을 줄이기 위해 파일 안의 row group 수가 지정 수를 약간 넘길 수 있습니다. 그러나 PER_THREAD_OUTPUT이 설정되어 있으면 각 파일에 스레드 하나만 쓰므로 다시 정확해집니다.

행 정렬로 Row Group Pruning 개선하기

DuckDB는 모든 row group에 대해 min/max 값을 포함한 컬럼별 통계를 기록하고, 앞서 설명한 것처럼 리더는 이 통계를 사용해 쿼리의 WHERE 절과 일치할 수 없는 row group을 건너뜁니다. 자주 필터링하는 컬럼 기준으로 데이터를 정렬해서 쓰면, 그 min/max 범위가 좁아지고 row group 사이에서 겹치지 않게 되어, 해당 컬럼의 선택성이 높은 쿼리는 훨씬 적은 row group만 스캔하게 됩니다.

COPY
    (FROM 'events.parquet' ORDER BY event_time)
    TO 'events-sorted.parquet'
    (FORMAT parquet);

이 방법은 각 row group이 정렬 키의 좁은 범위에 집중되도록 하는 ROW_GROUP_SIZE와 잘 어울립니다.

더 알아보기 (Learn more)