InfoGrab DocsInfoGrab Docs

ClickHouse 리뷰어 가이드라인

요약

이 페이지에서는 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 쿼리는 두 가지 방식으로 작성할 수 있습니다.

  1. 원시 SQL 쿼리
  2. 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초 이내로 유지하는 것을 목표로 합니다. 성능 기준은 기능 사용 방식과 데이터셋 크기에 따라 달라집니다.

쿼리를 리뷰할 때는 다음을 수행합니다.

  1. 코드에서 원시 SQL 이 명확하게 보이지 않으면 작성자에게 제공을 요청합니다.
  2. 파티셔닝과 기본 키를 파악하기 위해 테이블 구조(SHOW CREATE TABLE table_name FORMAT raw)를 확인합니다.
  3. 쿼리 필터가 테이블의 기본 키 또는 파티셔닝 칼럼과 맞는지 확인합니다.

예시 쿼리는 다음과 같습니다.

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 유형)는 선택 사항입니다.
Note

버전 파라미터를 생략하면 병합 후 남는 중복 제거 행이 임의로 정해집니다.

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;
Note

운영 쿼리에서는 FINAL을 피합니다. FINAL은 즉석 병합을 강제하므로 I/O 비용이 매우 클 수 있습니다. 위에서 설명한 쿼리 시점 중복 제거 패턴을 우선합니다.

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 쿼리는 두 가지 방식으로 작성할 수 있습니다.

  1. 원시 SQL 쿼리
  2. 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초 이내로 유지하는 것을 목표로 합니다. 성능 기준은 기능 사용 방식과 데이터셋 크기에 따라 달라집니다.

쿼리를 리뷰할 때는 다음을 수행합니다.

  1. 코드에서 원시 SQL 이 명확하게 보이지 않으면 작성자에게 제공을 요청합니다.
  2. 파티셔닝과 기본 키를 파악하기 위해 테이블 구조(SHOW CREATE TABLE table_name FORMAT raw)를 확인합니다.
  3. 쿼리 필터가 테이블의 기본 키 또는 파티셔닝 칼럼과 맞는지 확인합니다.

예시 쿼리는 다음과 같습니다.

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 유형)는 선택 사항입니다.
Note

버전 파라미터를 생략하면 병합 후 남는 중복 제거 행이 임의로 정해집니다.

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;
Note

운영 쿼리에서는 FINAL을 피합니다. FINAL은 즉석 병합을 강제하므로 I/O 비용이 매우 클 수 있습니다. 위에서 설명한 쿼리 시점 중복 제거 패턴을 우선합니다.