메인 콘텐츠로 건너뛰기

소개

많은 요인이 ClickHouse 쿼리 성능에 영향을 미칩니다. 대부분의 경우 핵심 요소는 ClickHouse가 쿼리의 WHERE 절 조건을 평가할 때 프라이머리 키를 사용할 수 있는지 여부입니다. 따라서 가장 일반적인 쿼리 패턴에 맞는 프라이머리 키를 선택하는 것은 효과적인 테이블 설계에 매우 중요합니다. 그렇더라도 프라이머리 키를 아무리 세심하게 조정하더라도 이를 효율적으로 활용할 수 없는 쿼리 사용 사례는 불가피하게 존재합니다. 사용자는 일반적으로 시계열(time series) 데이터에 ClickHouse를 사용하지만, 동시에 customer id, website URL 또는 product number와 같은 다른 비즈니스 차원을 기준으로 동일한 데이터를 분석하려는 경우도 많습니다. 이런 경우 WHERE 절 조건을 적용하기 위해 각 컬럼 값을 전체 스캔해야 할 수 있으므로 쿼리 성능이 상당히 저하될 수 있습니다. 이러한 상황에서도 ClickHouse는 여전히 비교적 빠르지만, 수백만 또는 수십억 개의 개별 값을 평가해야 하므로 “인덱스가 없는” 쿼리는 프라이머리 키를 기반으로 하는 쿼리보다 훨씬 느리게 실행됩니다. 전통적인 관계형 데이터베이스에서는 이 문제를 해결하는 한 가지 방법으로 테이블에 하나 이상의 “보조” 인덱스를 추가합니다. 이는 b-tree 구조로, 데이터베이스가 디스크에서 일치하는 모든 행을 O(n) 시간(테이블 스캔) 대신 O(log(n)) 시간에 찾을 수 있게 해주며, 여기서 n은 행 수입니다. 그러나 이러한 유형의 보조 인덱스는 ClickHouse(또는 다른 컬럼 지향 데이터베이스)에서는 작동하지 않습니다. 인덱스에 추가할 개별 행이 디스크에 존재하지 않기 때문입니다. 대신 ClickHouse는 특정 상황에서 쿼리 속도를 크게 향상시킬 수 있는 다른 유형의 인덱스를 제공합니다. 이러한 구조는 일치하는 값이 전혀 없다고 확실한 상당한 데이터 청크를 ClickHouse가 읽지 않고 건너뛸 수 있게 해주므로 “Skip” 인덱스라고 부릅니다.

기본 동작

데이터 스키핑 인덱스는 MergeTree 엔진 계열의 테이블에서만 사용할 수 있습니다. 각 데이터 스키핑 인덱스에는 4개의 주요 인수가 있습니다.
  • 인덱스 이름. 인덱스 이름은 각 파티션에 인덱스 파일을 생성하는 데 사용됩니다. 또한 인덱스를 삭제하거나 구체화할 때 매개변수로 필요합니다.
  • 인덱스 표현식. 인덱스 표현식은 인덱스에 저장될 값의 집합을 계산하는 데 사용됩니다. 컬럼, 단순 연산자, 그리고/또는 인덱스 유형에 따라 허용되는 일부 함수의 조합일 수 있습니다.
  • TYPE. 인덱스 유형은 각 인덱스 블록의 읽기 및 평가를 생략할 수 있는지를 판단하는 계산 방식을 제어합니다.
  • GRANULARITY. 각 인덱싱된 블록은 GRANULARITY개의 그래뉼로 구성됩니다. 예를 들어, 프라이머리 테이블 인덱스의 세분화 수준이 8192행이고 인덱스 세분화 수준이 4이면, 각 인덱싱된 “block”은 32768행이 됩니다.
사용자가 데이터 스키핑 인덱스를 생성하면 테이블의 각 데이터 파트(data part) 디렉터리에 두 개의 추가 파일이 생성됩니다.
  • skp_idx_{index_name}.idx, 정렬된 표현식 값을 포함합니다
  • skp_idx_{index_name}.mrk2, 연결된 데이터 컬럼 파일에 대한 해당 오프셋을 포함합니다.
쿼리를 실행하면서 관련 컬럼 파일을 읽을 때 WHERE 절의 필터링 조건 일부가 스킵 인덱스 표현식과 일치하면, ClickHouse는 인덱스 파일 데이터를 사용해 관련된 각 데이터 블록을 처리해야 하는지 아니면 건너뛸 수 있는지를 판단합니다(해당 블록이 프라이머리 키 적용으로 이미 제외되지 않았다고 가정할 때). 매우 단순화한 예시로, 예측 가능한 데이터가 로드된 다음 테이블을 살펴보겠습니다.
CREATE TABLE skip_table
(
  my_key UInt64,
  my_value UInt64
)
ENGINE MergeTree primary key my_key
SETTINGS index_granularity=8192;

