프로덕션에서의 파케트: 분석을 위한 데이터 읽기, 쓰기 및 최적화 방법
파케트는 단순한 저장 형식 그 이상입니다. 이는 아키텍처 차원의 최적화 도구이기도 합니다. 열 기반 구조, 읽기 시 필터링 기능, 사전 인코딩, 그리고 행 그룹 메타데이터 덕분에 I/O를 수십 배 줄이고 CSV 대비 데이터 크기를 최대 140배까지 축소할 수 있습니다. 데이터 엔지니어와 분석가에게는 이는 디스크 공간 절약뿐만 아니라 대화형 쿼리의 지연 시간 감소, 메모리 부담 완화, 예측 가능한 성능 달성 등을 의미합니다. 본 글에서는 중급/고급 수준의 파이썬에서 파케트를 다루는 실용적인 가이드를 제공합니다—pandas를 통한 기본 읽기부터 스키마, 파티셔닝, 압축까지 수동으로 제어하는 방법까지.
현대 데이터 파이프라인에서 파케트가 CSV를 대체하는 이유
CSV는 의미가 없는 텍스트 기반 형식입니다: 각 행은 하나의 블록으로 처리되며, 구분자는 실시간으로 파싱되고 타입은 경험적으로 추론됩니다. 반면 파케트는 바이너리, 엄격히 타입화된 열 기반 형식으로 내장된 메타데이터를 갖추고 있습니다. 주요 차이점은 다음과 같습니다:
- 열 기반 저장: 각 열이 별도로 저장되어 필요한 필드(예:
year와indicator_value)만 읽을 수 있으며 나머지는 무시할 수 있습니다—행 전체를 파싱하지 않아도 됩니다. - 행 그룹: 데이터는 블록(기본 ~128MB)으로 나뉘며 각 블록에는 통계(
min,max,null_count)가 포함됩니다.year == 2023으로 필터링할 때엔 엔진이 메모리에 로드하지 않고도 전체 행 그룹을 건너뜁니다. - 사전 인코딩: 반복성이 높은 문자열 열(지역, 카테고리 등)의 경우 파케트는 자동 또는 명시적으로 고유값 사전과 이를 참조하는 방식을 저장합니다—즉, ‘모스크바 지역’ × 50만 건 대신 코드
1× 50만 건으로 표현됩니다. - 타입 안전성: CSV에서는 숫자 필드에 NULL이 들어가면 해당 열 전체가
float64로 변환되지만, 파케트에서는 스키마에 타입이 엄격히 정의되어 작성 시 검증됩니다.
실제로의 차이는 매우 큽니다: 576MB 크기의 암 발병률 데이터셋은 파케트로 변환하면 4MB로 압축되며(압축 비율 ×144), 필터링된 하위 집합을 읽는 데 걸리는 시간은 12~18초에서 300ms 미만으로 줄어듭니다.
읽기: pandas에서 pyarrow를 이용한 세밀한 제어까지
pandas를 통한 기본 읽기
빠른 시작을 위해서는 pd.read_parquet()만으로 충분합니다:
import pandas as pd
df = pd.read_parquet(
"data_zis_109_v20260126.parquet",
columns=["object_name", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
pyarrow를 통한 직접 접근: 스키마와 메타데이터에 대한 제어
정밀한 제어가 필요할 때는 pyarrow.parquet를 직접 사용하세요:
import pyarrow.parquet as pq
# 데이터를 로드하지 않고 스키마와 메타데이터만 확인
schema = pq.read_schema("data_zis_109_v20260126.parquet")
meta = pq.read_metadata("data_zis_109_v20260126.parquet")
print(f"행: {meta.num_rows:,}, 열: {meta.num_columns}")
# 필터링과 열 선택을 통해 읽기
table = pq.read_table(
"data_zis_109_v20260126.parquet",
columns=["object_name", "object_oktmo", "year", "indicator_value"],
filters=[("year", "=", 2023)]
)
df = table.to_pandas() # 필요 시 pandas로 변환
pf = pq.ParquetFile("data_zis_109_v20260126.parquet")
for batch in pf.iter_batches(batch_size=100_000):
df_batch = batch.to_pandas()
# 배치 처리
쓰기: 간단한 방법부터 프로덕션 수준의 직렬화까지
자동 직렬화의 문제점
df.to_parquet()는 작동하지만 위험을 숨깁니다:
- 암묵적인 타입 결정:
int64에 NULL이 있으면float64로,datetime64는string으로 변환됩니다. - 고카디널리티 문자열 열에 대한 사전 인코딩 부재.
- 부적절한 행 그룹 크기(기본 64MB)로 필터링 효율이 떨어집니다.
명시적 스키마와 제어된 인코딩
import pyarrow as pa
import pyarrow.parquet as pq
schema = pa.schema([
pa.field("object_name", pa.string()),
pa.field("object_oktmo", pa.dictionary(pa.int32(), pa.string())),
pa.field("year", pa.int32()),
pa.field("indicator_value", pa.float64())
])
table = pa.Table.from_pandas(df, schema=schema, preserve_index=False)
pq.write_table(
table,
"data_zis_new_version.parquet",
compression="zstd",
row_group_size=500_000,
version="2.6",
write_statistics=True
)
파티셔닝과 파일 세트 작업
데이터가 논리적으로 분할되어 있다면(연도별, 지역별), write_to_dataset()를 사용하세요:
pq.write_to_dataset(
table,
root_path="data_zis_partitioned/",
partition_cols=["year"]
)
import pyarrow.dataset as ds
dataset = ds.dataset("data_zis_partitioned/", format="parquet", partitioning="hive")
table = dataset.to_table(filter=ds.field("year") == 2023)
무엇이 중요한가
- 파케트는 데이터베이스를 대체하지 않습니다: 추가만 가능한 읽기 중심의 분석 시나리오에 이상적이지만 트랜잭션, UPDATE, 인덱스를 지원하지 않습니다.
- 사전 인코딩은 카디널리티가 10⁴ 이하이고 반복성이 30%를 넘을 때 최대 효과를 발휘합니다. 고유값이 있는 열( UUID, 타임스탬프 등)에는 소용이 없습니다.
- 행 그룹 크기는 필터링 정확도에 영향을 미칩니다: 너무 작으면 메타데이터가 과도하게 생성되고, 너무 크면 불필요한 데이터를 건너뛰는 것이 비효율적입니다. 최적 범위는 10만~50만 행입니다.
- 통계(
write_statistics=True)는 프로덕션 환경에서 반드시 필요합니다. 없으면 필터링이 전체 스캔처럼 동작합니다. - 항상 스키마를 검증하고 작성하세요:
pa.Table.from_pandas(df, schema=schema)는 타입이 일치하지 않으면 오류를 발생시킵니다—이는 침묵하는 손상으로부터 보호합니다.
— Editorial Team
아직 댓글이 없습니다.