스파스 프라이머리 인덱스

스파스 프라이머리 인덱스

ClickHouse는 일반 데이터베이스처럼 모든 행을 인덱스에 담지 않아요. 대신 그래뉼(granule) 이라는 행 블록 단위로, 블록마다 한 행의 프라이머리 키 값만 저장하는 스파스(sparse, 희소) 프라이머리 인덱스를 씁니다. 이 덕분에 인덱스가 메모리에 통째로 들어갈 만큼 작아져, 프라이머리 키 컬럼에 대한 조건이 있는 쿼리를 아주 빠르게 걸러낼 수 있어요.

스파스 프라이머리 인덱스 생성

예시 테이블의 프라이머리 키가 (town, street)라고 생각해 볼게요. 클릭하우스는 삽입된 데이터를 프라이머리 키 컬럼 값 순서로 정렬·압축해서 각 컬럼을 별도 파일로 디스크에 저장합니다.

처리 단위로는 각 컬럼의 데이터를 그래뉼로 나눠요. 기본적으로 그래뉼 하나가 8,192행을 담고, 이게 클릭하우스 데이터 처리 메커니즘이 다루는 가장 작은 단위예요.

이 그래뉼 구조가 바로 인덱스를 '스파스'하게 만드는 이유예요. 행마다 인덱싱하는 대신, 클릭하우스는 그래뉼마다 첫 번째 행의 프라이머리 키 값만 저장합니다. 그래서 그래뉼 하나당 인덱스 엔트리가 하나 생겨요.

스파스하기 때문에 프라이머리 인덱스는 전체가 RAM에 들어갈 만큼 작아요. 프라이머리 키 컬럼에 술어(predicate)가 있는 쿼리를 빠르게 필터링할 수 있는 이유가 이거예요.

인덱스 사용

쿼리 가속화 과정은 이렇게 진행됩니다.

  1. 쿼리 술어 파싱 — 예시 쿼리는 두 프라이머리 키 컬럼에 술어가 있어요: town = 'LONDON' AND street = 'OXFORD STREET'.
  2. 프라이머리 인덱스 로드 — 가속화를 위해 클릭하우스가 테이블의 프라이머리 인덱스를 메모리로 로드해요.
  3. 일치하는 그래뉼 식별 — 인덱스 엔트리를 훑어 술어와 일치하는 행을 포함할 가능성이 있는 그래뉼, 다시 말해 건너뛸 수 없는 그래뉼을 찾아요.
  4. 그래뉼 로드와 처리 — 관련성이 있는 그래뉼과 쿼리에 필요한 다른 컬럼의 그래뉼을 함께 메모리로 로드해 처리합니다.

인덱스 모니터링

테이블의 각 데이터 파트마다 자신만의 프라이머리 인덱스를 가져요. 그 내용은 mergeTreeIndex 테이블 함수로 들여다볼 수 있습니다.

다음 쿼리는 각 데이터 파트의 프라이머리 인덱스에 몇 개의 엔트리가 있는지 보여줘요.

SELECT
    part_name,
    max(mark_number) AS entries
FROM mergeTreeIndex('uk', 'uk_price_paid_simple')
GROUP BY part_name;

이 파트들은 백그라운드에서 계속 더 큰 파트로 병합되죠. 어느 한 파트의 인덱스 첫 10개 엔트리는 이렇게 조회할 수 있어요.

SELECT 
    mark_number + 1 AS entry,
    town,
    street
FROM mergeTreeIndex('uk', 'uk_price_paid_simple')
WHERE part_name = (SELECT any(part_name) FROM mergeTreeIndex('uk', 'uk_price_paid_simple')) 
ORDER BY mark_number ASC
LIMIT 10;

인덱스가 실제로 그래뉼을 얼마나 건너뛰는지는 EXPLAIN 절로 확인할 수 있어요. 아래 쿼리로 프라이머리 인덱스가 어떻게 불필요한 그래뉼을 배제하는지 볼 수 있습니다.

EXPLAIN indexes = 1
SELECT
    max(price)
FROM
    uk.uk_price_paid_simple
WHERE
    town = 'LONDON' AND street = 'OXFORD STREET';

EXPLAIN 출력의 아랫부분에서 Granules: 3/3609 같은 줄을 볼 수 있어요. 예시에서는 전체 데이터 파트의 그래뉼 3,609개 중 단 3개만 인덱스 분석에 선택되었고, 나머지는 완전히 건너뛰었죠. 실제 쿼리로도 확인할 수 있어요. 약 3,000만 행 중 2만 5천여 행만 처리되고 나머지는 스킵되는 걸 볼 수 있습니다.

핵심 요점

  • 스파스 프라이머리 인덱스는 프라이머리 키 컬럼의 쿼리 조건과 일치할 행이 들어 있는 그래뉼을 식별해 불필요한 데이터를 건너뛰게 도와줘요.
  • 각 인덱스는 그래뉼의 첫 번째 행 프라이머리 키 값만 저장해(그래뉼은 기본 8,192행) 메모리에 들어갈 만큼 컴팩트해요.
  • MergeTree 테이블의 각 데이터 파트는 자신만의 프라이머리 인덱스를 가지며, 쿼리 실행 중 독립적으로 사용됩니다.
  • 쿼리 중 이 인덱스로 그래뉼을 건너뛰어 I/O와 메모리를 줄이면서 성능을 높여요.
  • mergeTreeIndex 테이블 함수로 인덱스 내용을, EXPLAIN 절로 인덱스 사용 여부를 확인할 수 있어요.

출처: Primary indexes

더 알아보기

  • 테이블 데이터 파트 — 인덱스가 붙는 데이터 파트가 무엇인지
  • 파트 병합 — 파트가 백그라운드에서 어떻게 합쳐지는지
  • 쿼리 병렬 실행 — 인덱스가 선택한 그래뉼이 어떻게 고병렬로 처리되는지