INSERT INTO skip_table SELECT number, intDiv(number,4096) FROM numbers(100000000);
프라이머리 키를 사용하지 않는 단순한 쿼리를 실행하면 my_value 컬럼의 1억 개 값을 모두 스캔합니다:
SELECT * FROM skip_table WHERE my_value IN (125, 700)
┌─my_key─┬─my_value─┐
│ 512000 │      125 │
│ 512001 │      125 │
│    ... |      ... |
└────────┴──────────┘

8192 rows in set. Elapsed: 0.079 sec. Processed 100.00 million rows, 800.10 MB (1.26 billion rows/s., 10.10 GB/s.
이제 아주 기본적인 스킵 인덱스를 추가합니다:
ALTER TABLE skip_table ADD INDEX vix my_value TYPE set(100) GRANULARITY 2;
일반적으로 스킵 인덱스는 새로 삽입된 데이터에만 적용되므로, 인덱스를 추가만 해서는 위 쿼리에 영향을 주지 않습니다. 이미 존재하는 데이터에도 인덱스를 적용하려면 다음 구문을 사용하십시오:
ALTER TABLE skip_table MATERIALIZE INDEX vix;
새로 생성한 인덱스를 사용해 쿼리를 다시 실행하세요:
SELECT * FROM skip_table WHERE my_value IN (125, 700)
┌─my_key─┬─my_value─┐
│ 512000 │      125 │
│ 512001 │      125 │
│    ... |      ... |
└────────┴──────────┘

8192 rows in set. Elapsed: 0.051 sec. Processed 32.77 thousand rows, 360.45 KB (643.75 thousand rows/s., 7.08 MB/s.)
800메가바이트의 1억 개 행을 처리하는 대신, ClickHouse는 360킬로바이트의 32,768개 행만 읽고 분석했습니다 — 각각 8,192개 행으로 이루어진 4개의 그래뉼입니다. 좀 더 시각적으로 보면, my_value가 125인 4,096개 행이 어떻게 읽혀 선택되었는지, 그리고 그 뒤의 행들은 디스크에서 읽지 않고 어떻게 건너뛰었는지 확인할 수 있습니다: 쿼리를 실행할 때 trace를 활성화하면 스킵 인덱스 사용에 대한 자세한 정보를 확인할 수 있습니다. clickhouse-client에서 send_logs_level을 설정하십시오:
SET send_logs_level='trace';
이는 쿼리 SQL과 테이블 인덱스를 튜닝할 때 유용한 디버깅 정보를 제공합니다. 위 예시에서 디버그 로그는 스킵 인덱스가 2개의 그래뉼만 남기고 나머지는 모두 걸러냈음을 보여줍니다:
<Debug> default.skip_table (933d4b2c-8cea-4bf9-8c93-c56e900eefd1) (SelectExecutor): Index `vix` has dropped 6102/6104 granules.

스킵 인덱스 타입

minmax

이 경량 인덱스 유형에는 매개변수가 필요하지 않습니다. 각 블록에 대해 인덱스 표현식의 최솟값과 최댓값을 저장하며, 표현식이 Tuple인 경우에는 튜플 요소의 각 멤버 값도 별도로 저장합니다. 이 유형은 값 기준으로 대체로 느슨하게 정렬되는 경향이 있는 컬럼에 적합합니다. 이 인덱스 유형은 일반적으로 쿼리 처리 중 적용 비용이 가장 낮습니다. 이 유형의 인덱스는 스칼라 또는 Tuple 표현식에서만 올바르게 작동합니다 — 배열 또는 맵 데이터 타입을 반환하는 표현식에는 인덱스가 적용되지 않습니다.

set

이 경량 인덱스 유형은 블록당 값 집합의 max_size라는 단일 매개변수를 받습니다(0이면 개별 값의 수가 무제한으로 허용됩니다). 이 집합에는 블록 내의 모든 값이 포함됩니다(값의 수가 max_size를 초과하면 비어 있습니다). 이 인덱스 유형은 각 그래뉼 집합 내에서는 낮은 카디널리티를 보이지만(즉, 값이 “뭉쳐” 있지만), 전체적으로는 더 높은 카디널리티를 갖는 컬럼에 적합합니다. 이 인덱스의 비용, 성능, 효율성은 블록 내 카디널리티에 따라 달라집니다. 각 블록에 고유값이 많이 포함되어 있으면, 큰 인덱스 집합에 대해 쿼리 조건을 평가하는 비용이 매우 커지거나, max_size를 초과해 인덱스가 비어 있으므로 인덱스가 적용되지 않을 수 있습니다.

text

자연어 또는 자유 형식 텍스트 검색(예: 큰 텍스트 컬럼에서 단어나 구를 검색하는 작업)이 포함된 워크로드를 위해 ClickHouse는 텍스트 인덱스(진정한 역인덱스)를 제공합니다. 텍스트 인덱스는 효율적인 전문 검색 의미 체계와 토큰화된 조회를 지원합니다. 결정론적 토큰 인덱싱을 제공하고 hasAnyToken, hasAllTokens와 같은 검색 함수에서 더 나은 성능을 제공할 뿐만 아니라, 일반적으로 사용되는 모든 텍스트 검색 함수도 최적화하므로 전문 검색 쿼리에는 텍스트 인덱스를 사용하는 것이 권장됩니다. 자세한 내용은 여기의 텍스트 인덱스 문서를 참조하십시오.

블룸 필터 타입

블룸 필터는 약간의 거짓 양성(false positive) 가능성을 감수하는 대신, 값이 집합에 속하는지 여부를 공간 효율적으로 검사할 수 있게 해주는 데이터 구조입니다. 스킵 인덱스에서는 거짓 양성이 큰 문제가 되지 않습니다. 단지 불필요한 블록을 몇 개 더 읽게 되는 정도이기 때문입니다. 다만 거짓 양성 가능성이 있으므로, 인덱싱된 표현식은 true가 될 가능성이 있는 경우에 사용해야 합니다. 그렇지 않으면 유효한 데이터가 건너뛰어질 수 있습니다. 블룸 필터는 많은 수의 개별 값을 검사하는 작업을 더 효율적으로 처리할 수 있으므로, 검사할 값이 많이 생성되는 조건식에 적합할 수 있습니다. 특히 블룸 필터 인덱스는 배열에 적용할 수 있으며, 이 경우 배열의 모든 값을 검사합니다. 또한 mapKeys 또는 mapValues 함수를 사용해 키 또는 값을 배열로 변환하면 맵에도 적용할 수 있습니다. 블룸 필터를 기반으로 하는 데이터 스키핑 인덱스 타입은 세 가지가 있습니다.
  • 기본 bloom_filter는 허용되는 “거짓 양성” 비율을 나타내는 0과 1 사이의 선택적 매개변수 하나를 받습니다(지정하지 않으면 .025를 사용합니다).
  • 특수한 tokenbf_v1 (Deprecated)). 이 인덱스는 사용되는 블룸 필터를 튜닝하기 위한 세 가지 매개변수를 받습니다. (1) 바이트 단위의 필터 크기(필터가 클수록 거짓 양성은 줄어들지만 저장 공간 사용량이 늘어남), (2) 적용할 해시 함수의 수(이 역시 많을수록 거짓 양성이 줄어듦), (3) 블룸 필터 해시 함수의 시드입니다. 이러한 매개변수가 블룸 필터 동작에 어떤 영향을 미치는지에 대한 자세한 내용은 여기의 계산기를 참고하십시오. 이 인덱스는 String, FixedString, Map 데이터 타입에서만 동작합니다. 입력 표현식은 영숫자가 아닌 문자를 기준으로 구분된 문자 시퀀스로 분할됩니다. 예를 들어 컬럼 값이 This is a candidate for a "full text" search이면 This is a candidate for full text search 토큰을 포함하게 됩니다. 이 인덱스는 LIKE, EQUALS, IN, hasToken() 및 이와 유사한, 긴 문자열 내에서 단어와 기타 값을 검색하는 용도로 설계되었습니다. 예를 들어 자유 형식의 애플리케이션 로그 라인이 들어 있는 컬럼에서 소수의 클래스 이름이나 라인 번호를 검색하는 데 사용할 수 있습니다.
  • 특수한 ngrambf_v1 (Deprecated). 이 인덱스는 token 인덱스와 동일하게 동작합니다. 블룸 필터 설정 앞에 인덱싱할 ngram 크기를 나타내는 매개변수 하나를 추가로 받습니다. ngram은 임의의 문자로 이루어진 길이 n의 문자열이므로, A short string이라는 문자열은 ngram 크기가 4일 때 다음과 같이 인덱싱됩니다.
    'A sh', ' sho', 'shor', 'hort', 'ort ', 'rt s', 't st', ' str', 'stri', 'trin', 'ring'
    
