LLM WikiAccess-protected knowledge portal

WIKI

Lance 2.x: AI 워크로드를 위한 컬럼형 포맷이 Parquet와 다른 이유

왜 AI가 Parquet에 불만족하는가 Parquet는 OLAP 분석을 위해 설계되었다. 행 그룹 row group 단위로 데이터를 묶고, 각 컬럼을 통째로 압축·인코딩한다. 전체 테이블 스캔과 집계에는 탁월하다. 그러나 AI 워크로드는 다른 패턴을 요구한다. 랜덤 접근 모델 훈련에서 배치를 구성할 때 수백만 개의 샘플 중 임의의 수천 개를 꺼낸다. Parquet는 행 그룹 전체를 디코딩해야 원하는 행에 접근할 수 있다. 포인

경로human/study/content/database-frontier/67-lance-format-ai-native-columnar-lakehouse.md
카테고리Study
태그#columnar #format #lakehouse #lance #mysql #native #study

왜 AI가 Parquet에 불만족하는가

Parquet는 OLAP 분석을 위해 설계되었다. 행 그룹(row group) 단위로 데이터를 묶고, 각 컬럼을 통째로 압축·인코딩한다. 전체 테이블 스캔과 집계에는 탁월하다. 그러나 AI 워크로드는 다른 패턴을 요구한다.

Lance 포맷은 이 문제들을 첫 번째 설계 원칙으로 삼고 만들어진 컬럼형 포맷이다.

이 챕터는 Lance 포맷 2.x 사양과 LanceDB 공식 문서, 2026년 5월 DuckDB 블로그 포스트를 기준으로 한다.


Lance 파일 포맷 아키텍처

Lance 파일(.lance)은 Rust로 작성된 엔진 위에서 동작하며 내부를 세 개의 계층으로 구성한다.

Parquet vs Lance: 랜덤 접근 경로 Parquet (분석 최적) Row Group 1 (수천~수십만 행) col_A [RLE 압축] | col_B [DELTA 압축] | col_C [임베딩 바이너리] → 행 하나를 읽으려면 Row Group 전체 디코딩 필요 Row Group 2 ... Row Group N (각 그룹이 독립 단위, 위치 기반 접근 불가) Footer: schema + row group offsets + column statistics 랜덤 행 접근: O(row_group_size) 디코딩 전체 스캔: ✅ 포인트 룩업: ❌ 버전 관리: 없음 벡터 인덱스: 없음 멀티모달 대형 바이너리: 비효율 Lance 2.x (AI 워크로드 최적) Fragment 1 행 위치 인덱스 내장 컬럼 오프셋 직접 접근 deletion file (삭제 비트맵) Fragment N 독립 단위, 병렬 읽기 대형 바이너리는 외부 참조 또는 인라인 저장 Manifest (버전 스냅샷) fragment 목록 + 스키마 + 인덱스 메타데이터 + 타임스탬프 Vector Index HNSW, IVF_PQ Scalar Index BTree, Bitmap, FTS 랜덤 행 접근: O(1) — 이웃 행 디코딩 불필요 전체 스캔: ✅ 포인트 룩업: ✅ 버전 관리: 내장 MVCC 벡터 인덱스: 내장
Lance 포맷 아키텍처와 Parquet 비교

Fragment 기반 설계의 핵심

Lance의 핵심 단위는 Fragment다. Parquet의 Row Group과 비슷해 보이지만 동작 방식이 다르다.

위치 주소 지정 (Positional Addressing)

Parquet에서 특정 행에 접근하려면 해당 행이 속한 Row Group 전체를 디코딩해야 한다. 행 그룹 크기가 1만 개라면, 원하는 행 1개를 얻기 위해 1만 개를 읽는다.

Lance는 Structural Encoding을 사용해 모든 행을 행 위치(row position)로 직접 주소 지정할 수 있게 만든다. 파일 내 어떤 행도 이웃 행의 디코딩 없이 꺼낼 수 있다. 배치 샘플링에서 N개 인덱스를 주면, 정확히 N개 행만 I/O를 발생시킨다.

Fragment와 Deletion File

