1474 words
7 minutes
[DE Design Pattern]08-05. Metadata Enhancer

5. Read Performance Optimization — Metadata Enhancer#

01. Pattern Overview#

Metadata Enhancer는 데이터 파일에 통계 정보(min, max, null count 등)를 메타데이터로 저장하여, 쿼리 시 데이터를 읽기 전에 불필요한 파일/블록을 스킵하는 패턴

  • ClickHouse sparse index가 granule 단위로 하는 것을, 파일 포맷 레벨에서 수행.
  • Sorter 패턴이 이 메타데이터의 효과를 극대화. 이는 데이터가 정렬되어 있으면 각 파일의 min/max 범위가 좁아져 스킵 확률이 높아지기 때문

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이 실행되면:

  1. Footer만 읽음 (파일의 마지막 몇 바이트)
  2. max=50이므로 50 초과 값이 이 파일에 없음을 확인
  3. 파일 전체를 스킵 → 데이터 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_vals
FROM pg_stats
WHERE 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: 5000
Column: id
min: 0018e1dc-1b80-4410-92f6-...
max: fffbe4f8-8d88-43d2-...
null_count: 0
num_values: 5000

3: 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=50
File 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=33
File 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 : 쿼리 엔진이 필터 조건을 스토리지 레이어까지 내려보내 불필요한 데이터를 최대한 일찍 걸러내는 최적화

[DE Design Pattern]08-05. Metadata Enhancer
https://yjinheon.netlify.app/posts/02de/00-de-design-pattern/08_data_storage/08-05-metadata_enhancer/
Author
Datamind
Published at
2026-03-27
License
CC BY-NC-SA 4.0