프라이머리 인덱스(Primary Indexes)
프라이머리 인덱스(Primary Indexes)
ClickHouse의 희소(sparse) 프라이머리 인덱스는 쿼리 실행 중 불필요한 데이터를 효율적으로 건너뛰게 해 줍니다. 이 문서에서는 이 인덱스가 어떻게 만들어지고, 어떻게 동작하며, 쿼리를 어떻게 가속화하는지 기본부터 설명할게요.
출처: 문서
본문
고급 인덱싱 세부 정보를 찾고 계세요? 이 페이지는 ClickHouse의 희소 프라이머리 인덱스가 무엇인지, 어떻게 구축되는지, 어떻게 동작하는지, 그리고 쿼리 가속화에 어떻게 도움이 되는지 소개합니다. 고급 인덱싱 전략과 더 깊은 기술적 세부 사항은 프라이머리 인덱스 딥다이브를 참고하세요.
ClickHouse의 희소 프라이머리 인덱스는 어떻게 동작하나요?
ClickHouse의 희소 프라이머리 인덱스는 테이블의 프라이머리 키 컬럼에 대한 쿼리 조건과 일치하는 데이터를 포함할 수 있는 그래뉼(granules) — 행 블록 — 을 효율적으로 식별하는 데 도움을 줍니다. 다음 섹션에서는 이 인덱스가 해당 컬럼들의 값에서 어떻게 구성되는지 설명합니다.
희소 프라이머리 인덱스 생성
희소 프라이머리 인덱스가 어떻게 구축되는지 설명하기 위해 uk_price_paid_simple 테이블과 몇 가지 애니메이션을 사용합니다. 상기하자면, ① 프라이머리 키 (town, street)를 가진 예제 테이블에서 ② 삽입된 데이터는 ③ 프라이머리 키 컬럼 값으로 정렬되어 디스크에 저장되고, 각 컬럼마다 별도의 파일로 압축됩니다.
처리를 위해 각 컬럼의 데이터는 ④ 논리적으로 그래뉼—각각 8,192행을 포함—로 나뉘며, 이것이 ClickHouse 데이터 처리 메커니즘이 다루는 가장 작은 단위입니다. 이 그래뉼 구조가 프라이머리 인덱스를 희소(sparse) 하게 만드는 이유이기도 합니다. ClickHouse는 모든 행을 인덱싱하는 대신 그래뉼당 한 행, 정확히는 첫 번째 행의 프라이머리 키 값만 ⑤ 저장합니다. 그 결과 그래뉼당 하나의 인덱스 항목이 생깁니다.
희소성 덕분에 프라이머리 인덱스는 전체가 메모리에 들어갈 만큼 작아지며, 프라이머리 키 컬럼에 대한 조건이 있는 쿼리를 빠르게 필터링할 수 있습니다. 다음 섹션에서는 이런 쿼리를 가속화하는 데 어떻게 도움이 되는지 보여줍니다.
프라이머리 인덱스 사용
다른 애니메이션으로 희소 프라이머리 인덱스가 쿼리 가속화에 어떻게 사용되는지 그려 보겠습니다:
1 쿼리 조건 파싱
예제 쿼리는 두 프라이머리 키 컬럼 모두에 조건을 포함합니다: town = 'LONDON' AND street = 'OXFORD STREET'.
2 프라이머리 인덱스 로드
쿼리를 가속화하기 위해 ClickHouse는 테이블의 프라이머리 인덱스를 메모리에 로드합니다.
3 일치하는 그래뉼 식별
그런 다음 인덱스 항목을 스캔하여 쿼리 조건과 일치하는 행을 포함할 수 있는 그래뉼, 즉 건너뛸 수 없는 그래뉼을 식별합니다.
4 그래뉼 로드 및 처리
이러한 잠재적으로 관련된 그래뉼은 쿼리에 필요한 다른 컬럼의 상응하는 그래뉼과 함께 메모리에 로드되어 처리됩니다.
프라이머리 인덱스 모니터링하기
테이블의 각 데이터 파트는 고유한 프라이머리 인덱스를 가집니다. mergeTreeIndex 테이블 함수를 사용해 이 인덱스들의 내용을 검사할 수 있습니다. 다음 쿼리는 예제 테이블의 각 데이터 파트에 대한 프라이머리 인덱스의 항목 수를 나열합니다:
SELECT
part_name,
max(mark_number) AS entries
FROM mergeTreeIndex('uk', 'uk_price_paid_simple')
GROUP BY part_name;
┌─part_name─┬─entries─┐
1. │ all_2_2_0 │ 914 │
2. │ all_1_1_0 │ 1343 │
3. │ all_0_0_0 │ 1349 │
└───────────┴─────────┘
이 쿼리는 현재 데이터 파트 중 하나의 프라이머리 인덱스에서 처음 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;
┌─entry─┬─town───────────┬─street───────────┐
1. │ 1 │ ABBOTS LANGLEY │ ABBEY DRIVE │
2. │ 2 │ ABERDARE │ RICHARDS TERRACE │
3. │ 3 │ ABERGELE │ PEN Y CAE │
4. │ 4 │ ABINGDON │ CHAMBRAI CLOSE │
5. │ 5 │ ABINGDON │ THORNLEY CLOSE │
6. │ 6 │ ACCRINGTON │ MAY HILL CLOSE │
7. │ 7 │ ADDLESTONE │ HARE HILL │
8. │ 8 │ ALDEBURGH │ LINDEN ROAD │
9. │ 9 │ ALDERSHOT │ HIGH STREET │
10. │ 10 │ ALFRETON │ ALMA STREET │
└───────┴────────────────┴──────────────────┘
마지막으로 EXPLAIN 절을 사용해 모든 데이터 파트의 프라이머리 인덱스가 예제 쿼리 조건과 일치하는 행을 절대 포함할 수 없는 그래뉼을 어떻게 건너뛰는지 확인합니다. 이 그래뉼들은 로드와 처리에서 제외됩니다:
EXPLAIN indexes = 1
SELECT
max(price)
FROM
uk.uk_price_paid_simple
WHERE
town = 'LONDON' AND street = 'OXFORD STREET';
┌─explain────────────────────────────────────────────────────────────────────────────────────────────────────┐
1. │ Expression ((Project names + Projection)) │
2. │ Aggregating │
3. │ Expression (Before GROUP BY) │
4. │ Expression │
5. │ ReadFromMergeTree (uk.uk_price_paid_simple) │
6. │ Indexes: │
7. │ PrimaryKey │
8. │ Keys: │
9. │ town │
10. │ street │
11. │ Condition: and((street in ['OXFORD STREET', 'OXFORD STREET']), (town in ['LONDON', 'LONDON'])) │
12. │ Parts: 3/3 │
13. │ Granules: 3/3609 │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
위 EXPLAIN 출력의 13행을 보면 모든 데이터 파트의 전체 3,609개 그래뉼 중 단 3개만 프라이머리 인덱스 분석에 의해 처리용으로 선택되었습니다. 나머지 그래뉼은 전부 건너뛰었죠. 쿼리를 그냥 실행해도 대부분의 데이터가 건너뛰어졌음을 확인할 수 있습니다:
SELECT max(price)
FROM uk.uk_price_paid_simple
WHERE (town = 'LONDON') AND (street = 'OXFORD STREET');
┌─max(price)─┐
1. │ 263100000 │ -- 263.10 million
└────────────┘
1 row in set. Elapsed: 0.010 sec. Processed 24.58 thousand rows, 159.04 KB (2.53 million rows/s., 16.35 MB/s.)
Peak memory usage: 13.00 MiB.
위에서 보듯 예제 테이블의 약 3,000만 행 중 약 25,000행만 처리되었습니다:
SELECT count() FROM uk.uk_price_paid_simple;
┌──count()─┐
1. │ 29556244 │ -- 29.56 million
└──────────┘
핵심 요점
- 희소 프라이머리 인덱스 는 프라이머리 키 컬럼에 대한 쿼리 조건과 일치하는 행을 포함할 수 있는 그래뉼을 식별하여 ClickHouse가 불필요한 데이터를 건너뛰게 도와줍니다.
- 각 인덱스는 모든 그래뉼의 첫 번째 행(기본적으로 그래뉼은 8,192행)의 프라이머리 키 값만 저장하므로 메모리에 들어갈 만큼 컴팩트합니다.
- MergeTree 테이블의 각 데이터 파트 는 고유한 프라이머리 인덱스 를 가지며, 쿼리 실행 중 독립적으로 사용됩니다.
- 쿼리 중에 인덱스는 ClickHouse가 그래뉼을 건너뛰게 해 주어 I/O와 메모리 사용을 줄이고 성능을 가속화합니다.
mergeTreeIndex테이블 함수로 인덱스 내용을 검사 하고EXPLAIN절로 인덱스 사용을 모니터링할 수 있습니다.
더 자세한 정보는 어디서?
ClickHouse에서 희소 프라이머리 인덱스가 어떻게 동작하는지 더 깊이 알아보고 싶다면, 전통적인 데이터베이스 인덱스와 어떻게 다른지와 사용 모범 사례를 담은 상세 인덱싱 딥다이브를 확인하세요. 프라이머리 인덱스 스캔이 선택한 데이터를 ClickHouse가 고도로 병렬로 처리하는 방법이 궁금하다면 여기의 쿼리 병렬 처리 가이드를 참고하세요.