본문으로 건너뛰기
김신건의 로그

Apache Parquet

· 수정 · 📖 약 5분 · 1,981자/단어 #data-engineering #parquet #columnar #file-format #storage
Parquet, 아파치 파케이, 파케이, Parquet 포맷, columnar file format

정의

Apache Parquet분석 쿼리에 최적화된 오픈소스 컬럼형 이진 파일 포맷. 2013년 Twitter + Cloudera 가 Google Dremel 논문 (2010) 을 참고해 발표. 현재 데이터 웨어하우스 / data lake 의 사실상 표준 저장 포맷.

같은 데이터를 CSV/JSON 대비 10-100배 작게, 분석 쿼리를 10-100배 빠르게 실행.

왜 컬럼형인가

Row-based (CSV, JSON, Avro):
  Row 1: [id=1, name="A", age=30, ..., col50]
  Row 2: [id=2, name="B", age=25, ..., col50]

  SELECT AVG(age) → 모든 컬럼을 읽고 age 만 뽑음.

Column-based (Parquet, ORC):
  id:   [1, 2, 3, ...]
  name: [A, B, C, ...]
  age:  [30, 25, 40, ...]  ← 이 컬럼만 스캔
  ...

  SELECT AVG(age) → age 컬럼만 I/O. 나머지 스킵.

컬럼형의 세 가지 이점:

  1. I/O 절감 - 필요한 컬럼만 읽음 (컬럼 프로젝션)
  2. 압축률 - 같은 타입/유사값이 연속 -> 사전, RLE, delta 압축이 잘 먹힘 (5-10배)
  3. CPU 효율 - 같은 타입이 연속 -> SIMD 벡터 명령, late materialization

파일 구조

flowchart TB
    File["Parquet File"]
    Header["Magic 'PAR1'"]
    RG1["Row Group 1<br/>(128 MB - 1 GB)"]
    RG2["Row Group 2"]
    RGN["Row Group N"]
    Footer["Footer<br/>(schema + metadata + statistics)"]
    Trailer["Magic 'PAR1'"]

    File --> Header
    File --> RG1
    File --> RG2
    File --> RGN
    File --> Footer
    File --> Trailer

    RG1 --> CC1["Column Chunk 1<br/>(컬럼 A 데이터)"]
    RG1 --> CC2["Column Chunk 2<br/>(컬럼 B 데이터)"]
    RG1 --> CCN["Column Chunk N"]

    CC1 --> Page1["Data Page 1<br/>(1-10 MB)"]
    CC1 --> Page2["Data Page 2"]
    CC1 --> Dict["Dictionary Page (optional)"]
계층크기 (권장)역할
File임의하나의 물리 파일 (.parquet)
Row Group128 MB - 1 GB수평 파티션, 병렬 read/write 단위
Column ChunkRow group / N한 컬럼의 데이터가 연속으로
Data Page1 - 10 MB압축/인코딩 단위
Dictionary PageColumn chunk 당 1개 (선택)값 -> 정수 매핑
Footer파일 끝schema + row group 위치 + 통계

Footer 가 파일 끝에 있는 이유: streaming write 를 지원하려면 파일을 다 쓴 뒤에야 전체 통계/위치를 알 수 있음. 대신 read 시 seek 두 번 (끝으로 -> footer 읽기 -> 필요한 row group 위치로).

인코딩 (Encoding)

압축 전 값을 정수/오프셋으로 표현하는 단계. 컬럼 특성에 따라 자동 선택.

Encoding적용 타입최적 케이스압축비
PLAIN모든 타입fallback, 무압축1x
RLE_DICTIONARY모든 타입 (일반적)낮은 카디널리티 (성별, 국가)10-100x
DELTA_BINARY_PACKEDINT32/INT64정렬/순차 정수 (id, timestamp)5-20x
DELTA_LENGTH_BYTE_ARRAYBYTE_ARRAY문자열 길이 varying3-10x
DELTA_BYTE_ARRAYBYTE_ARRAY정렬된 문자열5-15x
BYTE_STREAM_SPLITFLOAT/DOUBLE부동소수점 (뒤 codec 과 조합 개선)압축 개선

