분석 가속화하기

분석 가속화하기

이전 가이드에서 데이터 카탈로그에 ClickHouse를 연결하고 오픈 테이블 포맷을 직접 조회했어요. 이번에는 카탈로그의 데이터를 MergeTree 테이블로 로드해서 성능을 극대화하는 방법을 살펴봐요.

출처: 문서

본문

이전 섹션에서 데이터 카탈로그에 ClickHouse를 연결하고 오픈 테이블 포맷을 직접 조회했어요. 데이터를 원위치에서 조회하는 건 편리하지만, 오픈 테이블 포맷은 대시보드와 운영 보고를 뒷받침하는 저지연·고동시성 워크로드에 최적화되어 있지 않아요. 이런 사용 사례에는 데이터를 ClickHouse의 MergeTree 엔진으로 로드하는 게 훨씬 뛰어난 성능을 제공해요. MergeTree는 오픈 테이블 포맷을 직접 읽는 것보다 다음과 같은 여러 장점이 있어요:

  • 스파스 기본 인덱스(Sparse primary index) - 선택한 키를 기준으로 디스크에 데이터를 정렬해서, 쿼리 중 관련 없는 큰 범위의 행을 건너뛸 수 있어요.
  • 향상된 데이터 타입 - JSON, LowCardinality, Enum 같은 타입을 네이티브로 지원해서 더 압축된 저장과 빠른 처리가 가능해요.
  • 건너뛰기 인덱스(Skip indices)전문(full-text) 인덱스 - 쿼리의 필터 조건과 일치하지 않는 granule을 건너뛰게 해주는 보조 인덱스 구조로, 특히 텍스트 검색 워크로드에 효과적이에요.
  • 자동 압축이 포함된 빠른 삽입 - ClickHouse는 고처리량 삽입을 위해 설계되었고, 백그라운드에서 데이터 파트를 자동으로 병합해요. 이는 오픈 테이블 포맷의 압축(compaction)과 유사해요.
  • 동시 읽기에 최적화 - MergeTree의 컬럼형 저장 레이아웃과 여러 캐싱 레이어가 결합되어 높은 동시성을 가진 실시간 분석 워크로드를 지원해요. 오픈 테이블 포맷은 이를 위해 설계되지 않았어요.

이 가이드에서는 INSERT INTO SELECT를 사용해 카탈로그에서 MergeTree 테이블로 데이터를 로드해 더 빠른 분석을 수행하는 방법을 보여줄게요.

카탈로그에 연결하기

이전 가이드와 동일한 Unity Catalog 연결을 사용하고, Iceberg REST 엔드포인트로 연결할게요:

SET allow_database_iceberg = 1;

CREATE DATABASE unity
ENGINE = DataLakeCatalog('https://<workspace-id>.cloud.databricks.com/api/2.1/unity-catalog/iceberg-rest')
SETTINGS catalog_type = 'rest', catalog_credential = '<client-id>:<client-secret>', warehouse = 'workspace',
oauth_server_uri = 'https://<workspace-id>.cloud.databricks.com/oidc/v1/token', auth_scope = 'all-apis,sql';

테이블 나열하기

SHOW TABLES FROM unity
┌─name───────────────────────────────────────────────┐
│ unity.logs                                         │
│ unity.single_day_log                               │
└────────────────────────────────────────────────────┘

스키마 살펴보기

SHOW CREATE TABLE unity.`icebench.single_day_log`
CREATE TABLE unity.`icebench.single_day_log`
(
    `pull_request_number` Nullable(Int64),
    `commit_sha` Nullable(String),
    `check_start_time` Nullable(DateTime64(6, 'UTC')),
    `check_name` Nullable(String),
    `instance_type` Nullable(String),
    `instance_id` Nullable(String),
    `event_date` Nullable(Date32),
    `event_time` Nullable(DateTime64(6, 'UTC')),
    `event_time_microseconds` Nullable(DateTime64(6, 'UTC')),
    `thread_name` Nullable(String),
    `thread_id` Nullable(Decimal(20, 0)),
    `level` Nullable(String),
    `query_id` Nullable(String),
    `logger_name` Nullable(String),
    `message` Nullable(String),
    `revision` Nullable(Int64),
    `source_file` Nullable(String),
    `source_line` Nullable(Decimal(20, 0)),
    `message_format_string` Nullable(String)
)
ENGINE = Iceberg('s3://...')

이 테이블에는 ClickHouse CI 테스트 실행에서 나온 약 2억 8300만 개의 로그 행이 들어 있어요. 분석 성능을 살펴보기에 현실적인 데이터셋이에요.

SELECT count()
FROM unity.`icebench.single_day_log`
┌───count()─┐
│ 282634391 │ -- 282.63 million
└───────────┘

1 row in set. Elapsed: 1.265 sec.

데이터 레이크 테이블 조회하기

스레드 이름과 인스턴스 타입으로 로그를 필터링하고, 메시지 텍스트에서 오류를 검색한 뒤 logger로 그룹화하는 쿼리를 실행해 볼게요:

SELECT
    logger_name,
    count() AS c
FROM icebench.`icebench.single_day_log`
WHERE (thread_name = 'TCPHandler')
    AND (instance_type = 'm6i.4xlarge')
    AND hasToken(message, 'error')
GROUP BY logger_name
ORDER BY c DESC
LIMIT 5
┌─logger_name──────────────┬────c─┐
│ executeQuery             │ 6907 │
│ TCPHandler               │ 4145 │
│ TCP-Session              │  790 │
│ PostgreSQLConnectionPool │  530 │
│ ContextAccess (default)  │  392 │
└──────────────────────────┴──────┘

