스파스 프라이머리 인덱스
스파스 프라이머리 인덱스
ClickHouse는 일반 데이터베이스처럼 모든 행을 인덱스에 담지 않아요. 대신 그래뉼(granule) 이라는 행 블록 단위로, 블록마다 한 행의 프라이머리 키 값만 저장하는 스파스(sparse, 희소) 프라이머리 인덱스를 씁니다. 이 덕분에 인덱스가 메모리에 통째로 들어갈 만큼 작아져, 프라이머리 키 컬럼에 대한 조건이 있는 쿼리를 아주 빠르게 걸러낼 수 있어요.
스파스 프라이머리 인덱스 생성
예시 테이블의 프라이머리 키가 (town, street)라고 생각해 볼게요. 클릭하우스는 삽입된 데이터를 프라이머리 키 컬럼 값 순서로 정렬·압축해서 각 컬럼을 별도 파일로 디스크에 저장합니다.
처리 단위로는 각 컬럼의 데이터를 그래뉼로 나눠요. 기본적으로 그래뉼 하나가 8,192행을 담고, 이게 클릭하우스 데이터 처리 메커니즘이 다루는 가장 작은 단위예요.
이 그래뉼 구조가 바로 인덱스를 '스파스'하게 만드는 이유예요. 행마다 인덱싱하는 대신, 클릭하우스는 그래뉼마다 첫 번째 행의 프라이머리 키 값만 저장합니다. 그래서 그래뉼 하나당 인덱스 엔트리가 하나 생겨요.
스파스하기 때문에 프라이머리 인덱스는 전체가 RAM에 들어갈 만큼 작아요. 프라이머리 키 컬럼에 술어(predicate)가 있는 쿼리를 빠르게 필터링할 수 있는 이유가 이거예요.
인덱스 사용
쿼리 가속화 과정은 이렇게 진행됩니다.
- 쿼리 술어 파싱 — 예시 쿼리는 두 프라이머리 키 컬럼에 술어가 있어요:
town = 'LONDON' AND street = 'OXFORD STREET'. - 프라이머리 인덱스 로드 — 가속화를 위해 클릭하우스가 테이블의 프라이머리 인덱스를 메모리로 로드해요.
- 일치하는 그래뉼 식별 — 인덱스 엔트리를 훑어 술어와 일치하는 행을 포함할 가능성이 있는 그래뉼, 다시 말해 건너뛸 수 없는 그래뉼을 찾아요.
- 그래뉼 로드와 처리 — 관련성이 있는 그래뉼과 쿼리에 필요한 다른 컬럼의 그래뉼을 함께 메모리로 로드해 처리합니다.
인덱스 모니터링
테이블의 각 데이터 파트마다 자신만의 프라이머리 인덱스를 가져요. 그 내용은 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
더 알아보기
- 테이블 데이터 파트 — 인덱스가 붙는 데이터 파트가 무엇인지
- 파트 병합 — 파트가 백그라운드에서 어떻게 합쳐지는지
- 쿼리 병렬 실행 — 인덱스가 선택한 그래뉼이 어떻게 고병렬로 처리되는지