Dictionary encoding 이 기본. 카디널리티가 임계값을 넘으면 PLAIN 으로 fallback.

압축 (Compression Codec)

인코딩된 페이지를 물리적으로 압축. 인코딩 위에 codec 을 한 번 더 적용.

Codec압축비압축 속도해제 속도권장
SNAPPY2-4x매우 빠름매우 빠름옛 기본값, 균형형
ZSTD3-6x빠름빠름현대 기본값 권장
GZIP4-8x느림느림최대 압축 필요 시
LZ42-3x최고최고실시간 스트리밍
BROTLI5-10x매우 느림중간아카이빙 (드묾)
LZO2-4x빠름빠름Legacy (사실상 대체됨)
UNCOMPRESSED1x--디버깅만

IMPORTANT

ZSTD 가 현대 기본값. SNAPPY 보다 압축비가 좋고 GZIP 보다 빠름. 새 파일은 ZSTD 로 시작.

통계 (Statistics) & Predicate Pushdown

각 row group / column chunk / page 는 min, max, null_count 통계를 저장. Reader 는 이 통계만 보고 데이터를 읽지 않고 스킵 가능.

Query: SELECT * FROM t WHERE order_date >= '2026-01-01'

Row Group 1: min=2025-01-01, max=2025-12-31  →  skip (max < filter)
Row Group 2: min=2026-01-01, max=2026-06-30  →  read
Row Group 3: min=2026-07-01, max=2026-12-31  →  read

Bloom Filter (선택): 컬럼 값의 hash 집합. WHERE user_id = 12345 처럼 등호 조건에서 row group 을 확률적으로 스킵. 크기 ~1-2% 오버헤드.

Page Index (Parquet 2.5+): row group 안의 페이지 수준 min/max 를 별도로 저장. 페이지 단위 스킵.

스키마 진화 (Schema Evolution)

Parquet 은 다음을 지원:

  • 컬럼 추가: 새 컬럼 (기본값 or NULL)
  • 컬럼 제거: reader 무시
  • 컬럼 이름 변경: 메타 수정
  • 타입 승격: INT32 -> INT64, FLOAT -> DOUBLE
  • 중첩 구조 변경: struct 필드 추가

주의: 타입 강등 (INT64 -> INT32) 은 안전하지 않음. 오래된 파일과 새 스키마가 섞이면 reader 마다 처리 다름.

트랜잭션 스키마 진화가 필요하면 Iceberg / Delta Lake / Hudi 같은 table format 층을 위에 얹음.

논리 타입 (Logical Types, Parquet 2.x)

물리 타입 (INT32, INT64, BYTE_ARRAY 등) 위에 의미를 부여:

Logical Type물리 타입의미
STRINGBYTE_ARRAYUTF-8 문자열
DATEINT32epoch 이후 일수
TIMESTAMPINT64밀리/마이크로/나노초
DECIMAL(p,s)INT32/64/FIXED/BYTE_ARRAY고정소수점
UUIDFIXED_LEN_BYTE_ARRAY(16)128비트 UUID
JSONBYTE_ARRAYJSON 문자열
VARIANT(nested)반정형 데이터 (Parquet 2.11+)
GEOMETRY/GEOGRAPHYBYTE_ARRAYWKT/WKB (Parquet 2.11+)

중첩 구조 (Nested Schema)

Dremel 의 핵심 기여: 임의 깊이의 중첩 구조를 컬럼형으로 저장. Struct, List, Map 을 각 leaf 컬럼별 (definition level, repetition level) 로 인코딩.

schema: {
  user: {
    id: int64,
    tags: list<string>,
    addresses: list<{city: string, zip: string}>
  }
}

물리 저장:
  user.id                    (INT64)
  user.tags.list.element     (STRING) + rep/def levels
  user.addresses.list.city   (STRING) + rep/def levels
  user.addresses.list.zip    (STRING) + rep/def levels

