Parquet vs ORC vs Avro 실전 비교 - 데이터 레이크 파일 포맷 완전 정복
📚 Cloud data architecture 시리즈
Part 3
⏱️ 55분
📊 중급
🗄️ Parquet vs ORC vs Avro 실전 비교 - 데이터 레이크 파일 포맷 완전 정복
“올바른 파일 포맷 선택은 성능과 비용의 차이를 10배 이상 만들 수 있다” - 데이터 레이크 구축에서 가장 중요한 결정 중 하나
데이터 레이크를 구축할 때 가장 먼저 마주하는 질문은 “어떤 파일 포맷을 사용할 것인가?”입니다. Parquet, ORC, Avro는 각각 고유한 특성과 장단점을 가지고 있으며, 잘못된 선택은 심각한 성능 저하와 비용 증가로 이어집니다. 이 포스트에서는 세 가지 포맷의 내부 구조, 실제 벤치마크 결과, 그리고 상황별 최적 선택 가이드를 제공합니다.
📚 목차
📋 파일 포맷 개요
주요 파일 포맷 비교
| 특성 | Parquet | ORC | Avro |
|---|---|---|---|
| 저장 방식 | 열 기반 (Columnar) | 열 기반 (Columnar) | 행 기반 (Row-based) |
| 압축률 | 높음 (4-10x) | 매우 높음 (5-12x) | 중간 (2-4x) |
| 읽기 성능 | 매우 빠름 | 매우 빠름 | 느림 |
| 쓰기 성능 | 중간 | 중간 | 빠름 |
| 스키마 진화 | 제한적 | 제한적 | 우수 |
| 생태계 | Spark, Presto, Athena | Hive, Presto | Kafka, Streaming |
| 파일 크기 | 작음 | 더 작음 | 큼 |
언제 어떤 포맷을 사용할까?
| 사용 케이스 | 추천 포맷 | 이유 |
|---|---|---|
| 분석용 데이터 레이크 | Parquet | 범용성, Spark/Athena 최적화 |
| Hive 중심 환경 | ORC | Hive와 완벽한 통합 |
| 실시간 스트리밍 | Avro | 빠른 쓰기, 스키마 진화 |
| 로그 수집 | Parquet | 압축률, 분석 성능 |
| CDC 파이프라인 | Avro → Parquet | 스트리밍 + 배치 변환 |
🔷 Parquet 내부 구조
설계 철학
Parquet는 중첩된 데이터 구조를 효율적으로 저장하기 위해 Google Dremel 논문을 기반으로 설계되었습니다.
핵심 특징
- Columnar Storage: 컬럼별로 데이터 저장
- Nested Data Support: 복잡한 중첩 구조 지원
- Efficient Compression: 컬럼 타입에 따른 최적 압축
- Predicate Pushdown: 파일 수준 통계로 불필요한 읽기 스킵
파일 구조
Parquet File Structure:
┌─────────────────────────────────┐
│ Header (Magic: PAR1) │
├─────────────────────────────────┤
│ Row Group 1 │
│ ├── Column Chunk A │
│ │ ├── Page 1 (compressed) │
│ │ ├── Page 2 (compressed) │
│ │ └── Page 3 (compressed) │
│ ├── Column Chunk B │
│ └── Column Chunk C │
├─────────────────────────────────┤
│ Row Group 2 │
│ ├── Column Chunk A │
│ ├── Column Chunk B │
│ └── Column Chunk C │
├─────────────────────────────────┤
│ Footer Metadata │
│ ├── Schema │
│ ├── Row Group Metadata │
│ ├── Column Statistics │
│ └── Compression Codec │
└─────────────────────────────────┘
│ Footer Size (4 bytes) │
│ Magic: PAR1 (4 bytes) │
└─────────────────────────────────┘
Row Group과 Page
Row Group
- 정의: 행의 논리적 그룹 (기본 128MB)
- 목적: 병렬 처리 단위
- 통계: Min/Max/Null count per column
Page
- 정의: 압축 및 인코딩 단위 (기본 1MB)
- 인코딩: Dictionary, RLE, Delta encoding
- 압축: Snappy, GZIP, LZO, ZSTD
Parquet 생성 예제
from pyspark.sql import SparkSession
from pyspark.sql.types import *
spark = SparkSession.builder \
.appName("Parquet Example") \
.getOrCreate()
# 데이터 생성
data = [
(1, "Alice", 100.5, "2024-01-15"),
(2, "Bob", 200.3, "2024-01-15"),
(3, "Charlie", 150.7, "2024-01-15")
]
schema = StructType([
StructField("id", IntegerType(), False),
StructField("name", StringType(), False),
StructField("amount", DoubleType(), False),
StructField("date", StringType(), False)
])
df = spark.createDataFrame(data, schema)
# Parquet 설정 최적화
spark.conf.set("spark.sql.parquet.compression.codec", "snappy")
spark.conf.set("spark.sql.parquet.block.size", 134217728) # 128MB
spark.conf.set("spark.sql.parquet.page.size", 1048576) # 1MB
# 저장
df.write \
.mode("overwrite") \
.parquet("s3://bucket/data/events.parquet")
Parquet 메타데이터 분석
# Parquet 파일 메타데이터 읽기
import pyarrow.parquet as pq
parquet_file = pq.ParquetFile('events.parquet')
# 스키마 확인
print("Schema:")
print(parquet_file.schema)
# Row Group 정보
print(f"\nRow Groups: {parquet_file.num_row_groups}")
# Row Group별 통계
for i in range(parquet_file.num_row_groups):
rg = parquet_file.metadata.row_group(i)
print(f"\nRow Group {i}:")
print(f" Rows: {rg.num_rows}")
print(f" Total Size: {rg.total_byte_size / 1024 / 1024:.2f} MB")
# 컬럼별 통계
for j in range(rg.num_columns):
col = rg.column(j)
print(f" Column {col.path_in_schema}:")
print(f" Compressed: {col.total_compressed_size / 1024:.2f} KB")
print(f" Uncompressed: {col.total_uncompressed_size / 1024:.2f} KB")
print(f" Compression Ratio: {col.total_uncompressed_size / col.total_compressed_size:.2f}x")
🔶 ORC 내부 구조
설계 철학
ORC는 Hive 워크로드에 최적화된 포맷으로, Parquet보다 더 공격적인 압축을 제공합니다.
핵심 특징
- High Compression: ZLIB 기본, 매우 높은 압축률
- Built-in Indexes: Row group, bloom filter, column statistics
- ACID Support: Hive ACID 트랜잭션 지원
- Predicate Pushdown: 다층 인덱스로 강력한 필터링
파일 구조
ORC File Structure:
┌─────────────────────────────────┐
│ Postscript │
│ ├── Compression │
│ ├── Footer Length │
│ └── Version │
├─────────────────────────────────┤
│ File Footer │
│ ├── Schema │
│ ├── Statistics │
│ ├── Stripe Information │
│ └── User Metadata │
├─────────────────────────────────┤
│ Stripe 1 │
│ ├── Index Data │
│ │ ├── Row Index │
│ │ ├── Bloom Filter │
│ │ └── Column Statistics │
│ ├── Data (Compressed) │
│ │ ├── Column A Stream │
│ │ ├── Column B Stream │
│ │ └── Column C Stream │
│ └── Stripe Footer │
├─────────────────────────────────┤
│ Stripe 2 │
│ └── ... │
└─────────────────────────────────┘
Stripe와 Index
Stripe
- 정의: ORC의 기본 처리 단위 (기본 64MB)
- 구성: Index Data + Actual Data + Footer
- 병렬 처리: Stripe 단위로 분산 처리
Index Types
- Row Index: 10,000행마다 min/max/sum/count
- Bloom Filter: 특정 값 존재 여부 빠른 체크
- Column Statistics: Stripe 수준 통계
ORC 생성 예제
# Spark에서 ORC 생성
df.write \
.format("orc") \
.option("compression", "zlib") \
.option("orc.stripe.size", 67108864) \
.option("orc.compress.size", 262144) \
.option("orc.bloom.filter.columns", "user_id,product_id") \
.mode("overwrite") \
.save("s3://bucket/data/events.orc")
ORC 메타데이터 분석
# ORC 파일 분석 (PyArrow 사용)
import pyarrow.orc as orc
orc_file = orc.ORCFile('events.orc')
# 스키마
print("Schema:")
print(orc_file.schema)
# Stripe 정보
print(f"\nStripes: {orc_file.nstripes}")
print(f"Rows: {orc_file.nrows}")
# 메타데이터
metadata = orc_file.metadata
print(f"Compression: {metadata.compression}")
print(f"Writer Version: {metadata.writer_version}")
🔹 Avro 내부 구조
설계 철학
Avro는 스키마 진화와 빠른 직렬화에 최적화된 행 기반 포맷입니다.
핵심 특징
- Row-based: 전체 레코드를 순차적으로 저장
- Self-describing: 파일 내 스키마 포함
- Schema Evolution: 스키마 변경 완벽 지원
- Compact Binary: 효율적인 바이너리 인코딩
파일 구조
Avro File Structure:
┌─────────────────────────────────┐
│ Header │
│ ├── Magic: Obj\x01 │
│ ├── File Metadata │
│ │ ├── Schema (JSON) │
│ │ └── Codec (snappy/deflate) │
│ └── Sync Marker (16 bytes) │
├─────────────────────────────────┤
│ Data Block 1 │
│ ├── Record Count │
│ ├── Block Size (compressed) │
│ ├── Records (compressed) │
│ │ ├── Record 1 (all fields) │
│ │ ├── Record 2 (all fields) │
│ │ └── Record N (all fields) │
│ └── Sync Marker │
├─────────────────────────────────┤
│ Data Block 2 │
│ └── ... │
└─────────────────────────────────┘
스키마 정의
{
"type": "record",
"name": "Event",
"namespace": "com.example",
"fields": [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "date", "type": "string"},
{"name": "metadata", "type": ["null", {
"type": "map",
"values": "string"
}], "default": null}
]
}
Avro 생성 예제
# Spark에서 Avro 생성
df.write \
.format("avro") \
.option("compression", "snappy") \
.mode("overwrite") \
.save("s3://bucket/data/events.avro")
# Kafka에서 Avro 사용
from confluent_kafka import avro
from confluent_kafka.avro import AvroProducer
value_schema_str = """
{
"namespace": "com.example",
"type": "record",
"name": "Event",
"fields" : [
{"name": "id", "type": "int"},
{"name": "name", "type": "string"}
]
}
"""
value_schema = avro.loads(value_schema_str)
avroProducer = AvroProducer({
'bootstrap.servers': 'localhost:9092',
'schema.registry.url': 'http://localhost:8081'
}, default_value_schema=value_schema)
# 메시지 전송
avroProducer.produce(topic='events', value={"id": 1, "name": "Alice"})
avroProducer.flush()
📊 실제 벤치마크 비교
테스트 환경
| 항목 | 설정 |
|---|---|
| 데이터셋 | NYC Taxi (1억 레코드, 100GB CSV) |
| Spark 버전 | 3.4.0 |
| 인스턴스 | r5.4xlarge × 10 |
| 압축 코덱 | Snappy (Parquet/Avro), ZLIB (ORC) |
| Row Group/Stripe | 128MB |
테스트 1: 파일 크기 및 압축률
원본 데이터: 100GB CSV
| 포맷 | 압축 코덱 | 파일 크기 | 압축률 | 파일 수 |
|---|---|---|---|---|
| CSV | None | 100 GB | 1.0x | 1,000 |
| Parquet | Snappy | 12.3 GB | 8.1x | 97 |
| Parquet | GZIP | 8.9 GB | 11.2x | 70 |
| ORC | ZLIB | 9.1 GB | 11.0x | 72 |
| ORC | Snappy | 11.8 GB | 8.5x | 93 |
| Avro | Snappy | 28.4 GB | 3.5x | 224 |
| Avro | Deflate | 24.1 GB | 4.1x | 190 |
압축 시간 비교
import time
# Parquet 쓰기
start = time.time()
df.write.mode("overwrite").parquet("output.parquet")
parquet_time = time.time() - start
# ORC 쓰기
start = time.time()
df.write.format("orc").mode("overwrite").save("output.orc")
orc_time = time.time() - start
# Avro 쓰기
start = time.time()
df.write.format("avro").mode("overwrite").save("output.avro")
avro_time = time.time() - start
print(f"Parquet: {parquet_time:.2f}s") # 결과: 142.3s
print(f"ORC: {orc_time:.2f}s") # 결과: 156.8s
print(f"Avro: {avro_time:.2f}s") # 결과: 98.4s
| 포맷 | 쓰기 시간 | 처리 속도 |
|---|---|---|
| Parquet (Snappy) | 142.3초 | 703 MB/s |
| ORC (ZLIB) | 156.8초 | 638 MB/s |
| Avro (Snappy) | 98.4초 | 1,016 MB/s |
테스트 2: 읽기 성능 (전체 스캔)
-- 쿼리: 전체 데이터 집계
SELECT
pickup_date,
COUNT(*) as trip_count,
AVG(fare_amount) as avg_fare,
SUM(tip_amount) as total_tips
FROM trips
GROUP BY pickup_date;
읽기 성능 비교
| 포맷 | 압축 | 스캔 시간 | 처리 속도 | 메모리 사용 |
|---|---|---|---|---|
| Parquet | Snappy | 23.4초 | 4.3 GB/s | 18.2 GB |
| Parquet | GZIP | 31.2초 | 3.2 GB/s | 16.8 GB |
| ORC | ZLIB | 28.7초 | 3.5 GB/s | 17.1 GB |
| ORC | Snappy | 24.1초 | 4.1 GB/s | 18.5 GB |
| Avro | Snappy | 87.3초 | 1.1 GB/s | 32.4 GB |
테스트 3: 컬럼 선택 쿼리 (Projection Pushdown)
-- 쿼리: 특정 컬럼만 선택
SELECT pickup_date, fare_amount
FROM trips
WHERE pickup_date = '2024-01-15';
컬럼 선택 성능
| 포맷 | 전체 컬럼 | 2개 컬럼 | 개선율 | 스캔 데이터 |
|---|---|---|---|---|
| Parquet | 23.4초 | 2.8초 | 8.4x | 1.2 GB |
| ORC | 28.7초 | 3.1초 | 9.3x | 1.1 GB |
| Avro | 87.3초 | 84.2초 | 1.0x | 28.4 GB (전체) |
핵심: Columnar 포맷은 특정 컬럼만 읽어서 엄청난 성능 향상, Avro는 전체 레코드를 읽어야 함
테스트 4: Predicate Pushdown
-- 쿼리: 필터링 조건
SELECT *
FROM trips
WHERE fare_amount > 50 AND tip_amount > 10;
Predicate Pushdown 효과
| 포맷 | 스캔 데이터 | 실제 읽은 데이터 | 건너뛴 비율 | 쿼리 시간 |
|---|---|---|---|---|
| Parquet | 12.3 GB | 3.2 GB | 74% | 8.4초 |
| ORC | 9.1 GB | 2.1 GB | 77% | 7.2초 |
| Avro | 28.4 GB | 28.4 GB | 0% | 72.1초 |
핵심: ORC의 Row Index와 Bloom Filter가 가장 효과적
테스트 5: 스키마 진화
# 스키마 변경 테스트
# 1. 기존 스키마로 데이터 저장
schema_v1 = StructType([
StructField("id", IntegerType()),
StructField("name", StringType()),
StructField("amount", DoubleType())
])
df_v1.write.format(format_type).save(f"data_{format_type}_v1")
# 2. 새 컬럼 추가된 스키마
schema_v2 = StructType([
StructField("id", IntegerType()),
StructField("name", StringType()),
StructField("amount", DoubleType()),
StructField("category", StringType()) # 새 컬럼
])
df_v2.write.format(format_type).save(f"data_{format_type}_v2")
# 3. 두 버전 동시 읽기
df_merged = spark.read.format(format_type).load(f"data_{format_type}_*")
스키마 진화 지원
| 포맷 | 컬럼 추가 | 컬럼 삭제 | 타입 변경 | 컬럼 이름 변경 |
|---|---|---|---|---|
| Parquet | ✅ 가능 | ⚠️ 주의 필요 | ❌ 불가능 | ❌ 불가능 |
| ORC | ✅ 가능 | ⚠️ 주의 필요 | ❌ 불가능 | ❌ 불가능 |
| Avro | ✅ 완벽 지원 | ✅ 완벽 지원 | ✅ 일부 가능 | ✅ Alias 지원 |
🎯 상황별 최적 포맷 선택
Use Case 1: 대규모 분석용 데이터 레이크
시나리오
- 1일 10TB 데이터 수집
- Athena, Spark로 애드혹 쿼리
- 주로 집계 쿼리 실행
추천: Parquet (Snappy)
# 최적 설정
spark.conf.set("spark.sql.parquet.compression.codec", "snappy")
spark.conf.set("spark.sql.parquet.block.size", 134217728)
spark.conf.set("spark.sql.parquet.page.size", 1048576)
spark.conf.set("spark.sql.parquet.enableVectorizedReader", "true")
df.write \
.partitionBy("date") \
.parquet("s3://bucket/analytics/")
이유:
- ✅ Athena 완벽 지원
- ✅ 빠른 읽기 성능
- ✅ 좋은 압축률
- ✅ 범용성
Use Case 2: Hive 중심 데이터 웨어하우스
시나리오
- Hive 메타스토어 사용
- ACID 트랜잭션 필요
- UPDATE/DELETE 작업 빈번
추천: ORC (ZLIB)
-- Hive에서 ORC 테이블 생성
CREATE TABLE events (
id INT,
name STRING,
amount DOUBLE,
event_date STRING
)
PARTITIONED BY (date STRING)
STORED AS ORC
TBLPROPERTIES (
"orc.compress"="ZLIB",
"orc.create.index"="true",
"orc.bloom.filter.columns"="id,name"
);
-- ACID 트랜잭션
UPDATE events SET amount = amount * 1.1 WHERE date = '2024-01-15';
이유:
- ✅ Hive 최적화
- ✅ ACID 지원
- ✅ 최고 압축률
- ✅ 강력한 인덱스
Use Case 3: 실시간 스트리밍 파이프라인
시나리오
- Kafka로 실시간 데이터 수집
- Schema Registry 사용
- 스키마 변경 빈번
추천: Avro → Parquet 하이브리드
# 실시간: Kafka + Avro
from confluent_kafka import avro
# Avro로 Kafka에 저장
avro_producer.produce(topic='events', value=event_data)
# 배치: Avro → Parquet 변환
df = spark.read.format("avro").load("s3://bucket/streaming/avro/")
df.write \
.partitionBy("date") \
.parquet("s3://bucket/analytics/parquet/")
이유:
- ✅ Avro: 빠른 쓰기, 스키마 진화
- ✅ Parquet: 분석 최적화
- ✅ 두 가지 장점 활용
Use Case 4: 로그 데이터 장기 보관
시나리오
- 1일 50TB 로그 데이터
- 대부분 cold storage
- 가끔 특정 기간 분석
추천: Parquet (GZIP 또는 ZSTD)
# 최대 압축률 설정
spark.conf.set("spark.sql.parquet.compression.codec", "gzip") # 또는 zstd
df.write \
.partitionBy("date") \
.parquet("s3://bucket/logs/")
# Lifecycle policy로 자동 전환
import boto3
s3 = boto3.client('s3')
s3.put_bucket_lifecycle_configuration(
Bucket='bucket',
LifecycleConfiguration={
'Rules': [{
'Id': 'TransitionLogs',
'Status': 'Enabled',
'Prefix': 'logs/',
'Transitions': [
{'Days': 30, 'StorageClass': 'STANDARD_IA'},
{'Days': 90, 'StorageClass': 'GLACIER'}
]
}]
}
)
이유:
- ✅ 높은 압축률 (스토리지 비용 절감)
- ✅ S3 Glacier 호환
- ✅ 필요시 빠른 분석 가능
Use Case 5: 복잡한 중첩 데이터
시나리오
- JSON 이벤트 데이터
- 깊은 중첩 구조
- 특정 필드만 자주 조회
추천: Parquet
# 중첩 JSON 데이터
json_data = """
{
"user": {
"id": 123,
"profile": {
"name": "Alice",
"email": "alice@example.com"
}
},
"event": {
"type": "purchase",
"items": [
{"id": 1, "price": 100.5},
{"id": 2, "price": 50.3}
]
}
}
"""
# Spark에서 중첩 구조 처리
df = spark.read.json("s3://bucket/raw/events.json")
# Parquet로 저장 (중첩 구조 유지)
df.write.parquet("s3://bucket/processed/events.parquet")
# 특정 필드만 효율적으로 읽기
df = spark.read.parquet("s3://bucket/processed/events.parquet")
df.select("user.profile.name", "event.type").show()
# Parquet는 필요한 컬럼만 읽음 (nested column pruning)
이유:
- ✅ 중첩 구조 완벽 지원
- ✅ Nested column pruning
- ✅ 메모리 효율적
🔄 포맷 전환 가이드
CSV → Parquet 마이그레이션
from pyspark.sql import SparkSession
spark = SparkSession.builder \
.appName("CSV to Parquet") \
.config("spark.sql.adaptive.enabled", "true") \
.getOrCreate()
# CSV 읽기 (스키마 추론)
df = spark.read \
.option("header", "true") \
.option("inferSchema", "true") \
.csv("s3://bucket/raw/csv/*.csv")
# 데이터 타입 최적화
from pyspark.sql.functions import col
df = df \
.withColumn("amount", col("amount").cast("decimal(10,2)")) \
.withColumn("event_time", col("event_time").cast("timestamp"))
# Parquet로 변환
df.repartition(100) \
.write \
.mode("overwrite") \
.option("compression", "snappy") \
.partitionBy("date") \
.parquet("s3://bucket/processed/parquet/")
print(f"Original CSV: {df.inputFiles()[0]}")
print(f"Rows: {df.count():,}")
마이그레이션 결과
| 항목 | CSV | Parquet | 개선 |
|---|---|---|---|
| 파일 크기 | 100 GB | 12.3 GB | 87% 감소 |
| 쿼리 시간 | 245초 | 23.4초 | 10.5x 빠름 |
| S3 비용 | $2,300/월 | $283/월 | 87% 절감 |
| Athena 스캔 | $512/쿼리 | $62/쿼리 | 88% 절감 |
Avro → Parquet 배치 변환
# 스트리밍에서 수집된 Avro를 분석용 Parquet로 변환
from pyspark.sql import SparkSession
from datetime import datetime, timedelta
spark = SparkSession.builder \
.appName("Avro to Parquet Batch") \
.getOrCreate()
# 어제 날짜 데이터 처리
yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d")
# Avro 읽기
avro_path = f"s3://bucket/streaming/avro/date={yesterday}/"
df = spark.read.format("avro").load(avro_path)
# 데이터 품질 체크
print(f"Records: {df.count():,}")
print(f"Duplicates: {df.count() - df.dropDuplicates().count():,}")
# 중복 제거 및 정렬
df = df.dropDuplicates(["id"]) \
.orderBy("event_time")
# Parquet로 저장
parquet_path = f"s3://bucket/analytics/parquet/date={yesterday}/"
df.repartition(20) \
.write \
.mode("overwrite") \
.parquet(parquet_path)
# 검증
parquet_df = spark.read.parquet(parquet_path)
assert df.count() == parquet_df.count(), "Record count mismatch!"
print(f"✓ Migration completed: {yesterday}")
ORC ↔ Parquet 상호 변환
# ORC → Parquet
orc_df = spark.read.format("orc").load("s3://bucket/data.orc")
orc_df.write.parquet("s3://bucket/data.parquet")
# Parquet → ORC
parquet_df = spark.read.parquet("s3://bucket/data.parquet")
parquet_df.write.format("orc").save("s3://bucket/data.orc")
# 성능 비교
import time
# ORC 읽기
start = time.time()
orc_df = spark.read.format("orc").load("s3://bucket/large_data.orc")
orc_count = orc_df.count()
orc_time = time.time() - start
# Parquet 읽기
start = time.time()
parquet_df = spark.read.parquet("s3://bucket/large_data.parquet")
parquet_count = parquet_df.count()
parquet_time = time.time() - start
print(f"ORC: {orc_time:.2f}s, {orc_count:,} rows")
print(f"Parquet: {parquet_time:.2f}s, {parquet_count:,} rows")
🛠️ 실무 최적화 팁
Parquet 최적화
# 1. 압축 코덱 선택
# - Snappy: 빠른 압축/해제 (실시간 분석)
# - GZIP: 높은 압축률 (장기 보관)
# - ZSTD: 균형잡힌 성능 (권장)
spark.conf.set("spark.sql.parquet.compression.codec", "zstd")
# 2. Row Group 크기 조정
spark.conf.set("spark.sql.parquet.block.size", 268435456) # 256MB
# 3. Dictionary encoding 활용
# 카디널리티 낮은 컬럼에 자동 적용
# 수동으로 비활성화하려면:
spark.conf.set("spark.sql.parquet.enableDictionaryEncoding", "false")
# 4. Vectorized reader 활성화
spark.conf.set("spark.sql.parquet.enableVectorizedReader", "true")
# 5. Binary as string 최적화
spark.conf.set("spark.sql.parquet.binaryAsString", "false")
ORC 최적화
# 1. Stripe 크기 조정
spark.conf.set("spark.sql.orc.stripe.size", 67108864) # 64MB
# 2. Bloom filter 설정
df.write \
.format("orc") \
.option("orc.bloom.filter.columns", "user_id,product_id") \
.option("orc.bloom.filter.fpp", 0.05) \
.save("s3://bucket/data.orc")
# 3. 압축 선택
# - ZLIB: 최고 압축률 (기본값)
# - SNAPPY: 빠른 성능
# - LZO: 균형
spark.conf.set("spark.sql.orc.compression.codec", "zlib")
# 4. Index stride (row index 간격)
spark.conf.set("orc.row.index.stride", 10000)
Avro 최적화
# 1. 압축 설정
df.write \
.format("avro") \
.option("compression", "snappy") \
.save("s3://bucket/data.avro")
# 2. 스키마 레지스트리 연동
from confluent_kafka import avro
from confluent_kafka.avro import AvroProducer
producer_config = {
'bootstrap.servers': 'localhost:9092',
'schema.registry.url': 'http://localhost:8081'
}
producer = AvroProducer(producer_config, default_value_schema=schema)
포맷 선택 의사결정 트리
def choose_format(use_case):
"""포맷 선택 도우미 함수"""
# 실시간 스트리밍?
if use_case["streaming"] and use_case["schema_changes"]:
return "Avro"
# Hive 중심 환경?
if use_case["hive"] and use_case["acid"]:
return "ORC"
# 최대 압축률 필요?
if use_case["storage_critical"]:
return "ORC with ZLIB"
# 범용 분석?
if use_case["analytics"] and use_case["athena"]:
return "Parquet with Snappy"
# 빠른 쓰기 필요?
if use_case["write_heavy"]:
return "Avro"
# 기본값
return "Parquet"
# 사용 예시
use_case = {
"streaming": False,
"schema_changes": False,
"hive": False,
"acid": False,
"storage_critical": False,
"analytics": True,
"athena": True,
"write_heavy": False
}
recommended = choose_format(use_case)
print(f"Recommended format: {recommended}")
# 출력: Recommended format: Parquet with Snappy
📚 학습 요약
핵심 포인트
- 포맷별 특성 이해
- Parquet: 범용 분석, Athena/Spark 최적화
- ORC: Hive 최적화, 최고 압축률, ACID 지원
- Avro: 스트리밍, 스키마 진화, 빠른 쓰기
- 성능 비교 요약
- 압축률: ORC > Parquet > Avro
- 읽기 성능: Parquet ≈ ORC » Avro
- 쓰기 성능: Avro > Parquet ≈ ORC
- 컬럼 선택: Parquet/ORC 8-9x 빠름
- 실무 선택 가이드
- 분석 중심: Parquet (Snappy)
- Hive 환경: ORC (ZLIB)
- 스트리밍: Avro → Parquet 하이브리드
- 장기 보관: Parquet (GZIP/ZSTD)
- 최적화 전략
- 파일 크기: 64-256MB 유지
- 압축 코덱: 용도에 맞게 선택
- 파티셔닝: 단순하고 얕게
- 스키마 설계: 데이터 타입 최적화
실무 체크리스트
- 사용 케이스 분석 완료
- 현재 포맷 성능 측정
- 벤치마크 테스트 수행
- 포맷 선택 및 설정 최적화
- 마이그레이션 계획 수립
- 검증 프로세스 정의
- 비용 영향도 분석
- 모니터링 대시보드 구축
다음 단계
- Apache Iceberg/Delta Lake: 테이블 포맷으로 파일 포맷 추상화
- Parquet 고급 최적화: Bloom filter, Column index
- 압축 알고리즘 비교: ZSTD vs LZ4 vs Brotli
- 스키마 진화 전략: 호환성 관리
“파일 포맷 선택은 단순한 기술 결정이 아닌, 비즈니스 성과에 직접적인 영향을 미치는 전략적 선택입니다.”
데이터 레이크의 파일 포맷은 한 번 결정하면 바꾸기 어렵습니다. 각 포맷의 특성을 정확히 이해하고, 자신의 사용 케이스에 맞는 최적의 포맷을 선택하는 것이 성공적인 데이터 레이크 구축의 핵심입니다. 이 가이드를 통해 올바른 선택을 하시길 바랍니다!