Projection을 통한 쿼리 성능 개선
🧯

Projection을 통한 쿼리 성능 개선

ClickHouse 분류
Core Architecture
Type
Introduction
작성자

Ken

Projection은 ClickHouse가 제공하는 쿼리 최적화 기능으로, 테이블 내에 사전 집계되거나 다른 정렬 키로 구성된 데이터 복사본을 물리적으로 저장하는 메커니즘입니다. 원본 테이블의 일부로 관리되며, 쿼리 실행 시 옵티마이저가 자동으로 가장 효율적인 데이터 레이아웃을 선택합니다.

주요 특징

자동 관리

Projection은 테이블의 일부로 관리되어 INSERT, UPDATE, DELETE 시 자동으로 동기화됩니다. 별도의 데이터 파이프라인이나 동기화 로직이 필요하지 않습니다.

투명한 쿼리 최적화

쿼리 작성 시 Projection의 존재를 의식할 필요가 없습니다. 옵티마이저가 자동으로 최적의 Projection을 선택하여 실행합니다.

다중 Projection 지원

하나의 테이블에 여러 Projection을 정의할 수 있어, 다양한 쿼리 패턴을 동시에 최적화할 수 있습니다.

장점

1. 단순한 운영 복잡도

Materialized View와 달리 별도의 테이블 관리가 불필요하며, 원본 테이블의 생명주기에 자동으로 종속됩니다.

2. 완벽한 데이터 일관성

INSERT와 동시에 Projection이 업데이트되므로 데이터 불일치 위험이 없습니다. Materialized View의 비동기 업데이트로 인한 일시적 불일치 문제가 발생하지 않습니다.

3. 자동 쿼리 최적화

개발자가 쿼리를 수정하지 않아도 옵티마이저가 자동으로 Projection을 활용합니다. 기존 쿼리 코드 변경 없이 성능 개선이 가능합니다.

4. 효율적인 스토리지 관리

Projection 데이터는 원본 테이블의 파트 구조 내에 저장되어 파티션 관리, TTL, OPTIMIZE 등의 작업이 자동으로 적용됩니다.

5. 백업 및 복구 단순화

테이블을 백업하면 Projection도 함께 백업되며, 별도의 복구 절차가 필요하지 않습니다.

단점

1. 스토리지 오버헤드

Projection은 데이터의 물리적 복사본을 생성하므로 추가 디스크 공간이 필요합니다. 실제 테스트에서 원본 테이블 210.64 MiB에 대해 Projection이 약 32 MiB 추가로 사용되었습니다.

2. INSERT 성능 영향

데이터 삽입 시 모든 Projection을 동시에 업데이트해야 하므로, Projection이 많을수록 INSERT 성능이 저하됩니다.

3. 제한적인 집계 표현식

Projection의 SELECT 절에서 사용할 수 있는 집계 함수와 표현식이 제한적입니다. 복잡한 윈도우 함수나 조인은 지원되지 않습니다.

4. 쿼리 패턴 종속성

Projection은 특정 쿼리 패턴에 최적화되므로, 예상하지 못한 쿼리 패턴에는 도움이 되지 않을 수 있습니다.

5. 디버깅 어려움

옵티마이저가 자동으로 Projection을 선택하므로, 예상과 다른 실행 계획이 나올 경우 문제 파악이 어려울 수 있습니다.

Materialized View와의 차이점

관리 모델

Projection: 원본 테이블의 일부로 관리됩니다. DROP TABLE 시 Projection도 함께 삭제되며, 별도의 생명주기 관리가 불필요합니다.

Materialized View: 독립적인 테이블로 존재합니다. 원본 테이블과 별도로 관리해야 하며, 삭제 시에도 개별적으로 처리해야 합니다.

데이터 일관성

Projection: INSERT와 동시에 업데이트되어 완벽한 일관성을 보장합니다.

Materialized View: 비동기적으로 업데이트되므로 짧은 시간 동안 원본과 불일치할 수 있습니다.

쿼리 투명성

Projection: 옵티마이저가 자동으로 선택합니다. 개발자는 원본 테이블에 쿼리하면 됩니다.

-- Projection이 자동으로 사용됨
SELECT category, sum(revenue)
FROM sales_events
GROUP BY category;

Materialized View: 명시적으로 타겟 테이블을 지정해야 합니다.

-- 타겟 테이블을 명시적으로 쿼리
SELECT category, sum(total_revenue)
FROM category_analysis_mv
GROUP BY category;

