ClickHouse 리뷰어 가이드라인
GitLab v19.5요약
이 페이지에서는 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는 관계형 데이터베이스와 동일한 pre-deployment 및 post-deployment 마이그레이션 전략을 따릅니다.
일반 가이드라인#
데이터베이스 스키마 일관성 보장#
현재 ClickHouse 데이터베이스 스키마는 main.sql 파일 하나에 저장됩니다. 이 파일은 ActiveRecord 마이그레이션과 마찬가지로 마이그레이션이 실행될 때 자동으로 갱신됩니다.
main.sql 파일이 갱신되지 않거나 머지 리퀘스트에 커밋되지 않아, 마이그레이션으로 만들어진 스키마와 커밋된 스키마 파일이 서로 어긋나는 경우가 있습니다.
이를 탐지하기 위해 test Stage에서 CI job(clickhouse:check-schema)이 실행됩니다. 이 job은 새로 만들어진 스키마와 main.sql을 비교해 차이가 있으면 실패합니다.
- 이 job은 실패가 허용되지 않습니다. 실패하면 머지 리퀘스트 파이프라인이 실패합니다.
- 리뷰어는 항상 job 로그를 확인합니다. 실패했다면 차이를 꼼꼼히 살펴봅니다. 공백과 무관한 차이는 머지 리퀘스트 작성자와 논의해야 합니다.
- 거짓 양성인 경우(예: ClickHouse 버전 불일치로 인한 차이) 머지 리퀘스트에
pipeline:skip-check-clickhouse-schema레이블을 추가해 이 검사를 건너뜁니다.
실제 스키마 차이를 해결하려면 작성자가 모든 마이그레이션을 실행한 뒤 스키마를 덤프해 볼 수 있습니다.
bundle exec rake gitlab:clickhouse:migrate; bundle exec rake gitlab:clickhouse:schema:dump
데이터베이스 쿼리 리뷰#
GitLab의 ClickHouse 쿼리는 두 가지 방식으로 작성할 수 있습니다.
- 원시 SQL 쿼리
- QueryBuilder - ActiveRecord와 비슷한 추상화입니다(문서)
원시 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초 이내로 유지하는 것을 목표로 합니다. 성능 기준은 기능 사용 방식과 데이터셋 크기에 따라 달라집니다.
쿼리를 리뷰할 때는 다음을 수행합니다.
- 코드에서 원시 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/', 한 달 기간의 날짜 범위). -
쿼리를 실행하고 경과 시간과 읽은 행 수를 기록합니다.
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 granules는 테이블의 일부만 스캔했다는 뜻이며, 성능 측면에서 이상적입니다.
성능에 대해 논의를 제기할 시점은 다음과 같습니다.
- 쿼리가 1천만 행이 넘는 데이터를 스캔합니다.
- 쿼리 실행 시간이 지속적으로 5~10초를 넘습니다.
- 쿼리가 자주 실행될 예정입니다.
성능 검증에는 규모가 큰 네임스페이스(예: gitlab-org 또는 gitlab-org/gitlab)의 실제 데이터 또는 합성 데이터를 사용합니다.
새 구체화된 뷰 리뷰#
구체화된 뷰가 POPULATE 키워드와 함께 생성되었는지, 또는 대규모 데이터셋을 위한 백필 마이그레이션이 있는지 확인합니다.
테이블 엔진별 동작#
MergeTree 계열에서 기본 키(즉 ORDER BY)는 정렬과 인덱스를 정의할 뿐 고유성 제약이 아닙니다. 기본 키 값이 같은 행이 함께 존재할 수 있습니다. 데이터 적재 파이프라인이 중복이나 갱신을 만들어 낼 수 있다면 읽는 시점에 이를 처리하거나, 버전을 병합하는 엔진을 선택해야 합니다.
MergeTree engine#
- 자동 중복 제거가 없습니다.
- 데이터가 오직 추가만 되고 중복이 발생할 수 없을 때 사용합니다(예: 변경되지 않는 이벤트 로그).
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 비용이 매우 클 수 있습니다. 위에서 설명한 쿼리 시점 중복 제거 패턴을 우선합니다.