본문으로 건너뛰기
clickhouse검색
핵심 구조

스킵 인덱스와 프로젝션 (minmax, set, bloom filter, 전문 인덱스, 벡터 인덱스)

정렬 키로 걸러지지 않는 조건을 돕는 데이터 스킵 인덱스의 종류와 적용 기준, 다른 정렬 순서나 사전 집계를 같은 테이블 안에 두는 프로젝션, 그리고 전문 검색과 벡터 유사도 인덱스의 현재 상태를 정리합니다.

스킵 인덱스프로젝션bloom_filterminmax전문 검색벡터 검색2026-10 기준, 약 12분상태 표시 설명

스킵 인덱스는 "건너뛰기" 인덱스

ClickHouse의 데이터 스킵 인덱스(data skipping index)는 B-tree처럼 행을 찾아 주지 않습니다. 그래뉼 몇 개(GRANULARITY)마다 요약 정보를 저장해 두고, 쿼리 조건에 맞을 리 없는 블록을 건너뛰게 합니다. 그래서 정렬 키와 상관없는 열의 필터에서, 그 값이 테이블 전체에 흩어져 있지 않고 군집해 있을 때 효과가 큽니다. 모든 블록에 그 값이 하나씩 있으면 아무 블록도 건너뛰지 못합니다.

ALTER TABLE events ADD INDEX idx_url url TYPE bloom_filter(0.01) GRANULARITY 4;
ALTER TABLE events MATERIALIZE INDEX idx_url;  -- 기존 파트에도 적용

GRANULARITY 4면 그래뉼 4개(기본 32,768행)마다 인덱스 항목 하나입니다. 숫자가 작을수록 정밀하지만 인덱스가 커집니다.

종류와 고르는 기준

타입저장하는 것맞는 조건맞는 열
minmax블록의 최소, 최대=, <, >, BETWEEN, IN정렬 키와 상관관계 있는 숫자, 날짜 (예: 정렬 키가 ts인 테이블의 created_at)
set(N)블록의 고유 값 N개까지=, IN블록 안 종류가 적은 열 (상태, 카테고리). N을 넘는 블록은 인덱스가 비어 통과
bloom_filter(p)블룸 필터 (오탐률 p)=, IN, has고카디널리티 등가 조건 (URL, 세션 ID, 배열 요소)
tokenbf_v1(size, hashes, seed)토큰(단어) 블룸 필터hasToken, LIKE '%word%', =로그 메시지의 단어 검색
ngrambf_v1(n, size, hashes, seed)n-gram 블룸 필터LIKE '%부분%'한국어처럼 띄어쓰기 토큰이 안 맞는 부분 문자열 검색
text (전문 인덱스)역색인(inverted index)hasToken, hasAnyTokens, hasAllTokens로그 전문 검색. 아래 참고
vector_similarityHNSW 근사 최근접ORDER BY cosineDistance(...) LIMIT N임베딩 벡터

minmax를 가장 먼저

가장 싸고 자주 통합니다. 정렬 키가 (tenant_id, ts)인데 event_time으로도 필터한다면 둘은 거의 같은 순서로 증가하므로 minmax가 블록 대부분을 건너뜁니다. 반대로 user_id처럼 정렬 키와 무관하게 흩어진 값에는 쓸모가 없습니다.

set은 종류가 적을 때

set(100)은 블록마다 고유 값을 100개까지 저장합니다. 상태 열이 블록마다 5종류 정도면 WHERE status = 'error'에서 error가 없는 블록을 건너뜁니다. 블록마다 종류가 100개를 넘으면 그 블록은 "모름"으로 처리되어 읽습니다.

bloom_filter는 고카디널리티 등가 조건

WHERE session_id = 'abc...' 같은 바늘 찾기에 좋습니다. 오탐률 0.01이면 블록의 1%는 잘못 읽히지만, 그래도 전체 스캔보다는 훨씬 적습니다. 범위 조건(>, <)에는 전혀 쓰이지 않습니다. 배열 열에 만들면 has(tags, 'vip')를 돕습니다.