Reader 는 rep/def level 로 원래 nested 구조를 복원. JSON 을 flatten 하지 않고도 컬럼 프로젝션이 됨.

Row Group / Page 크기 결정

파라미터권장범위트레이드오프
Row Group256-512 MB128 MB - 1 GB크면 압축 좋음/random access 느림
Page1-10 MB512 KB - 64 MB작으면 필터링 세밀/오버헤드 증가
Rows per Row Group1M-10M컬럼 폭에 따라메모리 vs 압축

일반 권장: 256 MB row group + SNAPPY/ZSTD + Dictionary encoding.

Parquet vs 다른 포맷

포맷저장압축쿼리 속도스키마 진화중첩주요 사용처
ParquetColumn3-6x빠름우수우수데이터 레이크, 분석
ORCColumn4-8x빠름우수우수Hive/Spark
AvroRow2-4x느림최고우수스트리밍 (Kafka)
CSVRow1-2x매우 느림없음없음Legacy, 사람 읽기
JSONRow1-3x느림우수우수API, 반정형
JSONL/NDJSONRow1-3x느림우수우수로그, 스트리밍

대략: Parquet 은 CSV 대비 크기 10-100배 감소, 분석 쿼리 10-100배 빠름.

라이브러리

언어/도구라이브러리특징
PythonpyarrowC++ Arrow 백엔드, 권장
Pythonpandas (v2.0+)내부적으로 pyarrow 사용
Python/Rustpolars매우 빠름, 최근 표준화
SQL (in-process)duckdbParquet 을 SQL 로 바로
JVMparquet-mr, SparkHadoop 생태계
Goparquet-goSegment/InfluxData 활용
Rustarrow-rs, parquetzero-copy, 매우 빠름

실전 예제

Python (pyarrow)

import pyarrow as pa
import pyarrow.parquet as pq

# 쓰기
table = pa.table({
    'id': [1, 2, 3],
    'name': ['a', 'b', 'c'],
    'ts': pa.array([1706000000, 1706000001, 1706000002], type=pa.timestamp('ms'))
})

pq.write_table(
    table,
    'out.parquet',
    compression='zstd',
    row_group_size=1_000_000,
    use_dictionary=True,
    write_statistics=True,
)

# 읽기 (필요한 컬럼만 + 필터)
table = pq.read_table(
    'out.parquet',
    columns=['id', 'ts'],
    filters=[('id', '>=', 2)],   # predicate pushdown
)

DuckDB (SQL 로 바로)

-- Parquet 을 테이블처럼 쿼리
SELECT COUNT(*) FROM 's3://bucket/data/**/*.parquet'
WHERE ts >= '2026-01-01';

-- Parquet 으로 저장
COPY (SELECT * FROM orders WHERE year = 2026)
TO 'orders_2026.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);

Spark / Glue

# read
df = spark.read.parquet("s3://bucket/data/")
df.filter("year = 2026").select("id", "amount").show()

# write with partitioning
df.write \
  .mode("overwrite") \
  .partitionBy("year", "month") \
  .option("compression", "zstd") \
  .parquet("s3://bucket/out/")

데이터 레이크에서의 파티셔닝 관용

디렉토리 구조로 파티션 표현 (Hive 스타일):

s3://bucket/events/
├── year=2026/
│   ├── month=01/
│   │   ├── day=01/
│   │   │   └── part-00000.parquet
│   │   └── day=02/
│   └── month=02/

Athena / Spectrum / Spark 는 WHERE year=2026 AND month=01 을 만나면 그 폴더의 파일만 스캔. 파티션 프루닝 = 비용/시간 절감의 핵심.

Small Files Problem

파티션이 세분화되거나 스트리밍 flush 가 잦으면 파일이 작아짐 (KB ~ MB). 결과:

  • Row group 이 너무 작아 압축비 하락
  • 파일 open/close 오버헤드 폭증
  • 메타데이터 오버헤드 (파일당 footer)
  • S3 LIST/GET 요청 비용 증가
  • 쿼리 엔진의 파일 스캔 병렬화 손해

