ClickHouse 리뷰어 가이드라인
GitLab v19.2이 페이지는 ClickHouse 리뷰어를 위한 입문 자료와 가이드를 제공합니다. ClickHouse 리뷰어는 ClickHouse OLAP 데이터베이스에 경험이 있는 도메인 전문가입니다. 리뷰어의 책임은 ClickHouse 관련 변경 사항을 검증하고 ClickHouse가 구성될 수 있는 모든 GitLab 환경에서 올바르게 작동하는지 확인하는 것입니다.
이 페이지는 ClickHouse 리뷰어를 위한 입문 자료와 가이드를 제공합니다.
ClickHouse 리뷰어 업무 범위#
ClickHouse 리뷰어는 ClickHouse OLAP 데이터베이스에 경험이 있는 도메인 전문가입니다. ClickHouse와 상호작용하는 애플리케이션 코드가 수정될 때마다 ClickHouse 데이터베이스 리뷰가 필요합니다. 예시는 다음과 같습니다:
- 새 ClickHouse 마이그레이션 추가.
- ClickHouse 쿼리를 실행하는 서비스 클래스 변경.
- 새 ClickHouse 쿼리 도입.
리뷰어의 책임은 ClickHouse 관련 변경 사항을 검증하고 ClickHouse가 구성될 수 있는 모든 GitLab 환경에서 올바르게 작동하는지 확인하는 것입니다.
ClickHouse 리뷰어를 위한 리소스#
- GitLab 내 ClickHouse: GitLab에서의 ClickHouse 사용 개요.
- GitLab 데이터베이스 리뷰어 가이드라인: 데이터베이스 마이그레이션에 관해 특히 ClickHouse에도 적용되는 일반 원칙. ClickHouse는 관계형 데이터베이스와 동일한 배포 전 및 배포 후 마이그레이션 전략을 따릅니다.
일반 가이드라인#
데이터베이스 스키마 일관성 보장#
현재 ClickHouse 데이터베이스 스키마는 단일 main.sql 파일에 저장됩니다. 이 파일은 ActiveRecord 마이그레이션과 유사하게 마이그레이션 실행 시 자동으로 업데이트됩니다.
때로는 main.sql 파일이 머지 리퀘스트에서 업데이트되거나 커밋되지 않아 마이그레이션으로 구축된 스키마와 커밋된 스키마 파일 간에 불일치가 발생할 수 있습니다.
이 문제를 감지하기 위해 CI 잡(clickhouse:check-schema)이 테스트 스테이지 중에 실행됩니다. 이 잡은 새로 구축된 스키마를 main.sql과 비교하고 차이가 있으면 실패합니다.
- 이 잡은 실패해도 괜찮도록 허용되지 않습니다. 실패하면 MR 파이프라인이 실패합니다.
- 리뷰어로서 잡 로그를 항상 확인하세요. 실패하는 경우 차이를 신중하게 검사하세요. 공백이 아닌 관련 차이는 MR 작성자와 논의해야 합니다.
- 거짓 양성의 경우(예: ClickHouse 버전 불일치로 인한 차이), MR에
pipeline:skip-check-clickhouse-schema라벨을 추가하여 이 검사를 건너뜁니다.
정당한 스키마 차이를 해결하기 위해 작성자는 모든 마이그레이션이 실행되도록 하고 스키마를 덤프할 수 있습니다:
bundle exec rake gitlab:clickhouse:migrate; bundle exec rake gitlab:clickhouse:schema:dump
데이터베이스 쿼리 리뷰#
GitLab의 ClickHouse 쿼리는 두 가지 방법으로 작성할 수 있습니다:
- Raw SQL 쿼리
- QueryBuilder – ActiveRecord와 유사한 추상화(문서)
Raw SQL 쿼리를 리뷰할 때 변수 보간에 주의를 기울이세요:
-
권장: 민감한 데이터가 로그에 기록되지 않도록 변수는 ClickHouse의 플레이스홀더 구문을 사용해야 합니다:
sql = 'SELECT * FROM events WHERE id > {min_id:UInt64}' -
고정 문자열 보간(예: 문자열이 Ruby 상수에 할당되는 경우)은 SQL 인젝션 또는 잘못된 형식의 쿼리를 방지하기 위해 항상 적절한 인용을 사용해야 합니다:
SQL = "SELECT * FROM events WHERE type = #{ClickHouse::Client::Quoting.quote('Issue')}"
데이터베이스 쿼리 성능 리뷰#
ClickHouse는 대용량 데이터셋을 효율적으로 처리할 수 있지만 복잡한 집계에서도 쿼리 실행을 10초 이내로 유지하는 것을 목표로 합니다. 성능 기대치는 기능 사용량과 데이터셋 크기에 따라 다릅니다.
쿼리를 리뷰할 때:
- 코드에서 명확하게 보이지 않는 경우 작성자에게 Raw SQL 제공을 요청합니다.
- 테이블 구조를 검토합니다(
SHOW CREATE TABLE table_name FORMAT raw) 파티셔닝과 기본 키를 이해하기 위해. - 쿼리 필터가 테이블의 기본 키 또는 파티셔닝 열과 일치하는지 확인합니다.
쿼리 예시:
SELECT count(DISTINCT contributions.author_id) AS contributor_count
FROM (
SELECT argMax(author_id, contributions.updated_at) AS author_id
FROM contributions
WHERE
startsWith(contributions.path, {namespace_path:String})
AND contributions.created_at BETWEEN {from:Date} AND {to:Date}
GROUP BY id
) contributions
이 쿼리는 필터 열(path, created_at)이 기본 키에 포함된 경우 잘 동작합니다:
CREATE TABLE contributions (
id UInt64,
path String,
author_id UInt64,
target_type LowCardinality(String),
action UInt8,
created_at Date,
updated_at DateTime64(6, 'UTC')
) ENGINE = ReplacingMergeTree
PARTITION BY toYear(created_at)
ORDER BY (path, created_at, author_id, id);
성능 검증 단계:
-
대표적인 파라미터로 테스트합니다(예:
namespace_path='9970/', 1개월 날짜 범위). -
쿼리를 실행하고 경과 시간과 읽은 행 수를 기록합니다:
Elapsed: 0.062s Read: 1,111,111 rows (15.55 MB) -
스캔한 행을 전체 테이블 행과 비교합니다(
SELECT COUNT(*) FROM contributions). 제대로 제한된 쿼리는 전체 행의 일부만 읽어야 합니다.
쿼리 플랜 검사:
EXPLAIN indexes=1을 사용하여 필터가 기본 키 인덱스를 사용하는지 확인합니다:
EXPLAIN indexes=1
SELECT count(DISTINCT author_id) FROM contributions ...
FORMAT raw
플랜에서의 발췌:
PrimaryKey
Keys:
path
created_at
Condition: and((created_at in (-Inf, 20361]), and((created_at in [20332, +Inf)), (path in ['9970/', '99700'))))
Parts: 11/11
Granules: 185/72937
Search Algorithm: generic exclusion search
출력에서 PrimaryKey 섹션을 찾고 Granules 비율을 확인하세요.
예를 들어: 185/72937 그래뉼은 테이블의 작은 부분만 스캔되었음을 의미합니다 - 성능에 이상적입니다.
성능에 대한 논의를 제기할 때:
- 쿼리가 1000만 행 이상을 스캔하는 경우.
- 쿼리가 5~10초 실행 시간을 지속적으로 초과하는 경우.
- 쿼리가 자주 실행될 경우.
성능 검증이 대규모 네임스페이스(예: gitlab-org 또는 gitlab-org/gitlab)의 실제(또는 합성) 데이터를 사용하도록 하세요.
새 구체화된 뷰 리뷰#
구체화된 뷰가 POPULATE 키워드로 생성되었는지 또는 대용량 데이터셋에 대한 백필 마이그레이션이 있는지 확인하세요.
테이블 엔진별 동작#
MergeTree 계열에서 기본 키(즉, ORDER BY)는 정렬/인덱스를 정의하며 고유성 제약이 아닙니다. 동일한 기본 키 값을 가진 행이 공존할 수 있습니다. 수집 파이프라인이 중복 또는 업데이트를 생성할 수 있는 경우 읽기 시간에 처리하거나(또는 버전을 축소하는 엔진을 선택해야 합니다).
MergeTree 엔진#
- 자동 중복 제거 없음.
- 데이터가 완전히 추가 전용이고 중복이 발생할 수 없는 경우(예: 불변 이벤트 로그) 사용합니다.
CREATE TABLE events
(
event_id UInt64,
timestamp DateTime,
user_id UInt64,
payload String
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)
ORDER BY (event_id, timestamp);
ReplacingMergeTree#
ReplacingMergeTree는 백그라운드 병합 중에 중복 기본 키를 축소할 수 있습니다(비결정적 타이밍). 병합이 발생할 때까지 읽기에서 여전히 여러 버전이 보일 수 있으므로 쿼리 시간 중복 제거가 권장됩니다.
모범 사례:
- 버전 열(단조 증가, 일반적으로
DateTime64) 제공. - 소프트 삭제를 위한 선택적 삭제 플래그(
Bool유형).
버전 파라미터를 생략하면 병합 후 중복 제거된 행은 임의적입니다.
CREATE TABLE items
(
id UInt64,
name String,
status LowCardinality(String),
updated_at DateTime64(6), -- acts as the version
deleted Bool DEFAULT 0 -- deleted flag for marking a record deleted
)
ENGINE = ReplacingMergeTree(updated_at, deleted)
ORDER BY id
행을 중복 제거하려면 버전 열로 argMax를 사용하고 기본 키로 GROUP BY를 수행하세요:
SELECT *
FROM (
SELECT
id,
argMax(name, updated_at) AS name,
argMax(status, updated_at) AS status,
argMax(deleted, updated_at) AS deleted
FROM items
GROUP BY id
) AS items
WHERE deleted = false
ClickHouse 콘솔이나 테스트 케이스에서는 FINAL 수정자를 사용할 수 있습니다.
SELECT * FROM items FINAL;
프로덕션 쿼리에서 FINAL을 피하세요. FINAL은 즉석에서 축소/병합을 강제하며 I/O 측면에서 매우 비쌀 수 있습니다. 위에서 언급한 쿼리 시간 중복 제거 패턴을 선호하세요.