ClickHouse — 열 지향 분석 DB 를 속까지 · 성긴 기본 키 · 实验
정렬 키 순서를 바꿔 가며 건너뛴 그래뉼을 센다
목표
같은 200만 행을 정렬 키가 다른 여러 표에 넣고, EXPLAIN indexes = 1 의 그래뉼 수와 rows_read 로 성긴 기본 인덱스가 무엇을 건너뛰는지 확인한다. 마지막에 주어진 쿼리에 맞는 정렬 키를 직접 고른다.
왜 중요한가
ClickHouse 표의 정렬 키는 만든 뒤에 바꾸기 어렵고, 잘못 고르면 인덱스가 있어도 매번 전부 읽는다. 어느 열을 앞에 둘지는 감이 아니라 "이 쿼리는 몇 그래뉼을 고르나" 로 정한다. 이 실습의 채점기는 여러분이 적은 숫자를 믿지 않는다 — 여러분이 저장한 SELECT 에 EXPLAIN 을 붙여 읽기 전용으로 다시 돌리고, 같은 SELECT 를 다시 실행해 읽은 행을 재서 대조한다.
단계
1. 데이터베이스 sparse 와 표 sparse.hits 를 만드세요 — 열 site LowCardinality(String), user_id UInt32, ts DateTime, dur_ms UInt32 (이 순서), 엔진 MergeTree, 정렬 키 ORDER BY (site, user_id).
2. /opt/lab/fixtures/sparse/hits.sql 을 한 번 실행해 200만 행을 넣고 OPTIMIZE TABLE sparse.hits FINAL 로 파트를 하나로 합치세요.
3. site = 'docs.example' 인 행의 dur_ms 합을 내는 /root/ch/sparse/q_site.sql 을 만들고, 그 SELECT 에 EXPLAIN indexes = 1 을 붙인 출력을 /root/ch/sparse/explain_site.txt 에 저장한 뒤, PrimaryKey 단계의 고른/전체 그래뉼과 검색 방식을 /root/ch/sparse/granules.json 에 selected·total·search 로 적으세요.
4. user_id = 4242 인 행의 dur_ms 합을 내는 /root/ch/sparse/q_user.sql 을 만들고, EXPLAIN 의 고른 그래뉼·검색 방식과 이 쿼리의 rows_read 를 /root/ch/sparse/user.json 에 selected·search·rows_read 로 적으세요.
5. 같은 열에 정렬 키만 ORDER BY (user_id, site) 인 sparse.hits_us 를 만들어 sparse.hits 의 행을 옮기고 파트를 하나로 합치세요. 그 표에서 user_id = 4242 의 합을 내는 /root/ch/sparse/q_user_us.sql, site = 'docs.example' 의 합을 내는 /root/ch/sparse/q_site_us.sql 을 만들고 두 쿼리의 rows_read 를 /root/ch/sparse/order.json 에 user_rows_read·site_rows_read 로 적으세요.
6. sparse.hits 와 같은 열·정렬 키에 SETTINGS index_granularity = 1024 인 sparse.hits_g1k 를 만들어 행을 옮기고 합치세요. 그 표의 marks·primary_key_size(system.parts)와, user_id = 4242 의 합을 내는 /root/ch/sparse/q_user_g1k.sql 의 rows_read 를 /root/ch/sparse/granularity.json 에 marks·primary_key_size·user_rows_read 로 적으세요.
7. 정렬 키 ORDER BY (site, user_id, ts) 에 기본 키 PRIMARY KEY (site, user_id) 를 따로 둔 sparse.hits_pk 와, 같은 정렬 키에 PRIMARY KEY 를 적지 않은 sparse.hits_full 을 만들어 행을 옮기고 합치세요. 두 표의 primary_key_size 를 /root/ch/sparse/pk.json 에 hits_pk·hits_full 로 적으세요.
8. 하루치 사이트 보고서 WHERE site = 'api.example' AND ts >= '2026-09-10 00:00:00' AND ts < '2026-09-11 00:00:00' 의 dur_ms 합을 sparse.hits 에서 내는 /root/ch/sparse/q_day_hits.sql 과, 정렬 키를 여러분이 골라 만든 sparse.hits_day(같은 열·같은 행, 파트 하나)에서 내는 /root/ch/sparse/q_day.sql 을 만드세요. q_day.sql 의 rows_read 가 q_day_hits.sql 의 4분의 1 이하여야 합니다. 두 값을 /root/ch/sparse/day.json 에 hits_rows_read·day_rows_read 로 적으세요.
참고
- 서버는 파드가 뜰 때 이미 떠 있습니다(127.0.0.1:9000). 멈췄다면
ch-up. - EXPLAIN 저장:
clickhouse-client -q "EXPLAIN indexes = 1 $(cat q_site.sql)" > explain_site.txt. PrimaryKey 블록 안의Granules: 고른/전체를 봅니다(윗줄 ReadFromMergeTree 의 숫자와 헷갈리지 마세요). - 읽은 행 재기:
clickhouse-client --use_query_condition_cache 0 --queries-file q_user.sql --format JSON | jq .statistics.rows_read. 쿼리 조건 캐시를 꼭 끄세요 — 26.8 은 기본으로 켜져 있어, 같은 조건을 두 번째로 돌리면 읽는 행이 줄어든 값이 나옵니다. 채점기도 끄고 잽니다. CREATE TABLE 새표 AS sparse.hits ENGINE = MergeTree ORDER BY (...)로 열만 복사하고 정렬 키를 새로 줄 수 있습니다. 행은INSERT INTO 새표 SELECT * FROM sparse.hits.- 흔한 실수: 합치지 않고(OPTIMIZE FINAL 없이) 재는 것 — 파트가 여럿이면 파트마다 그래뉼을 따로 세어 숫자가 달라집니다. PRIMARY KEY 에 정렬 키의 앞부분이 아닌 열을 적으면 표가 만들어지지 않습니다.
- 공식 문서: [A practical introduction to primary indexes](https://clickhouse.com/docs/guides/clickhouse/data-modelling/sparse-primary-indexes) · [Primary indexes](https://clickhouse.com/docs/concepts/core-concepts/primary-indexes) · [Choosing a primary key](https://clickhouse.com/docs/concepts/best-practices/choosing-a-primary-key) · [MergeTree](https://clickhouse.com/docs/reference/engines/table-engines/mergetree-family/mergetree) · [EXPLAIN](https://clickhouse.com/docs/reference/statements/explain) · [system.parts](https://clickhouse.com/docs/reference/system-tables/parts)
8个步骤
- 종류가 적은 열을 앞에 둔 표
- 200만 행을 넣고 파트를 하나로
- 첫 키 열로 거르면 — 이진 탐색
- 두 번째 키 열로만 거르면
- 키 순서를 뒤집은 표
- 그래뉼을 1024행으로 줄이면
- PRIMARY KEY 를 정렬 키의 앞부분으로
- 쿼리에 맞는 정렬 키를 고른다