이 인덱스는 텍스트 검색에도 유용할 수 있으며, 특히 중국어처럼 단어 경계가 없는 언어에서 더욱 그렇습니다.
전문 검색 워크로드에는 더 이상 권장되지 않는 tokenbf_v1 또는 ngrambf_v1 인덱스보다 전용 text index(전문 검색용 Text index 참고)를 사용하는 것이 좋습니다. text index는 진정한 역인덱스(inverted index)를 제공하므로, 토큰 기반 블룸 필터 인덱스보다 검색 성능이 더 뛰어나고 동작이 더 예측 가능하며 유연성과 성능도 더 우수합니다.

스킵 인덱스 함수

데이터 스키핑 인덱스의 핵심 목적은 자주 사용되는 쿼리가 분석해야 하는 데이터 양을 줄이는 것입니다. ClickHouse 데이터의 분석 지향적 특성상, 이러한 쿼리 패턴에는 대부분 함수형 표현식이 포함됩니다. 따라서 스킵 인덱스가 효율적으로 동작하려면 일반적으로 사용되는 함수와 올바르게 상호작용해야 합니다. 이는 다음 두 경우 중 하나에서 일어날 수 있습니다.
  • 데이터가 삽입될 때 인덱스가 함수형 표현식으로 정의되어 있고, 해당 표현식의 결과가 인덱스 파일에 저장되는 경우
  • 쿼리를 처리할 때 저장된 인덱스 값에 표현식을 적용하여 블록을 제외할지 여부를 판단하는 경우