해결:

  1. Compaction job: 작은 파일을 주기적으로 256 MB+ 로 합침
  2. 파티션 세분화 완화: day 대신 month
  3. Iceberg / Delta Lake / Hudi: 자동 compaction, snapshot
  4. S3 Tables (2024+): 관리형 compaction

함정

WARNING

INT96 timestamp = Legacy (Impala 호환). 새 파일은 INT64 + timestamp logical type 사용. INT96 은 나노초까지만 표현 못함 + 표준 X.

CAUTION

row group 이 너무 큼 = OOM 위험, 스트리밍 flush 시 지연. 너무 작음 = 위 small files 문제. 256 MB 근처가 sweet spot.

WARNING

Dictionary encoding 이 fallback 하면 조용히 압축비 붕괴. Column cardinality 를 확인. 필요하면 dictionary_page_size_limit 조정.

IMPORTANT

파일 rename/append 없다. Parquet 은 immutable. 수정하려면 새 파일로 write + 옛 파일 삭제. Iceberg/Delta 가 이 위에 트랜잭션 semantics 를 얹음.

CAUTION

Schema 변경 후 옛 파일과 섞임 = reader 따라 결과 다름. 스키마 이력 관리 (Iceberg schema ID) 또는 partitioned by schema version.

WARNING

UTF-8 아닌 문자열 을 STRING 으로 저장 = reader 별 처리 다름. BINARY (bytes) 로 저장하고 앱 레벨에서 처리.

IMPORTANT

압축 codec 미지정 = SNAPPY 기본 (라이브러리별). 새 파이프라인은 ZSTD 로 명시. GZIP 은 CPU 부담 크니 아카이빙에만.

관련 위키

이 글의 용어 (8개)
[AWS] Amazon Redshiftcloud
정의 Amazon Redshift 는 AWS 가 관리하는 페타바이트 규모 컬럼형 데이터 웨어하우스 입니다. 2012년 PostgreSQL 8.0.2 를 기반으로 시작해 MPP (…
[AWS] Athena: 서버리스 SQL on S3cloud
정의 Amazon Athena 는 S3 에 있는 데이터를 서버 없이 SQL 로 쿼리 하는 서비스. 클러스터 프로비저닝, 스키마 로딩, 인덱스 생성 없이 데이터 파일 (Parque…
[AWS] Glue: 서버리스 ETL + Data Catalogcloud
정의 AWS Glue 는 서버리스 ETL + 통합 메타데이터 카탈로그 플랫폼. Apache Spark 로 데이터 변환을 실행하고, Hive 호환 Data Catalog 로 스키마…
[AWS] Redshift Spectrum: S3 데이터 직접 쿼리cloud
정의 Redshift Spectrum 은 Redshift 에서 S3 에 있는 데이터를 로드하지 않고 직접 SQL 로 쿼리 하는 기능. 별도 Spectrum 실행 계층 (Spect…
[AWS] S3 Glacier: Instant / Flexible / Deep Archivecloud
정의 Amazon S3 Glacier 는 S3 의 장기 아카이빙 스토리지 클래스 3종. 자주 접근하지 않는 데이터를 극도로 저렴하게 (Standard 대비 최대 95% 절감) 저…
[AWS] S3: object storage, storage classes, lifecyclecloud
정의 S3 = AWS 의 object storage. bucket + key + object. 11 9's durability (99.999999999%), 무한 확장. 2026…
데이터 웨어하우스data-engineering
정의 데이터 웨어하우스 (Data Warehouse, DW) 는 여러 운영 시스템에서 흘러 들어온 이력 데이터를 통합해, 대규모 분석 쿼리를 빠르게 실행하도록 최적화된 중앙 저장…
ETL / ELTdata-engineering
정의 ETL = Extract (추출) + Transform (변환) + Load (적재). 여러 소스에서 데이터를 뽑아 정제한 뒤 데이터 웨어하우스 에 저장하는 파이프라인. E…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기