스토리지 위치

Projection: 원본 테이블의 파트 구조 내에 저장됩니다.

Materialized View: 완전히 별도의 테이블과 파트로 저장됩니다.

유연성

Projection: 집계와 정렬 변경에 제한적입니다. 복잡한 변환이나 조인은 불가능합니다.

Materialized View: 거의 모든 SQL 표현식을 사용할 수 있습니다. 조인, 서브쿼리, 복잡한 변환 모두 가능합니다.

성능 비교 요약

항목
Projection
Materialized View
쿼리 성능
매우 빠름 (자동 최적화)
매우 빠름 (직접 쿼리)
INSERT 성능
다소 느림 (동기)
빠름 (비동기)
스토리지 효율
높음 (통합 관리)
중간 (별도 저장)
관리 복잡도
낮음
중간
데이터 일관성
완벽
일시적 불일치 가능

성능 검증 실험

코드:

GitHub clickhouse-hols/workload/projection at main · litkhai/clickhouse-holsGitHub clickhouse-hols/workload/projection at main · litkhai/clickhouse-hols

테스트 환경

  • 데이터셋: 1,000만 건의 이벤트 데이터
  • 테이블 크기: 210.64 MiB
  • 컬럼 수: 18개 (다양한 차원 및 측정값)
  • 시간 범위: 2024년 1월 ~ 4월

테스트 케이스 1: 카테고리별 집계 쿼리

쿼리 패턴:

SELECT category, country, sum(revenue), sum(quantity), count()
FROM sales_events
WHERE toYYYYMM(event_time) = 202401
GROUP BY category, country

결과:

실행 방식
읽은 행 수
읽은 데이터
쿼리 시간
메모리 사용
Projection OFF
2,678,400
106.52 MiB
313 ms
171.09 MiB
Projection ON
2,678,400
106.52 MiB
129 ms
180.99 MiB
Materialized View
120
14.03 KiB
123 ms
12.38 MiB

분석:

  • Projection 활성화 시 쿼리 시간이 58.8% 감소했습니다.
  • Materialized View는 읽은 데이터량이 현저히 적어 메모리 효율성이 가장 높았습니다.
  • Projection은 원본 테이블 접근이 필요한 경우에도 최적화된 경로를 제공했습니다.

테스트 케이스 2: 브랜드 일별 통계

쿼리 패턴:

SELECT brand, customer_segment, sum(revenue), count(DISTINCT user_id)
FROM sales_events
WHERE toDate(event_time) BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY brand, customer_segment

이 쿼리에서 brand_daily_stats Projection이 자동으로 활용되어 성능이 크게 향상되었습니다.

스토리지 오버헤드 분석

원본 테이블: 210.64 MiB (10,000,000 행)

Projection 스토리지:
- category_analysis: ~12 KB (파티션당 ~1 KB)
- brand_daily_stats: ~32 MiB (세분화된 집계)

Materialized View: 1.89 KiB (40 집계 행)

해석:

  • category_analysis Projection은 높은 집계 수준으로 매우 작은 공간만 사용했습니다.
  • brand_daily_stats Projection은 세분화된 집계로 인해 더 많은 공간을 사용했지만, 원본 대비 15% 수준입니다.
  • Materialized View는 가장 적은 스토리지를 사용했지만, 이는 단일 집계 뷰만 생성했기 때문입니다.

실행 계획 분석

Projection 활성화 시:

ReadFromMergeTree (projection_test.sales_events)
  Indexes:
    - Partition 인덱스로 3/12 파트만 스캔
    - Primary Key로 327/1222 그래뉼만 스캔

옵티마이저가 파티션 프루닝과 Primary Key 인덱스를 효과적으로 활용하여 불필요한 데이터 읽기를 최소화했습니다.

실전 사용 가이드

Projection 생성 구문

ALTER TABLE table_name
ADD PROJECTION projection_name
(
    SELECT
        dimension1,
        dimension2,
        sum(metric1) as total_metric1,
        avg(metric2) as avg_metric2,
        count() as event_count
    GROUP BY dimension1, dimension2
);

Projection 구체화

-- 비동기 구체화 (권장)
ALTER TABLE table_name
MATERIALIZE PROJECTION projection_name;

-- 동기 구체화 (테스트용)
ALTER TABLE table_name
MATERIALIZE PROJECTION projection_name
SETTINGS mutations_sync = 1;

Projection 활성화/비활성화

-- 전역 활성화
SET allow_experimental_projection_optimization = 1;