각 스킵 인덱스 유형은 인덱스 구현에 적합한 ClickHouse 함수의 일부 집합에 대해서만 동작하며, 해당 함수 목록은 여기에서 확인할 수 있습니다. 일반적으로 Set 인덱스와 블룸 필터 기반 인덱스(또 다른 유형의 Set 인덱스)는 모두 순서가 없으므로 범위 조건에는 적합하지 않습니다. 반면 MinMax 인덱스는 범위가 서로 교차하는지 매우 빠르게 판단할 수 있으므로 범위 조건에서 특히 효과적입니다. 부분 일치 함수인 LIKE, startsWith, endsWith, hasToken의 효율성은 사용한 인덱스 유형, 인덱스 표현식, 그리고 데이터의 구체적인 형태에 따라 달라집니다.

스킵 인덱스 설정

스킵 인덱스에 적용할 수 있는 설정은 두 가지입니다.
  • use_skip_indexes (0 또는 1, 기본값 1). 모든 쿼리가 스킵 인덱스를 효율적으로 사용할 수 있는 것은 아닙니다. 특정 필터 조건이 대부분의 그래뉼을 포함할 가능성이 높다면 데이터 스키핑 인덱스를 적용해도 불필요한 비용만 발생하며, 경우에 따라 그 비용이 상당할 수 있습니다. 어떤 스킵 인덱스도 효과를 내기 어려운 쿼리에는 이 값을 0으로 설정하십시오.
  • force_data_skipping_indices (쉼표로 구분된 인덱스 이름 목록). 이 설정은 일부 유형의 비효율적인 쿼리를 방지하는 데 사용할 수 있습니다. 스킵 인덱스를 사용하지 않으면 테이블 쿼리 비용이 지나치게 커지는 경우, 하나 이상의 인덱스 이름과 함께 이 설정을 사용하면 나열된 인덱스를 사용하지 않는 모든 쿼리에 대해 예외가 반환됩니다. 이렇게 하면 잘못 작성된 쿼리가 서버 리소스를 소모하는 것을 방지할 수 있습니다.

스킵 인덱스 모범 사례