ALTER TABLE events ADD INDEX idx_tags tags TYPE bloom_filter(0.01) GRANULARITY 2;
ALTER TABLE events ADD INDEX idx_status status TYPE set(50) GRANULARITY 4;
ALTER TABLE events ADD INDEX idx_created created_at TYPE minmax GRANULARITY 1;

주의 인덱스를 만든다고 자동으로 빨라지지 않습니다. 추가 전후에 EXPLAIN indexes = 1로 Skip 단계에서 Granules가 실제로 줄었는지 확인하세요. 줄지 않으면 인덱스 유지 비용(머지마다 재계산, 디스크)만 드는 셈이니 DROP INDEX 합니다.

EXPLAIN indexes = 1
SELECT count() FROM events WHERE has(tags, 'vip') AND ts >= today() - 30;
-- 결과의 Skip 항목: Name: idx_tags, Type: bloom_filter, Parts: 20/40, Granules: 350/9800

가설 인덱스로 미리 재기 (26.x)

인덱스를 실제로 만들어 물리화하기 전에 "있다고 치고" 효과를 계산해 보는 가설(hypothetical) 스킵 인덱스가 26.x에 들어왔습니다. 수 TB 테이블에서 MATERIALIZE INDEX를 돌리기 전에 후보를 걸러낼 때 유용합니다. 사용 방법과 설정은 버전에 따라 다르므로 공식 문서의 EXPLAIN 항목을 확인하세요.

전문(full-text) 인덱스

로그 메시지 검색을 위한 역색인입니다. 토크나이저(splitByNonAlpha, splitByString, ngrams, array)를 골라 단어 단위로 색인하고, hasToken, hasAnyTokens, hasAllTokens 함수로 검색합니다. 블룸 필터 계열보다 정확하고 빠르지만 인덱스 크기가 큽니다. 26.2 부터 GA 라서 따로 설정을 켤 필요가 없고(그 전 버전은 allow_experimental_full_text_index 같은 설정이 필요했습니다), 26.4 부터는 인덱스에서 직접 읽는 방식이 기본입니다. 구문 검색(support_phrase_search)만 아직 Exp 입니다.

CREATE TABLE logs
(
    ts      DateTime,
    message String,
    INDEX idx_msg message TYPE text(tokenizer = 'splitByNonAlpha') GRANULARITY 1
)
ENGINE = MergeTree ORDER BY ts;

SELECT count() FROM logs WHERE hasToken(message, 'timeout');

한국어 로그는 공백 토큰이 잘 맞지 않으므로 ngrams(3) 토크나이저나 ngrambf_v1을 검토합니다. ClickStack이 로그 검색에 이런 인덱스를 씁니다.

벡터 유사도 인덱스

임베딩(Array(Float32))에 HNSW 기반 근사 최근접 탐색 인덱스를 만들 수 있습니다. 인덱스가 없으면 cosineDistance로 전체를 읽는 정확 탐색이 되고, 수천만 행까지는 그것만으로도 충분히 빠른 경우가 많습니다. 인덱스는 수억 행 이상에서 지연을 줄여 줍니다.

CREATE TABLE docs
(
    id   UInt64,
    emb  Array(Float32),
    INDEX idx_emb emb TYPE vector_similarity('hnsw', 'cosineDistance', 1536)
)
ENGINE = MergeTree ORDER BY id;

SELECT id, cosineDistance(emb, [/* 질의 벡터 */]) AS d
FROM docs
ORDER BY d
LIMIT 10;
  • 벡터 인덱스는 파트마다 만들어지므로 머지 때 다시 빌드되고, 빌드에 CPU와 메모리를 많이 씁니다. 적재 후 머지가 끝나는 시점을 감안해 운영합니다.
  • 양자화(bf16, f16, i8) 옵션으로 인덱스 크기를 줄일 수 있습니다.
  • 벡터 검색은 Beta 단계를 거쳐 왔고, Cloud에서는 GA로 제공되는 구성이 있습니다. 상세 상태는 기능 상태판과 공식 문서를 함께 보세요.