5 rows in set. Elapsed: 8.921 sec. Processed 282.63 million rows, 5.42 GB (31.68 million rows/s., 607.26 MB/s.)
Peak memory usage: 4.35 GiB.

이 쿼리는 거의 9초가 걸려요. ClickHouse가 오브젝트 스토리지의 모든 Parquet 파일을 전체 테이블 스캔해야 하기 때문이에요. 파티셔닝으로 성능을 개선할 수 있지만, logger_name 같은 컬럼은 카디널리티가 너무 높아서 효과적으로 파티셔닝하기 어려울 수 있어요. 또한 데이터를 더 줄여줄 Text 인덱스 같은 인덱스도 없어요. 바로 여기서 MergeTree가 두각을 나타내요.

MergeTree에 데이터 로드하기

최적화된 테이블 만들기

스키마를 최적화하려고 노력하며 MergeTree 테이블을 만들게요. Iceberg 스키마와의 몇 가지 핵심 차이점을 확인해 볼게요:

  • Nullable 래퍼 없음 - Nullable을 제거하면 저장 효율과 쿼리 성능이 좋아져요.
  • level, instance_type, thread_name, check_name 컬럼에 LowCardinality(String) - 고유 값이 적은 컬럼을 사전 인코딩해서 압축을 개선하고 필터링을 빠르게 해줘요.
  • message 컬럼에 전문(full-text) 인덱스 - hasToken(message, 'error') 같은 토큰 기반 텍스트 검색을 가속화해요.
  • (instance_type, thread_name, toStartOfMinute(event_time)) 키의 ORDER BY - 일반적인 필터 패턴에 맞춰 디스크에 데이터를 정렬해서 스파스 기본 인덱스가 관련 없는 granule을 건너뛸 수 있게 해요.
SET enable_full_text_index = 1;

CREATE TABLE single_day_log
(
    `pull_request_number` Int64,
    `commit_sha` String,
    `check_start_time` DateTime64(6, 'UTC'),
    `check_name` LowCardinality(String),
    `instance_type` LowCardinality(String),
    `instance_id` String,
    `event_date` Date32,
    `event_time` DateTime64(6, 'UTC'),
    `event_time_microseconds` DateTime64(6, 'UTC'),
    `thread_name` LowCardinality(String),
    `thread_id` Decimal(20, 0),
    `level` LowCardinality(String),
    `query_id` String,
    `logger_name` String,
    `message` String,
    `revision` Int64,
    `source_file` String,
    `source_line` Decimal(20, 0),
    `message_format_string` String,
    INDEX text_idx(message) TYPE text(tokenizer = splitByNonAlpha)
)
ENGINE = MergeTree
ORDER BY (instance_type, thread_name, toStartOfMinute(event_time))

카탈로그에서 데이터 삽입하기

INSERT INTO SELECT를 사용해 데이터 레이크 테이블의 약 3억 행을 ClickHouse 테이블로 로드할게요:

INSERT INTO single_day_log SELECT * FROM icebench.`icebench.single_day_log`
282634391 rows in set. Elapsed: 237.680 sec. Processed 282.63 million rows, 5.42 GB (1.19 million rows/s., 22.79 MB/s.)
Peak memory usage: 18.62 GiB.

쿼리 다시 실행하기

이제 MergeTree 테이블에 동일한 쿼리를 실행하면 성능이 극적으로 개선되는 걸 볼 수 있어요:

SELECT
    logger_name,
    count() AS c
FROM single_day_log
WHERE (thread_name = 'TCPHandler')
    AND (instance_type = 'm6i.4xlarge')
    AND hasToken(message, 'error')
GROUP BY logger_name
ORDER BY c DESC
LIMIT 5
┌─logger_name──────────────┬────c─┐
│ executeQuery             │ 6907 │
│ TCPHandler               │ 4145 │
│ TCP-Session              │  790 │
│ PostgreSQLConnectionPool │  530 │
│ ContextAccess (default)  │  392 │
└──────────────────────────┴──────┘

5 rows in set. Elapsed: 0.220 sec. Processed 13.84 million rows, 2.85 GB (62.97 million rows/s., 12.94 GB/s.)
Peak memory usage: 1.12 GiB.

이제 동일한 쿼리가 0.22초 만에 완료돼요. 약 40배 빨라진 거예요. 이 개선을 이끄는 두 가지 핵심 최적화가 있어요:

  • 스파스 기본 인덱스 - ORDER BY (instance_type, thread_name, ...) 키 덕분에 ClickHouse가 instance_type = 'm6i.4xlarge'thread_name = 'TCPHandler'와 일치하는 granule로 바로 점프해서, 처리하는 행이 2억 8300만 개에서 1400만 개로 줄어들어요.
  • 전문 인덱스 - message 컬럼의 text_idx 인덱스 덕분에 hasToken(message, 'error')가 모든 메시지 문자열을 스캔하는 대신 인덱스를 통해 해결되어, ClickHouse가 읽어야 하는 데이터를 더 줄여줘요.

그 결과 실시간 대시보드를 안정적으로 구동할 수 있는 쿼리가 완성돼요. 오브젝트 스토리지의 Parquet 파일을 조회하는 방식으로는 따라올 수 없는 규모와 지연 시간이에요.

더 알아보기 (Learn more)