스킵 인덱스는 직관적으로 이해하기 어려우며, 특히 RDBMS 계열의 보조 행 기반 인덱스나 문서 저장소의 역색인에 익숙한 경우 더욱 그렇습니다. 실제로 효과를 얻으려면 ClickHouse 데이터 스키핑 인덱스를 적용할 때 인덱스 계산 비용을 상쇄할 만큼 충분한 그래뉼(granule) 읽기를 줄여야 합니다. 특히 인덱싱된 블록(block)에 값이 단 한 번이라도 나타나면 해당 블록 전체를 메모리로 읽어 평가해야 하므로, 인덱스 비용만 불필요하게 발생하게 됩니다. 다음과 같은 데이터 분포를 살펴보겠습니다: 프라이머리/ORDER BY 키가 timestamp이고, visitor_id에 인덱스가 있다고 가정하겠습니다. 다음 쿼리를 살펴보겠습니다:
SELECT timestamp, url FROM table WHERE visitor_id = 1001`
이러한 데이터 분포에서는 전통적인 보조 인덱스가 매우 유리합니다. 요청된 visitor_id를 가진 5개의 행을 찾기 위해 32768개 행을 모두 읽는 대신, 보조 인덱스에는 단 5개의 행 위치만 포함되므로, 디스크에서는 그 5개 행만 읽으면 됩니다. 그러나 ClickHouse의 데이터 스키핑 인덱스는 정확히 그 반대로 동작합니다. visitor_id 컬럼의 32768개 값은 모두 검사되며, 스킵 인덱스의 유형과 관계없이 동일합니다. 따라서 ClickHouse 쿼리 속도를 높이기 위해 키 컬럼에 인덱스를 단순히 추가하려는 자연스러운 생각은 대개 올바르지 않습니다. 이 고급 기능은 프라이머리 키 수정(How to Pick a Primary Key 참조), 프로젝션 사용, 또는 materialized view 사용과 같은 다른 대안을 먼저 검토한 뒤에만 사용해야 합니다. 데이터 스키핑 인덱스가 적합한 경우에도, 인덱스와 테이블 모두에 대해 세심한 튜닝이 필요한 경우가 많습니다. 대부분의 경우 유용한 스킵 인덱스를 사용하려면 프라이머리 키와 대상 비프라이머리 컬럼/표현식 사이에 강한 상관관계가 있어야 합니다. 상관관계가 없다면(위 그림과 같이), 수천 개 값으로 이루어진 블록 안의 행 중 적어도 하나가 필터링 조건을 만족할 가능성이 높아 스킵되는 블록은 거의 없습니다. 반대로 프라이머리 키의 값 범위(예: 시간대)가 잠재적인 인덱스 컬럼의 값(예: 텔레비전 시청자 연령)과 강하게 연관되어 있다면, minmax 유형의 인덱스가 유용할 가능성이 큽니다. 또한 데이터를 삽입할 때 이 상관관계를 높일 수 있다는 점에도 유의하십시오. 예를 들어 정렬/ORDER BY 키에 추가 컬럼을 포함하거나, 프라이머리 키와 연관된 값이 삽입 시 그룹화되도록 배칭 삽입할 수 있습니다. 예를 들어 프라이머리 키가 많은 수의 사이트 이벤트를 포함하는 타임스탬프라 하더라도, 특정 site_id에 대한 모든 이벤트를 수집 프로세스에서 함께 그룹화해 삽입할 수 있습니다. 그러면 소수의 site ids만 포함하는 그래뉼이 많이 생성되므로, 특정 site_id 값으로 검색할 때 많은 블록을 스킵할 수 있습니다. 스킵 인덱스의 또 다른 좋은 후보는 높은 카디널리티를 가지며 개별 값이 데이터에서 비교적 희소한 표현식입니다. 한 가지 예로, API 요청의 오류 코드를 추적하는 관측성 플랫폼을 들 수 있습니다. 특정 오류 코드는 데이터에서는 드물지만, 검색에서는 특히 중요할 수 있습니다. error_code 컬럼에 set 스킵 인덱스를 사용하면 오류를 포함하지 않는 대다수의 블록을 건너뛸 수 있으므로 오류 중심 쿼리의 성능을 크게 향상할 수 있습니다. 마지막으로, 가장 중요한 모범 사례는 테스트, 또 테스트, 그리고 다시 테스트하는 것입니다. 다시 말해, 문서 검색을 위한 b-tree 보조 인덱스나 inverted index와는 달리, 데이터 스키핑 인덱스의 동작은 쉽게 예측되지 않습니다. 이를 테이블에 추가하면 데이터 수집과, 여러 이유로 인덱스의 이점을 얻지 못하는 쿼리 모두에 의미 있는 비용이 발생합니다. 따라서 항상 실제 환경과 유사한 데이터로 테스트해야 하며, 테스트에는 유형, 세분화 수준 크기 및 기타 매개변수의 변형도 포함되어야 합니다. 실제로 테스트해 보면 단순한 사고 실험만으로는 분명히 드러나지 않는 패턴과 함정을 발견하는 경우가 많습니다.
마지막 수정일 2026년 6월 10일