각 Fragment는 독립 단위다. 삭제는 실제로 행을 지우는 대신 Deletion File(비트맵)에 기록한다. 읽기 시 비트맵으로 삭제된 행을 필터링한다. 이 방식으로 빈번한 업데이트(UPSERT)를 소형 쓰기로 처리할 수 있다.

Fragment가 너무 많아지면 조각화가 발생한다. 이 경우 Compaction(컴팩션)으로 여러 Fragment를 하나로 합친다. 운영자는 Fragment 수와 I/O 증폭을 모니터링해 컴팩션 주기를 정한다.


MVCC 버전 관리와 Time Travel

Lance 테이블 포맷은 Manifest 파일 목록으로 버전 히스토리를 관리한다.

my_dataset/
  _versions/
    1.manifest    ← 초기 1000개 행
    2.manifest    ← 500개 추가, Fragment 2 생성
    3.manifest    ← 행 100개 삭제 (deletion file 추가)
  _indices/       ← 인덱스 파일 (HNSW 등)
  fragment_0/     ← Fragment 데이터 파일
  fragment_1/

새 버전을 만들 때 기존 데이터를 복사하지 않는다. 새 Manifest가 새 Fragment(또는 변경된 Fragment)와 기존 Fragment를 모두 참조한다. 변경되지 않은 데이터는 그대로 공유된다. 이것이 Zero-Copy 버전 관리다.

특정 버전을 참조하려면 Manifest 번호를 지정하면 된다. ML 실험 재현 시 데이터셋 버전을 Manifest 번호로 고정하면 완전한 재현성을 확보할 수 있다.

import lance

# 현재 버전 읽기
ds = lance.dataset("s3://my-bucket/training_data")

# 특정 버전 고정 (Time Travel)
ds_v2 = lance.dataset("s3://my-bucket/training_data", version=2)

# PyTorch Dataset으로 변환
torch_dataset = ds.to_torch(columns=["image", "label"])

벡터 인덱스 통합

Lance는 별도 벡터 DB 없이도 파일 수준에서 ANN(Approximate Nearest Neighbor) 검색을 지원한다.

지원 인덱스 종류는 다음과 같다.

인덱스 타입특징적합 사용 사례
HNSW높은 recall, 메모리 상주중소 규모 정밀 검색
IVF_PQ큰 데이터셋, 메모리 효율수억 개 임베딩 검색
Bitmap저카디널리티 스칼라 필터분류 레이블 필터링
BTree범위 조회날짜·숫자 필터
FTS (Full-Text Search)텍스트 검색키워드 기반 검색

벡터 검색과 스칼라 필터를 함께 쓰는 Hybrid Search가 가능하다. "범주 = 'cat'이고 임베딩 유사도 Top 10" 같은 쿼리를 단일 연산으로 처리한다.

# 벡터 + 스칼라 하이브리드 검색
result = ds.to_table(
    nearest={"column": "embedding", "q": query_vector, "k": 10},
    filter="category = 'cat'"
)

인덱스는 테이블 포맷 수준에 저장되므로 LanceDB뿐 아니라 DuckDB 같은 외부 쿼리 엔진에서도 활용된다.


DuckDB 통합: SQL로 Lance 읽고 쓰기

2026년 5월, DuckDB와 Lance 팀이 협업해 lance-duckdb 확장을 공개했다. SQL 분석 엔진인 DuckDB가 Lance 파일을 직접 읽고 쓸 수 있게 된 것이다.

-- Lance 확장 로드
INSTALL lance FROM 'core';
LOAD lance;

-- Lance 테이블 조회
SELECT image_id, label, metadata
FROM lance_scan('s3://my-bucket/training_data')
WHERE split = 'train'
LIMIT 100;

-- 쿼리 결과를 Lance 포맷으로 저장
COPY (SELECT * FROM processed_data)
TO 's3://my-bucket/output' (FORMAT lance);

이 통합이 의미 있는 이유는 아키텍처 패턴의 완성도 때문이다.