프로젝션: 같은 테이블 안의 다른 정렬

프로젝션(projection)은 테이블의 각 파트 안에 다른 정렬 순서나 사전 집계 결과를 같이 저장하는 기능입니다. 머티리얼라이즈드 뷰와 달리 테이블이 따로 없고, INSERT와 머지와 뮤테이션이 자동으로 함께 처리되며, 쿼리 최적화기가 알아서 프로젝션을 고릅니다.

-- 정렬 키는 (tenant_id, ts) 인데 user_id 로 자주 찍는 쿼리가 있을 때
ALTER TABLE events ADD PROJECTION p_user
(
    SELECT * ORDER BY user_id, ts
);
ALTER TABLE events MATERIALIZE PROJECTION p_user;

-- 사전 집계 프로젝션
ALTER TABLE events ADD PROJECTION p_daily
(
    SELECT toDate(ts) AS d, event, count(), sum(duration)
    GROUP BY d, event
);
비교프로젝션머티리얼라이즈드 뷰
저장 위치원본 파트 안 (디스크는 추가로 듦)별도 테이블
선택최적화기가 자동쿼리에서 MV 대상 테이블을 직접 지정
조인, 필터, 다른 엔진불가 (같은 테이블 열만)가능
삭제, 뮤테이션자동 반영원본과 따로 관리
FINAL, 경량 삭제제한 있음 (deduplicate_merge_projection_mode 설정)없음

프로젝션은 "정렬 키를 하나 더 두고 싶다"는 요구에 가장 가볍게 답합니다. 반면 집계 결과를 다른 테이블로 모으거나 조인이 필요하면 머티리얼라이즈드 뷰입니다.

참고 ReplacingMergeTree 같은 중복 제거 엔진에 프로젝션을 두면 머지 때 프로젝션 일관성을 어떻게 유지할지 deduplicate_merge_projection_mode(throw, drop, rebuild)로 정해야 합니다. 기본값 throw는 그런 테이블에 프로젝션 추가를 거부합니다.

인덱스 운영

-- 인덱스 목록과 크기
SELECT table, name, type_full, granularity,
       formatReadableSize(data_compressed_bytes) AS size
FROM system.data_skipping_indices
WHERE table = 'events';

-- 프로젝션 목록
SELECT table, name, type, formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.projection_parts
WHERE active
GROUP BY table, name, type;

ALTER TABLE events DROP INDEX idx_status;
ALTER TABLE events DROP PROJECTION p_user;
  • 인덱스와 프로젝션은 모두 머지마다 다시 계산됩니다. 적재가 많은 테이블에 여러 개 두면 머지가 느려져 파트가 쌓일 수 있습니다. Too many parts 증상이 생기면 인덱스 수를 먼저 의심하세요.
  • MATERIALIZE INDEX와 MATERIALIZE PROJECTION은 뮤테이션입니다. 큰 테이블은 IN PARTITION으로 나눠 실행합니다.
  • 쿼리에서 특정 인덱스를 쓰지 않게 하려면 SETTINGS ignore_data_skipping_indices = 'idx_x', 프로젝션을 끄려면 optimize_use_projections = 0으로 비교 실험을 합니다.

결정 흐름

  1. 정렬 키로 해결되는가. 된다면 인덱스를 만들지 않습니다.
  2. 정렬 키와 상관관계 있는 숫자, 날짜 열인가. minmax.
  3. 종류가 적은 열의 등가 조건인가. set.
  4. 고카디널리티 등가 조건, 배열 포함 검색인가. bloom_filter.
  5. 텍스트 단어 검색인가. tokenbf_v1 또는 text.
  6. 다른 정렬 순서가 통째로 필요한가. 프로젝션, 그다음 MV.

공식 문서

이 문서에서