5. Read Performance Optimization — Metadata Enhancer
01. Pattern Overview
Metadata Enhancer는 데이터 파일에 통계 정보(min, max, null count 등)를 메타데이터로 저장하여, 쿼리 시 데이터를 읽기 전에 불필요한 파일/블록을 스킵하는 패턴
- ClickHouse sparse index가 granule 단위로 하는 것을, 파일 포맷 레벨에서 수행.
- Sorter 패턴이 이 메타데이터의 효과를 극대화. 이는 데이터가 정렬되어 있으면 각 파일의 min/max 범위가 좁아져 스킵 확률이 높아지기 때문
Parquet Footer 통계
Apache Parquet는 columnar file format이다. 각 파일 끝에 footer가 있고, 여기에 컬럼별 통계가 기록
Parquet File┌─────────────────────────────┐│ Row Group 0 ││ ├── age 컬럼: [19, 24, 50, 33, 38, 39, 40] ││ ├── name 컬럼: [...] ││ └── ... │├─────────────────────────────┤│ Footer (메타데이터) ││ ├── age: min=19, max=50, nulls=0 ││ ├── name: min="Alice", max="Zoe" ││ └── num_rows: 7 │└─────────────────────────────┘쿼리 WHERE age > 50이 실행되면:
- Footer만 읽음 (파일의 마지막 몇 바이트)
- max=50이므로 50 초과 값이 이 파일에 없음을 확인
- 파일 전체를 스킵 → 데이터 I/O 없음
Footer는 데이터보다 훨씬 작으므로 모든 파일의 footer를 읽는 오버헤드는 무시
Table File Format의 추가 메타데이터 레이어
Delta Lake, Iceberg, Hudi는 Parquet footer 위에 commit log에 추가 통계를 저장한다. 이 덕분에 Parquet footer조차 읽지 않고 commit log만으로 파일 스킵이 가능
Delta Lake commit log (JSON){ "add": { "path": "part-00001.snappy.parquet", "size": 50437, "stats": { "numRecords": 6100, "minValues": {"type": "galaxy", "version": "Android 10"}, "maxValues": {"type": "mac", "version": "v1716..."}, "nullCount": {"type": 0, "full_name": 0, "version": 0} } }}이 구조 덕분에 쿼리 계획 단계에서 파일을 열지 않고 commit log만 읽어서 어떤 파일을 읽어야 하는지 결정 가능
RDBMS의 통계
- PostgreSQL, MySQL 등은 테이블/컬럼별 통계를 별도 시스템 테이블에 저장하고,
- 쿼리 플래너가 이를 활용하여 최적 실행 계획을 생성
-- PostgreSQL: 통계 확인SELECT tablename, attname, n_distinct, most_common_valsFROM pg_statsWHERE tablename = 'visits';
-- 통계가 오래되었으면 수동 갱신ANALYZE visits;RDBMS 통계는 쿼리 플래너를 위한 것이고, Parquet 통계는 데이터 스킵을 위한 것이라는 차이가 있지만, “메타데이터로 불필요한 작업을 줄인다”는 본질은 동일
02. Consequences
쓰기 오버헤드: 데이터를 쓸 때 각 컬럼의 min/max/null count를 계산해야 한다. 대부분의 경우 무시할 수준이지만, 컬럼이 매우 많은 wide table에서는 체감될 수 있다.
Out-of-date 통계: RDBMS에서는 데이터가 조금씩 변경되면 자동 통계 갱신 임계치에 도달하지 못해 통계가 오래될 수 있다. 오래된 통계는 잘못된 실행 계획으로 이어져 오히려 성능이 나빠질 수 있다. ANALYZE TABLE로 수동 갱신이 필요
Parquet 파일 쓰기 (통계 자동 생성)
from pyspark.sql import SparkSession
spark = SparkSession.builder.getOrCreate()
input_dataset = spark.read.schema( "id STRING, age INT, name STRING, event_time TIMESTAMP").json("/data/users")
# Parquet으로 저장 → footer에 min/max/null 통계가 자동 생성됨input_dataset.write.mode("overwrite").parquet("/output/users_parquet")별도 설정 없이 Parquet으로 쓰기만 하면 통계가 자동 생성된다. JSON, CSV로 저장하면 이 메타데이터가 없으므로 data skipping이 불가능하다. 이것이 분석 워크로드에서 Parquet를 쓰는 핵심 이유 중 하나다.
Parquet 통계 확인
import pyarrow.parquet as pq
# Parquet 파일의 메타데이터 읽기parquet_file = pq.ParquetFile("/output/users_parquet/part-00000.snappy.parquet")metadata = parquet_file.metadata
# Row Group 0의 컬럼별 통계 확인for i in range(metadata.row_group(0).num_columns): col = metadata.row_group(0).column(i) print(f"Column: {col.path_in_schema}") if col.statistics: print(f" min: {col.statistics.min}") print(f" max: {col.statistics.max}") print(f" null_count: {col.statistics.null_count}") print(f" num_values: {col.statistics.num_values}")출력 예시:
Column: age min: 19 max: 50 null_count: 0 num_values: 5000Column: id min: 0018e1dc-1b80-4410-92f6-... max: fffbe4f8-8d88-43d2-... null_count: 0 num_values: 50003: Delta Lake commit log 통계 확인
from pyspark.sql import SparkSession
spark = SparkSession.builder \ .config("spark.jars.packages", "io.delta:delta-spark_2.12:3.1.0") \ .getOrCreate()
# Delta Lake으로 저장input_dataset.write.format("delta").save("/output/users_delta")
# commit log에서 통계 확인commit_log = spark.read.json("/output/users_delta/_delta_log/*.json")commit_log.select("add.path", "add.stats").show(truncate=False)03. Sorter + Metadata Enhancer 시너지
이 두 패턴이 결합되면 효과가 극대화된다. 정렬 없이 저장하면 파일 간 값 범위가 겹치고, 정렬 후 저장하면 범위가 분리된다.
# 정렬 없이 저장된 2개 파일File A: age [19, 50, 24, 38] → min=19, max=50File B: age [33, 40, 39, 22] → min=22, max=40
WHERE age = 22 → File A(19~50 포함 가능), File B(22~40 포함 가능)→ 두 파일 모두 읽어야 함
# age 기준 정렬 후 저장된 2개 파일File A: age [19, 22, 24, 33] → min=19, max=33File B: age [38, 39, 40, 50] → min=38, max=50
WHERE age = 22 → File A(19~33 포함 가능), File B(38~50 불가능)→ File A만 읽으면 됨Concept
- Metadata Enhancer : 데이터 파일에 컬럼별 통계(min, max, null count)를 메타데이터로 저장하여 불필요한 파일/블록 스킵을 가능하게 하는 패턴
- Parquet Footer : Parquet 파일 끝에 위치하는 메타데이터 블록. 각 row group의 컬럼별 min/max/null count 등 통계 포함
- Data Skipping : 메타데이터 통계를 확인하여 쿼리 조건에 해당하지 않는 파일/블록을 읽지 않는 최적화
- Commit Log 통계 : Delta Lake, Iceberg 등이 Parquet footer 위에 추가로 저장하는 파일 레벨 통계. 파일을 열지 않고 스킵 결정 가능
- ANALYZE TABLE : RDBMS에서 테이블 통계를 수동으로 갱신하는 명령. 오래된 통계로 인한 비효율적 실행 계획 방지
- Row Group : Parquet 파일 내 행의 논리적 묶음 단위. 각 row group마다 독립적인 통계가 존재
- Columnar File Format : 컬럼 단위로 데이터를 저장하는 파일 형식(Parquet, ORC). 필요한 컬럼만 읽을 수 있고 통계 기반 스킵 지원
- Predicate Pushdown : 쿼리 엔진이 필터 조건을 스토리지 레이어까지 내려보내 불필요한 데이터를 최대한 일찍 걸러내는 최적화