Iceberg 테이블 엔진
Iceberg 테이블 엔진
기존 Iceberg 테이블에 직접 접근하려면 Iceberg 테이블 함수를 사용해요. 영구적인 ClickHouse 테이블이 필요하거나, 쓰기 가능한 백엔드에 명시적 스키마로 새로운 독립형 Iceberg 테이블을 만들고 싶다면 Iceberg 테이블 엔진을 사용해요.
Iceberg 테이블 엔진은 사용할 수 있지만 제약이 있을 수 있어요. ClickHouse는 원래 외부에서 스키마가 변경되는 테이블을 지원하도록 설계되지 않았기 때문에 Iceberg 테이블 엔진의 기능에 영향을 줄 수 있어요. 그 결과 일반 테이블에서 동작하는 일부 기능을 사용할 수 없거나 제대로 동작하지 않을 수 있고, 특히 이전 analyzer를 사용할 때 그렇답니다.
이 엔진은 Amazon S3, Azure, HDFS, 그리고 로컬에 저장된 Apache Iceberg 테이블과의 데이터 통합을 제공해요.
출처: 문서
본문
테이블 생성하기
명시적 스키마 없이 사용하면 Iceberg 테이블이 이미 스토리지에 존재해야 해요. 쓰기 가능한 백엔드에 새로운 독립형 Iceberg 테이블을 만들려면 CREATE TABLE 문에 스키마를 지정해줘요.
CREATE TABLE iceberg_table_s3
ENGINE = IcebergS3(url, [, NOSIGN | access_key_id, secret_access_key, [session_token]], format, [,compression], [,extra_credentials])
CREATE TABLE iceberg_table_azure
ENGINE = IcebergAzure(connection_string|storage_account_url, container_name, blobpath, [account_name, account_key, format, compression])
CREATE TABLE iceberg_table_hdfs
ENGINE = IcebergHDFS(path_to_table, [,format] [,compression_method])
CREATE TABLE iceberg_table_local
ENGINE = IcebergLocal(path_to_table, [,format] [,compression_method])
엔진 인자
인자에 대한 설명은 S3, AzureBlobStorage, HDFS, File 엔진의 인자 설명과 각각 일치해요. format은 Iceberg 테이블의 데이터 파일 형식을 의미해요.
IcebergS3의 경우 선택적 extra_credentials 매개변수로 ClickHouse Cloud에서 역할 기반 접근을 위한 role_arn을 전달할 수 있어요. 구성 단계는 Secure S3를 참고해요.
엔진 매개변수는 Named Collections로 지정할 수도 있어요.
예시
CREATE TABLE iceberg_table ENGINE=IcebergS3('http://test.s3.amazonaws.com/clickhouse-bucket/test_table', 'test', 'test')
named collections 사용하기:
<clickhouse>
<named_collections>
<iceberg_conf>
<url>http://test.s3.amazonaws.com/clickhouse-bucket/</url>
<access_key_id>test</access_key_id>
<secret_access_key>test</secret_access_key>
</iceberg_conf>
</named_collections>
</clickhouse>
CREATE TABLE iceberg_table ENGINE=IcebergS3(iceberg_conf, filename = 'test_table')
별칭(Aliases)
Iceberg 테이블 엔진은 disk 설정에서 스토리지 백엔드를 자동으로 감지해 IcebergS3, IcebergAzure, IcebergLocal로 각각 배포해요. disk가 지정되지 않으면 기본적으로 IcebergS3 구현을 사용해요.
데이터 타입
아래 표는 스키마 추론 중(읽기 목적) Iceberg 데이터 타입이 ClickHouse 데이터 타입으로 매핑되는 방식을 보여줘요.
기본(Primitive) 타입
| Iceberg type | ClickHouse type | Notes |
|---|---|---|
boolean |
Bool |
|
int |
Int32 |
|
long, bigint |
Int64 |
|
float |
Float32 |
|
double |
Float64 |
|
date |
Date32 |
|
time |
Int64 |
자정 이후 마이크로초 |
timestamp |
DateTime64(6) |
마이크로초, 타임존 없음 |
timestamptz |
DateTime64(6, 'UTC') |
마이크로초, UTC 타임존 |
timestamp_ns |
DateTime64(9) |
나노초, 타임존 없음 (Iceberg v3부터만) |
timestamptz_ns |
DateTime64(9, 'UTC') |
나노초, UTC 타임존 (Iceberg v3부터만) |
string, binary |
String |
|
uuid |
UUID |
|
fixed(N) |
FixedString(N) |
|
decimal(P, S) |
Decimal(P, S) |
복합(Complex) 타입
| Iceberg type | ClickHouse type |
|---|---|
list |
Array |
map |
Map |
struct |
Tuple |
스키마 및 쓰기 호환성 제약
위 데이터 타입 매핑은 읽기에 적용돼요. ClickHouse가 Iceberg 스키마를 만들거나 진화시키고 데이터를 쓸 때는 다음 제약이 적용돼요.
- ClickHouse는
Bool,Decimal,FixedString,Int8,UInt8,Int16,UInt16을 포함하는 Iceberg 스키마를 만들 수 없고, 해당 타입으로 컬럼을 추가하거나 수정할 수 없어요. 해당 연산은 지원하지 않는 타입 예외로 실패해요. 단, 기존 Iceberg 컬럼에Bool이나Decimal값을 삽입하는 것 자체는 막지 않아요 - ClickHouse가 Iceberg 스키마를 만들거나 진화시킬 때 모든
DateTime과DateTime64컬럼을 마이크로초 정밀도·타임존 없는 Icebergtimestamp로 매핑해요. ClickHouse는timestamptz,timestamp_ns,timestamptz_ns스키마 타입을 생성할 수 없으므로 더 높은 정밀도와 타임존 의미는 Iceberg 스키마에 표현되지 않아요 - ClickHouse는
decimal,fixed,timestamp_ns,timestamptz_ns컬럼을 직접 파티션 필드로 사용하는 Iceberg 테이블에 쓸 수 없어요. 해당 연산은 지원하지 않는 타입 예외로 실패해요 - ClickHouse가 경계를 직렬화할 수 없는 타입(
Bool,Decimal포함)을 포함하는 데이터 파일의 경우, ClickHouse는 Iceberg manifest 항목에서 모든 하한·상한 컬럼 경계를 생략해요. 컬럼 크기와 null 개수는 여전히 포함되고 데이터도 정확하지만, Reader는 해당 파일에 대해 manifest 레벨 min-max 가지치기(pruning)를 사용할 수 없어요
스키마 진화(Schema evolution)
ClickHouse는 시간이 지나면서 스키마가 진화한 Iceberg 테이블을 읽는 것을 지원해요. 컬럼이 추가·제거·재정렬되거나 필수에서 nullable로 변경된 테이블이 여기에 포함돼요. 또한 다음 타입 캐스팅을 지원해요.
- int -> long
- float -> double
- decimal(P, S) -> decimal(P’, S) where P’ > P.
현재 중첩 구조나 배열·맵 내부 요소의 타입을 바꾸는 것은 불가능해요. 동적 스키마 추론으로 생성한 뒤 스키마가 변경된 테이블을 읽으려면, 테이블 생성 시 allow_dynamic_metadata_for_data_lakes = true로 설정해주세요.
파티션 가지치기(Partition pruning)
ClickHouse는 Iceberg 테이블에 대한 SELECT 쿼리 중 파티션 가지치기를 지원해서, 관련 없는 데이터 파일을 건너뛰어 쿼리 성능을 최적화해요. 파티션 가지치기를 활성화하려면 use_iceberg_partition_pruning = 1로 설정해주세요. Iceberg 파티션 가지치기에 대한 자세한 내용은 https://iceberg.apache.org/spec/#partitioning를 참고해요.
타임 트래블(Time travel)
ClickHouse는 Iceberg 테이블에 대한 타임 트래블을 지원해서, 특정 타임스탬프나 스냅샷 ID로 과거 데이터를 조회할 수 있어요.
Manifest 파일 압축(Compaction)
시간이 지나면서 Iceberg 테이블에 빈번한 쓰기가 발생하면 현재 스냅샷의 manifest 목록에 작은 manifest 파일이 많이 쌓일 수 있어요. manifest 목록이 길어지면 데이터 파일을 발견하기 위해 모든 manifest 파일을 읽어야 하므로 쿼리 계획이 느려져요. ClickHouse는 OPTIMIZE TABLE ... MANIFEST 문으로 이러한 manifest 파일을 더 적고 큰 파일로 압축할 수 있어요.
OPTIMIZE TABLE example_table MANIFEST SETTINGS allow_experimental_iceberg_compaction = 1;
이 연산은 동일한 데이터 파일을 통합된 manifest 파일 집합으로 참조하는 새 스냅샷(replace 연산)을 만들어요. 데이터 파일은 다시 쓰이지 않고, 행이 추가·삭제·중복 제거되지도 않아요 — 단지 manifest 레이어만 재배열될 뿐이에요.
요구 사항 및 동작
- 이 기능은 실험적이며
allow_experimental_iceberg_compaction설정 뒤에 있어요. 설정을 활성화하지 않으면 문이 예외를 던져요 - 압축은 현재 스냅샷의 manifest 목록에 있는 manifest 파일 수가
iceberg_manifest_min_count_to_compact설정(기본100, Iceberg 테이블 속성commit.manifest.min-count-to-merge의 문서화된 기본값)으로 주어진 임계값을 초과할 때만 시도돼요. 현재 개수가 임계값 이하이면 압축을 건너뛰고 새 스냅샷을 만들지 않아요. 더 적극적으로 압축하려면 임계값을 더 낮게 설정해요 OPTIMIZE TABLE ... MANIFEST는 Iceberg 테이블에서만 지원돼요. 다른 테이블 엔진에 실행하면 예외가 발생해요OPTIMIZE TABLE ... MANIFEST는 Iceberg format-version 2 테이블에서만 지원돼요. format-version 1 테이블에 실행하면 예외가 발생하고, format-version 3 테이블에도 예외가 발생해요. v3 행 계보first_row_id메타데이터가 아직 manifest 재작성에서 왕복(round-trip)되지 않기 때문이에요OPTIMIZE TABLE ... MANIFEST는 파일별key_metadata를 포함하는 암호화된 Iceberg 테이블에서는 지원되지 않아요. manifest 재작성에서 이 암호화 메타데이터를 보존하는 것이 아직 구현되지 않아 문이NOT_IMPLEMENTED예외를 던져요
삭제된 행 처리
ClickHouse는 다음 삭제 방법을 사용하는 Iceberg 테이블 읽기를 지원해요.
- Position deletes
- Equality deletes (버전 25.8+부터 지원)
- Deletion vectors (v3에서 도입)
Deletion vector 지원은 읽기 전용이에요. ClickHouse는 deletion vector를 쓰거나, 업데이트하거나, 압축하지 않아요. ALTER TABLE ... DELETE와 ALTER TABLE ... UPDATE는 Iceberg format-version 3 테이블에서 지원되지 않아요.
기본 사용법
SELECT * FROM example_table ORDER BY 1
SETTINGS iceberg_timestamp_ms = 1714636800000
SELECT * FROM example_table ORDER BY 1
SETTINGS iceberg_snapshot_id = 3547395809148285433
참고: 같은 쿼리에서 iceberg_timestamp_ms와 iceberg_snapshot_id 매개변수를 둘 다 지정할 수 없어요.
중요한 고려 사항
- 스냅샷은 일반적으로 다음 경우에 생성돼요:
- 테이블에 새 데이터가 쓰일 때
- 일종의 데이터 압축이 수행될 때
- 스키마 변경은 일반적으로 스냅샷을 만들지 않아요 — 이는 스키마 진화를 겪은 테이블에 타임 트래블을 사용할 때 중요한 동작으로 이어져요
예시 시나리오
이 시나리오들은 외부 Iceberg Writer가 만든 스키마 변경을 설명하기 위해 Spark를 사용해요.
시나리오 1: 새 스냅샷 없는 스키마 변경
다음 연산 순서를 생각해보죠.
-- Create a table with two columns
CREATE TABLE IF NOT EXISTS spark_catalog.db.time_travel_example (
order_number int,
product_code string
)
USING iceberg
OPTIONS ('format-version'='2')
-- Insert data into the table
INSERT INTO spark_catalog.db.time_travel_example VALUES
(1, 'Mars')
ts1 = now() // A piece of pseudo code
-- Alter table to add a new column
ALTER TABLE spark_catalog.db.time_travel_example ADD COLUMN (price double)
ts2 = now()
-- Insert data into the table
INSERT INTO spark_catalog.db.time_travel_example VALUES (2, 'Venus', 100)
ts3 = now()
-- Query the table at each timestamp
SELECT * FROM spark_catalog.db.time_travel_example TIMESTAMP AS OF ts1;
+------------+------------+
|order_number|product_code|
+------------+------------+
| 1| Mars|
+------------+------------+
SELECT * FROM spark_catalog.db.time_travel_example TIMESTAMP AS OF ts2;
+------------+------------+
|order_number|product_code|
+------------+------------+
| 1| Mars|
+------------+------------+
SELECT * FROM spark_catalog.db.time_travel_example TIMESTAMP AS OF ts3;
+------------+------------+-----+
|order_number|product_code|price|
+------------+------------+-----+
| 1| Mars| NULL|
| 2| Venus|100.0|
+------------+------------+-----+
다른 타임스탬프에서의 쿼리 결과:
- ts1 & ts2에서: 원래 두 컬럼만 나타나요
- ts3에서: 세 컬럼 모두 나타나고, 첫 행의 price는 NULL이에요
시나리오 2: 과거와 현재 스키마의 차이
현재 시점의 타임 트래블 쿼리는 현재 테이블과 다른 스키마를 보여줄 수 있어요.
-- Create a table
CREATE TABLE IF NOT EXISTS spark_catalog.db.time_travel_example_2 (
order_number int,
product_code string
)
USING iceberg
OPTIONS ('format-version'='2')
-- Insert initial data into the table
INSERT INTO spark_catalog.db.time_travel_example_2 VALUES (2, 'Venus');
-- Alter table to add a new column
ALTER TABLE spark_catalog.db.time_travel_example_2 ADD COLUMN (price double);
ts = now();
-- Query the table at a current moment but using timestamp syntax
SELECT * FROM spark_catalog.db.time_travel_example_2 TIMESTAMP AS OF ts;
+------------+------------+
|order_number|product_code|
+------------+------------+
| 2| Venus|
+------------+------------+
-- Query the table at a current moment
SELECT * FROM spark_catalog.db.time_travel_example_2;
+------------+------------+-----+
|order_number|product_code|price|
+------------+------------+-----+
| 2| Venus| NULL|
+------------+------------+-----+
이것은 ALTER TABLE이 새 스냅샷을 만들지 않지만, 현재 테이블에 대해 Spark가 스냅샷이 아닌 최신 메타데이터 파일에서 schema_id 값을 가져오기 때문에 발생해요.
시나리오 3: 과거와 현재 스키마의 차이
두 번째는 타임 트래블을 하는 동안 데이터가 전혀 쓰이기 전 테이블의 상태를 얻을 수 없다는 점이에요.
-- Create a table
CREATE TABLE IF NOT EXISTS spark_catalog.db.time_travel_example_3 (
order_number int,
product_code string
)
USING iceberg
OPTIONS ('format-version'='2');
ts = now();
-- Query the table at a specific timestamp
SELECT * FROM spark_catalog.db.time_travel_example_3 TIMESTAMP AS OF ts; -- Finises with error: Cannot find a snapshot older than ts.
ClickHouse에서 동작은 Spark와 일관돼요. Spark Select 쿼리를 ClickHouse Select 쿼리로 바꿔 생각하면 같은 방식으로 동작해요.
메타데이터 파일 해석(Metadata file resolution)
ClickHouse에서 Iceberg 테이블 엔진을 사용할 때 시스템은 Iceberg 테이블 구조를 설명하는 올바른 metadata.json 파일을 찾아야 해요. 이 해석 과정이 어떻게 동작하는지 설명할게요.
후보 검색
- 직접 경로 지정:
iceberg_metadata_file_path를 설정하면 시스템은 이 정확한 경로를 Iceberg 테이블 디렉터리 경로와 결합해 사용해요- 이 설정이 제공되면 다른 모든 해석 설정은 무시돼요
- 테이블 UUID 일치:
iceberg_metadata_table_uuid가 지정되면 시스템은:metadata디렉터리의.metadata.json파일만 봐요- 지정한 UUID(대소문자 무시)와 일치하는
table-uuid필드를 포함하는 파일로 필터링해요
- 기본 검색:
- 위 설정이 모두 없으면
metadata디렉터리의 모든.metadata.json파일이 후보가 돼요
- 위 설정이 모두 없으면
가장 최근 파일 선택
위 규칙으로 후보 파일을 식별한 뒤, 시스템은 어느 것이 가장 최근인지 판단해요.
iceberg_recent_metadata_file_by_last_updated_ms_field가 활성화되면:last-updated-ms값이 가장 큰 파일이 선택돼요
- 그렇지 않으면:
- 버전 번호가 가장 높은 파일이 선택돼요
- (버전은
V.metadata.json또는V-uuid.metadata.json형식의 파일명에서V로 나타나요)
참고: 언급된 모든 설정(명시적으로 다르게 지정하지 않는 한)은 엔진 수준 설정이며 테이블 생성 시 지정해야 해요. 아래처럼 말이죠.
CREATE TABLE example_table ENGINE = Iceberg(
's3://bucket/path/to/iceberg_table'
) SETTINGS iceberg_metadata_table_uuid = '6f6f6407-c6a5-465f-a808-ea8900e35a38';
참고: Iceberg Catalog가 일반적으로 메타데이터 해석을 처리하지만, ClickHouse의 Iceberg 테이블 엔진은 S3에 저장된 파일을 Iceberg 테이블로 직접 해석하기 때문에 이러한 해석 규칙을 이해하는 것이 중요해요.
데이터 캐시
Iceberg 테이블 엔진과 테이블 함수는 S3, AzureBlobStorage, HDFS 스토리지와 동일하게 데이터 캐싱을 지원해요. 여기를 참고해요.
메타데이터 캐시
Iceberg 테이블 엔진과 테이블 함수는 manifest 파일, manifest 목록, metadata json의 정보를 저장하는 메타데이터 캐시를 지원해요. 캐시는 메모리에 저장돼요. 이 기능은 기본적으로 활성화되는 use_iceberg_metadata_files_cache 설정으로 제어돼요.
비동기 메타데이터 프리페칭(Asynchronous metadata prefetching)
Iceberg 테이블 생성 시 iceberg_metadata_async_prefetch_period_ms를 설정하면 비동기 메타데이터 프리페칭을 활성화할 수 있어요. 0(기본값)이거나 메타데이터 캐싱이 활성화되지 않으면 비동기 프리페칭은 비활성화돼요. 이 기능을 활성화하려면 0이 아닌 밀리초 값을 지정해야 해요. 이 값은 프리페칭 주기 사이의 간격을 나타내요.
활성화하면 서버는 원격 카탈로그를 나열하고 새 메타데이터 버전을 감지하는 반복적인 백그라운드 연산을 실행해요. 그런 다음 그것을 파싱하고 스냅샷을 재귀적으로 탐색해 활성 manifest 목록 파일과 manifest 파일을 가져와요. 이미 메타데이터 캐시에 있는 파일은 다시 다운로드되지 않아요. 각 프리페칭 주기가 끝나면 최신 메타데이터 스냅샷이 메타데이터 캐시에 있게 돼요.
CREATE TABLE example_table ENGINE = Iceberg(
's3://bucket/path/to/iceberg_table'
) SETTINGS
iceberg_metadata_async_prefetch_period_ms = 60000;
읽기 연산에서 비동기 메타데이터 프리페칭을 최대한 활용하려면 iceberg_metadata_staleness_ms 매개변수를 Query 또는 Session 매개변수로 지정해야 해요. 기본적으로(0 - 미지정) 각 쿼리 맥락에서 서버는 원격 카탈로그에서 최신 메타데이터를 가져와요. 메타데이터 지연에 대한 허용 오차를 지정하면 서버는 원격 카탈로그를 호출하지 않고 캐시된 메타데이터 스냅샷 버전을 사용할 수 있어요. 캐시에 메타데이터 버전이 있고 지정한 지연 창 안에 다운로드되었다면 쿼리를 처리하는 데 사용돼요. 그렇지 않으면 원격 카탈로그에서 최신 버전을 가져와요.
SELECT count() FROM icebench_table WHERE ...
SETTINGS iceberg_metadata_staleness_ms=120000
참고: 비동기 메타데이터 프리페칭은 ICEBERG_SCEDULE_POOL에서 실행돼요. 이는 활성 Iceberg 테이블의 백그라운드 연산을 위한 서버 측 스레드풀이에요. 이 스레드풀의 크기는 iceberg_background_schedule_pool_size 서버 구성 매개변수(기본 10)로 제어돼요.
참고: 비동기 프리페칭이 활성화되면 메타데이터 캐시 크기가 모든 활성 테이블의 최신 메타데이터 스냅샷을 온전히 담기에 충분해야 한다고 예상하고 있어요.