2026년 실전 멀티모달 Lakehouse 패턴
Iceberg 테이블
구조화 데이터 거버넌스
BI / OLAP 쿼리
SQL 카탈로그 관리
Trino·Spark 연동
DuckDB (SQL 브릿지)
양쪽 포맷 읽기/쓰기
로컬 분석과 ETL
Arrow 인터체인지
Lance 테이블
훈련 데이터 / 임베딩
랜덤 접근 + 벡터 검색
PyTorch DataLoader 직결
멀티모달 (이미지·오디오·텍스트)
Iceberg가 더 적합한 경우
여러 팀이 공유하는 분석 테이블
엄격한 스키마 진화와 거버넌스
대형 배치 집계 쿼리
Lance가 더 적합한 경우
ML 훈련·검색 파이프라인
벡터 인덱스와 랜덤 접근이 핵심
멀티모달 데이터셋 버전 관리
Lance + Iceberg + DuckDB 분리 아키텍처

운영 체크리스트

Fragment 수 관리

Fragment가 늘어나면 쿼리 계획 오버헤드가 증가한다. 일반적인 권장 기준은 다음과 같다.

# LanceDB 컴팩션 실행
import lancedb
db = lancedb.connect("s3://my-bucket")
table = db.open_table("training_data")
table.compact_files()

인덱스 갱신 주기

데이터를 추가하면 기존 인덱스가 자동으로 갱신되지 않는다. 인덱스가 오래될수록 검색 recall이 떨어진다. 데이터 추가 후 주기적으로 인덱스를 재구축하거나 incremental index append를 사용한다.

스토리지 비용 관리

버전이 쌓이면 오래된 Manifest와 삭제 파일이 쌓인다. 주기적인 Cleanup(vacuum)으로 오래된 버전을 정리한다. 실험 재현에 필요한 버전만 유지하고 나머지는 제거한다.

# 7일 이전 버전 정리
table.cleanup_old_versions(older_than=timedelta(days=7))

PyTorch DataLoader 연동

Lance가 빠른 이유 중 하나는 GPU 훈련 루프와의 직결이다. to_torch() 메서드로 PyTorch Dataset을 즉시 만들 수 있다.

from torch.utils.data import DataLoader

ds = lance.dataset("s3://my-bucket/training_data", version=5)
torch_ds = ds.to_torch(
    columns=["image", "label"],
    batch_size=256,
    shuffle=True
)
loader = DataLoader(torch_ds, num_workers=4)

shuffle=True로 셔플링을 설정하면 Lance가 내부적으로 랜덤 접근 경로를 사용해 배치를 구성한다. 시퀀셜 I/O만 지원하는 Parquet와 달리 배치 구성 비용이 낮다.


도입 전 확인할 제약

성숙도 차이: Lance는 Parquet·Iceberg 대비 생태계가 작다. Spark 네이티브 연동, 카탈로그 거버넌스, 멀티엔진 ACID 쓰기 등에서 아직 제한이 있다. 분석 중심 워크로드는 여전히 Iceberg가 더 적합하다.

프로덕션 운영 경험: Fragment 컴팩션, 인덱스 재구축, 버전 정리의 운영 경험이 Parquet 기반 시스템보다 축적이 적다. 도입 초기에는 소규모 파이프라인부터 시작한다.

쿼리 엔진 지원: 2026년 5월 기준 DuckDB 통합이 공개되었지만 Trino·Flink의 Lance 네이티브 커넥터는 아직 성숙 단계다. 기존 분석 스택을 그대로 쓰려면 DuckDB를 브릿지로 사용하는 패턴이 현실적이다.


정리

Lance 2.x는 "더 나은 Parquet"가 아니라 AI 워크로드가 요구하는 다른 접근법이다. 랜덤 접근, 내장 버전 관리, 멀티모달 지원, 벡터 인덱스 통합이 설계의 첫 번째 원칙이다.

운영자에게 실용적인 판단 기준은 단순하다. 전체 스캔과 SQL 분석이 중심이면 Iceberg+Parquet을 유지한다. 훈련 데이터 배치 샘플링, 벡터 검색, 실험 재현이 중심이면 Lance가 적합하다. 두 가지를 모두 쓰는 팀은 Iceberg(분석)와 Lance(AI)를 DuckDB로 연결하는 패턴을 선택하고 있다.

References