Delta Lake란?
Delta Lake는 데이터 레이크 위에 ACID 트랜잭션, 스키마 관리, 타임 트래블 등을 제공하는 오픈소스 스토리지 레이어입니다.
왜 성능 튜닝이 필요한가?
Delta Lake는 기본 설정만으로도 잘 동작하지만, 데이터가 수십 GBTB 규모로 커지면 스몰 파일 누적, 비용이 높은인 파일 레이아웃, 불필요한 이전 버전 파일 등이 쿼리 성능을 크게 떨어뜨립니다.
특히 실시간 스트리밍이나 잦은 소량 INSERT가 반복되는 테이블에서는, 튜닝 없이 방치하면 쿼리 시간이 수 배수십 배 느려질 수 있습니다.
이 체크리스트는 Delta Lake 테이블이 느려졌을 때 체크해야 할 항목을 우선순위 순서로 정리합니다.
권장 실행 순서
처음 성능 튜닝을 시작한다면 아래 순서를 따르세요:
| 순서 | 작업 | 설명 |
|---|
| 1 | Liquid Clustering 설정 | 자주 필터링하는 컬럼을 클러스터링 키로 지정 |
| 2 | OPTIMIZE 실행 | 기존 데이터에 클러스터링을 소급 적용하고 파일 병합 |
| 3 | VACUUM 스케줄 등록 | 불필요한 이전 버전 파일 삭제로 스토리지 절약 |
| 4 | 모니터링 체크 | DESCRIBE DETAIL로 파일 수/크기를 주기적 확인 |
1. 클러스터링 전략 선택
의사결정 가이드
| 기준 | Liquid Clustering | Z-ORDER | Hive 파티셔닝 |
|---|
| DBR 버전 | 13.3+ | 모든 버전 | 모든 버전 |
| 클러스터링 키 변경 | ALTER TABLE로 즉시 변경 | OPTIMIZE 재실행 필요 | 테이블 재생성 필요 |
| 다중 키 성능 | 우수 (모든 키 동등) | 첫 번째 키에 편향 | 키 순서에 의존 |
| 스몰 파일 자동 정리 | 자동 (OPTIMIZE 시) | 수동 OPTIMIZE 필요 | 수동 관리 |
| 추천 시나리오 | 신규 테이블 기본 선택 | 레거시 호환 | 날짜 기반 대량 삭제 |
Liquid Clustering 설정
아래 SQL은 새 테이블을 만들면서 region과 order_date를 클러스터링 키로 지정합니다. 이 두 컬럼으로 필터링하는 쿼리가 자동으로 데이터 스킵의 혜택을 받습니다.
-- 신규 테이블: 생성 시 CLUSTER BY로 클러스터링 키 지정
CREATE TABLE my_catalog.sales.orders (
order_id BIGINT,
customer_id BIGINT,
order_date DATE,
region STRING,
amount DECIMAL(12,2)
)
CLUSTER BY (region, order_date);
이미 존재하는 테이블에 Liquid Clustering을 적용하려면 ALTER TABLE을 사용합니다. 키 변경이 즉시 반영되지만, 기존 데이터에 적용하려면 OPTIMIZE를 한 번 실행해야 합니다.
-- 기존 테이블에 Liquid Clustering 전환
ALTER TABLE my_catalog.sales.orders
CLUSTER BY (region, order_date);
-- 클러스터링 적용 (기존 데이터에 소급)
OPTIMIZE my_catalog.sales.orders;
Z-ORDER (레거시)
Liquid Clustering을 지원하지 않는 구버전 환경에서만 사용합니다. DBR 13.3 이상이라면 Liquid Clustering을 권장합니다.
-- Liquid Clustering 지원 안 되는 환경에서만 사용
OPTIMIZE my_catalog.sales.orders
ZORDER BY (region, order_date);
Liquid Clustering과 Z-ORDER는 동시에 사용할 수 없습니다.
Liquid Clustering이 설정된 테이블에 ZORDER를 실행하면 오류가 발생합니다.
2. OPTIMIZE 전략
OPTIMIZE는 여러 개의 작은 파일을 더 큰 파일로 병합(compaction)하고, 클러스터링 키에 따라 데이터를 재배치합니다. 스몰 파일이 줄어들면 쿼리 시 읽어야 할 파일 수가 감소하여 성능이 크게 향상됩니다.
실행 타이밍
| 워크로드 패턴 | OPTIMIZE 빈도 | 이유 |
|---|
| 시간당 수십~수백 건 append | 4~6시간마다 | 스몰 파일 누적 방지 |
| 배치 ETL (1일 1회) | ETL 직후 1회 | 바로 분석 쿼리에 최적화 |
| 스트리밍 (분 단위 마이크로배치) | 1~2시간마다 | 마이크로배치 파일 병합 |
| 읽기 전용 (append 없음) | 불필요 | 이미 최적화된 상태 |
예약 실행
전체 테이블을 OPTIMIZE하거나, WHERE 조건으로 최근 데이터만 부분 최적화할 수 있습니다. 부분 OPTIMIZE는 대형 테이블에서 실행 시간을 크게 줄여줍니다.
-- 전체 테이블 OPTIMIZE (Lakeflow Job 또는 Scheduled Query에서 실행)
OPTIMIZE my_catalog.sales.orders;
-- 부분 OPTIMIZE — 최근 7일 데이터만 최적화 (대형 테이블에 권장)
OPTIMIZE my_catalog.sales.orders
WHERE order_date >= current_date() - INTERVAL 7 DAYS;
OPTIMIZE 결과 확인
이 쿼리는 테이블의 작업 히스토리를 보여줍니다. numFilesRemoved(제거된 파일 수)가 크다면 스몰 파일이 많이 병합되었다는 의미입니다.
DESCRIBE HISTORY my_catalog.sales.orders;
-- numFilesAdded, numFilesRemoved를 확인
-- numFilesRemoved가 크면 스몰 파일이 많았다는 의미
3. VACUUM 모범 사례
VACUUM이 왜 필요한가?
Delta Lake는 UPDATE, DELETE, OPTIMIZE를 실행할 때마다 새 파일을 생성하고, 이전 파일은 Time Travel(과거 버전 조회)을 위해 그대로 남겨둡니다. 이 “더 이상 현재 버전에서 사용하지 않는 파일”이 계속 쌓이면 스토리지 비용이 증가하고, 메타데이터 목록이 커져 파일 리스팅 속도도 느려집니다. VACUUM은 보존 기간이 지난 이전 파일을 물리적으로 삭제합니다.
기본 실행
아래 SQL은 기본 보존 기간(7일)보다 오래된 불필요 파일을 삭제합니다.
-- 기본 보존 기간 (7일) 적용
VACUUM my_catalog.sales.orders;
-- 보존 기간을 명시적으로 지정 (7일 = 168시간)
VACUUM my_catalog.sales.orders RETAIN 168 HOURS; -- 7일
안전한 VACUUM 설정
테이블 속성으로 보존 기간을 설정해두면, VACUUM 실행 시 별도로 기간을 지정하지 않아도 됩니다.
-- 테이블 속성으로 보존 기간 설정 (권장)
ALTER TABLE my_catalog.sales.orders
SET TBLPROPERTIES (
'delta.deletedFileRetentionDuration' = 'interval 7 days', -- 삭제된 파일 보존 기간
'delta.logRetentionDuration' = 'interval 30 days' -- 트랜잭션 로그 보존 기간
);
VACUUM 주의사항
| 항목 | 권장 | 이유 |
|---|
| 보존 기간 | >= 7일 | Time Travel, 롤백 여유 |
| 실행 빈도 | 1일 1회 | 스토리지 비용 절감 |
| 실행 시간대 | 오프피크 | I/O 부하 분산 |
spark.databricks.delta.retentionDurationCheck.enabled | true (기본) | 실수로 0시간 VACUUM 방지 |
VACUUM RETAIN 0 HOURS는 Time Travel을 완전히 파괴합니다.
반드시 retentionDurationCheck를 비활성화해야 실행되며, 프로덕션에서는 금지합니다.
4. Photon 활성화 체크리스트
| 확인 항목 | 방법 | 기대 효과 |
|---|
| 클러스터 Runtime | Photon 체크박스 또는 x.x.x-photon-scala2.12 선택 | 자동 활성화 |
| SQL Warehouse | Serverless 또는 Pro 선택 | 기본 Photon 적용 |
| 워크로드 유형 | Scan, Filter, Agg, Join 위주 | 2~8x 속도 향상 |
| 비호환 UDF | Python UDF → SQL 내장 함수로 변환 | Photon 최적화 경로 유지 |
| 파일 포맷 | Parquet (Delta 기본) | Photon 최적 포맷 |
Photon 효과 확인
-- Query Profile에서 Photon 사용 여부 확인
-- SQL Warehouse > Query History > 쿼리 선택 > Query Profile
-- 각 연산자에 ⚡ 아이콘이 표시되면 Photon 적용
-- Photon이 적용되지 않는 대표 패턴:
-- 1. Python UDF 사용 시
-- 2. 비표준 데이터 타입 (MapType 일부 연산)
-- 3. 일부 복잡한 정규식 처리
5. 스몰 파일 문제 진단 및 해결
스몰 파일이 왜 문제인가?
Delta Lake는 데이터를 Parquet 파일로 저장합니다. 파일 하나를 읽을 때마다 메타데이터 파싱, 파일 열기/닫기, 네트워크 요청 등의 오버헤드가 발생합니다.
예를 들어 1GB 데이터가 10개의 100MB 파일에 있으면 오버헤드가 10번이지만, 1000개의 1MB 파일에 있으면 오버헤드가 1000번 발생합니다. 데이터 양은 같지만 파일 수가 많으면 I/O 비용이 수십 배 증가합니다.
이 쿼리는 테이블의 전체 파일 수(numFiles)와 총 크기(sizeInBytes)를 보여줍니다. 평균 파일 크기 = sizeInBytes / numFiles로 계산하세요.
-- 파일 수와 평균 크기 확인
DESCRIBE DETAIL my_catalog.sales.orders;
-- numFiles, sizeInBytes 확인
-- 평균 파일 크기 = sizeInBytes / numFiles
| 평균 파일 크기 | 상태 | 조치 |
|---|
| 64MB ~ 1GB | 정상 | 유지 |
| 10MB ~ 64MB | 경고 | OPTIMIZE 실행 |
| < 10MB | 심각 | 즉시 OPTIMIZE, 원인 파악 |
| > 1GB | 과대 | 클러스터링 키 추가 검토 |
원인별 해결
-- 원인 1: 잦은 소량 INSERT
-- 해결: 마이크로배치 크기 증가 + 주기적 OPTIMIZE
OPTIMIZE my_catalog.sales.orders;
-- 원인 2: 과도한 파티셔닝 (예: 날짜+지역+상품 3중 파티션)
-- 해결: Liquid Clustering으로 전환하면 파티션 없이 동일 효과
ALTER TABLE my_catalog.sales.orders
CLUSTER BY (region, order_date);
-- 원인 3: 스트리밍 writeStream의 기본 출력 (1분 마이크로배치 → 매분 1개 파일 생성)
-- 해결: trigger 간격 조정
-- (PySpark 예시)
# df.writeStream \
# .trigger(processingTime="5 minutes") \ # 1분 → 5분으로 늘려 파일 생성 빈도 감소
# .option("checkpointLocation", "...") \
# .toTable("my_catalog.sales.orders")
자동 컴팩션 활성화
이 설정을 켜면 쓰기 작업 시 자동으로 파일을 최적 크기로 병합합니다. 수동 OPTIMIZE를 자주 실행하기 어려운 환경에 유용합니다.
ALTER TABLE my_catalog.sales.orders
SET TBLPROPERTIES (
'delta.autoOptimize.optimizeWrite' = 'true', -- 쓰기 시 파일 크기 최적화
'delta.autoOptimize.autoCompact' = 'true' -- 쓰기 후 자동 파일 병합
);
6. 쿼리 최적화 팁
Predicate Pushdown 확인
클러스터링 키나 파티션 키로 필터링하면 Delta Lake가 불필요한 파일을 건너뛰어 읽기 양을 크게 줄일 수 있습니다(Data Skipping). 단, 필터 컬럼에 함수를 적용하면 이 최적화가 무효화됩니다.
-- 좋은 예: 클러스터링/파티션 키로 직접 필터 → Data Skipping 적용
SELECT * FROM my_catalog.sales.orders
WHERE region = 'APAC' AND order_date >= '2026-01-01';
-- 나쁜 예: 함수 적용 시 pushdown 불가 → 전체 파일 스캔
SELECT * FROM my_catalog.sales.orders
WHERE YEAR(order_date) = 2026; -- pushdown 안 됨!
-- 수정: 범위 필터로 변경하면 Data Skipping 다시 적용
SELECT * FROM my_catalog.sales.orders
WHERE order_date >= '2026-01-01' AND order_date < '2027-01-01';
Column Pruning
필요한 컬럼만 선택하면 읽는 데이터 양이 줄어듭니다. Parquet는 컬럼별로 데이터를 저장하므로 SELECT * 대신 필요 컬럼만 지정하면 I/O가 크게 감소합니다.
-- 나쁜 예: SELECT * — 모든 컬럼을 읽음
SELECT * FROM my_catalog.sales.orders WHERE region = 'APAC';
-- 좋은 예: 필요한 컬럼만 — 읽기 I/O 절감
SELECT order_id, customer_id, amount
FROM my_catalog.sales.orders
WHERE region = 'APAC';
JOIN 최적화
작은 테이블(수만 건 이하)을 조인할 때 BROADCAST 힌트를 사용하면, Spark가 작은 테이블을 전체 노드에 복사하여 셔플 없이 조인합니다.
-- 작은 테이블(customers)을 BROADCAST → 셔플 제거로 대폭 속도 향상
SELECT /*+ BROADCAST(c) */
o.order_id, o.amount, c.customer_name
FROM my_catalog.sales.orders o
JOIN my_catalog.sales.customers c ON o.customer_id = c.customer_id
WHERE o.order_date >= '2026-01-01';
캐시 활용
대화형 분석에서 동일 데이터를 반복 조회할 때 캐시를 활용하면 두 번째 쿼리부터 메모리에서 즉시 읽습니다.
-- 반복 사용 테이블 캐시 (대화형 분석 시)
CACHE SELECT order_id, customer_id, amount, region
FROM my_catalog.sales.orders
WHERE order_date >= current_date() - INTERVAL 30 DAYS;
종합 체크리스트
| # | 항목 | SQL/설정 | 확인 |
|---|
| 1 | Liquid Clustering 설정 | ALTER TABLE ... CLUSTER BY (...) | [ ] |
| 2 | OPTIMIZE 스케줄 등록 | Lakeflow Job 또는 Scheduled Query | [ ] |
| 3 | VACUUM 스케줄 등록 | VACUUM ... RETAIN 168 HOURS | [ ] |
| 4 | 보존 기간 테이블 속성 설정 | deletedFileRetentionDuration | [ ] |
| 5 | Photon Runtime 사용 | 클러스터 또는 SQL Warehouse 확인 | [ ] |
| 6 | 스몰 파일 평균 크기 확인 | DESCRIBE DETAIL → 64MB~1GB | [ ] |
| 7 | autoOptimize 활성화 | optimizeWrite + autoCompact | [ ] |
| 8 | 쿼리에 SELECT * 없음 | 필요 컬럼만 지정 | [ ] |
| 9 | 필터 조건에 함수 미사용 | 범위 조건으로 변환 | [ ] |
| 10 | BROADCAST 힌트 적용 | 작은 차원 테이블 조인 시 | [ ] |