-- 쿼리별 비활성화
SELECT ...
SETTINGS allow_experimental_projection_optimization = 0;

Projection 삭제

ALTER TABLE table_name
DROP PROJECTION projection_name;

모범 사례

1. 자주 사용되는 집계 패턴 식별 쿼리 로그를 분석하여 반복적으로 실행되는 GROUP BY 패턴을 찾습니다.

SELECT
    extractAll(query, 'GROUP BY ([^\s]+)')[1] as group_by_clause,
    count() as query_count
FROM system.query_log
WHERE type = 'QueryFinish' AND query LIKE '%GROUP BY%'
GROUP BY group_by_clause
ORDER BY query_count DESC
LIMIT 10;

2. 카디널리티 고려 집계 결과의 카디널리티가 너무 높으면 Projection의 효과가 제한적입니다. 일반적으로 집계 후 행 수가 원본의 10% 이하일 때 효과적입니다.

3. 파티션 키와 조화 Projection의 GROUP BY 키가 테이블의 파티션 키와 잘 맞으면 파티션 프루닝과 시너지 효과가 있습니다.

4. 시간 범위 쿼리 최적화 시계열 데이터에서는 날짜/시간을 집계 키로 포함하면 효과적입니다.

ADD PROJECTION time_series_stats
(
    SELECT
        toStartOfHour(event_time) as hour,
        metric_name,
        sum(value),
        avg(value),
        count()
    GROUP BY hour, metric_name
);

5. 다중 Projection 전략 서로 다른 쿼리 패턴을 위해 여러 Projection을 생성할 수 있지만, 3~4개 이하로 제한하는 것이 좋습니다.

사용 시나리오

Projection이 적합한 경우

실시간 대시보드: 동일한 집계 쿼리가 반복적으로 실행되는 경우

-- 실시간 매출 대시보드
ADD PROJECTION realtime_sales
(SELECT toStartOfMinute(event_time) as minute,
        sum(revenue), count()
 GROUP BY minute)

다차원 분석: 여러 차원의 조합으로 집계하는 OLAP 쿼리

ADD PROJECTION multidim_analysis
(SELECT region, category, brand,
        sum(revenue), count(DISTINCT user_id)
 GROUP BY region, category, brand)

시계열 집계: 시간 기반 롤업이 필요한 경우

ADD PROJECTION hourly_metrics
(SELECT toStartOfHour(timestamp) as hour,
        metric_type, avg(value), max(value)
 GROUP BY hour, metric_type)

Materialized View가 더 적합한 경우

복잡한 변환 로직: 조인, 서브쿼리, 윈도우 함수가 필요한 경우

비동기 처리 선호: INSERT 성능이 중요하고 약간의 데이터 지연이 허용되는 경우

독립적인 데이터 수명주기: 집계 데이터를 원본과 다르게 관리해야 하는 경우

외부 테이블 참조: 여러 테이블의 조인 결과를 저장해야 하는 경우

결론

ClickHouse Projection은 쿼리 성능 최적화를 위한 강력한 도구로, 특히 반복적인 집계 패턴에서 탁월한 효과를 보입니다. 실험 결과 쿼리 시간을 최대 58.8% 단축시키면서도, 관리 복잡도를 크게 낮출 수 있었습니다.

Projection을 선택해야 하는 경우:

  • 운영 단순성과 데이터 일관성이 중요한 경우
  • 자주 사용되는 집계 패턴이 명확한 경우
  • 테이블 구조가 안정적이고 스키마 변경이 적은 경우

Materialized View를 선택해야 하는 경우:

  • 복잡한 변환 로직이 필요한 경우
  • INSERT 성능이 매우 중요한 경우
  • 집계 데이터를 독립적으로 관리해야 하는 경우

실무에서는 두 기능을 상황에 맞게 조합하여 사용하는 것이 최선의 전략입니다. 간단한 집계는 Projection으로, 복잡한 변환은 Materialized View로 처리하면 최적의 성능과 관리 효율성을 동시에 달성할 수 있습니다.

참고 문서:

  • ClickHouse Projections Official Documentation
  • Understanding Projections
  • Performance Optimization with Projections

사용 코드:

clickhouse-hols/workload/projection at main · litkhai/clickhouse-hols

ClickHouse Hands-on Labs . Contribute to litkhai/clickhouse-hols development by creating an account on GitHub.

clickhouse-hols/workload/projection at main · litkhai/clickhouse-hols