InfoGrab DocsInfoGrab Docs

ClickHouse를 활용한 머지 리퀘스트 애널리틱스

요약

머지 리퀘스트 애널리틱스 기능은 프로젝트에서 머지된 머지 리퀘스트에 대한 통계를 보여 주고 레코드 수준의 메타데이터도 함께 제공합니다. 차트 아래에서는 페이지당 12개월 단위로 페이지가 나뉜 머지 리퀘스트 목록을 확인할 수 있습니다.

머지 리퀘스트 애널리틱스 기능은 프로젝트에서 머지된 머지 리퀘스트에 대한 통계를 보여 주고 레코드 수준의 메타데이터도 함께 제공합니다. 집계 항목은 다음과 같습니다.

  • 평균 머지 소요 시간: 생성 시각과 머지 시각 사이의 기간입니다.
  • 월별 집계: 머지된 머지 리퀘스트를 12개월치로 보여 주는 차트입니다.

차트 아래에서는 페이지당 12개월 단위로 페이지가 나뉜 머지 리퀘스트 목록을 확인할 수 있습니다.

다음 기준으로 필터링할 수 있습니다.

  • Author
  • Assignee
  • Labels
  • Milestone
  • Source branch
  • Target branch

현재 성능 문제#

  • 집계 쿼리에는 전용 인덱스가 필요하며, 이로 인해 디스크 공간이 추가로 듭니다(인덱스 전용 스캔).
  • 12개월 전체를 쿼리하면 느립니다(문 실행 시간 초과). 그래서 프론트엔드가 월 단위로 데이터를 요청합니다(데이터베이스 쿼리 12회).
  • 전용 인덱스를 사용하더라도 머지 리퀘스트 물량이 많아 그룹 수준에서 이 기능을 제공하기는 어렵습니다.

예시 쿼리#

특정 월에 머지된 머지 리퀘스트 수를 가져옵니다.

SELECT COUNT(*)
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-12-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2023-01-01 00:00:00'

merge_request_metrics 테이블은 첫 페이지 로드 시간을 개선하기 위해 (target_project_id를 추가하는 방식으로) 비정규화했습니다. 이 쿼리는 날짜 범위가 좁을 때는 잘 동작하지만, 범위가 넓어지면 시간 초과가 발생할 수 있습니다.

필터를 하나 더 추가하면 merge_requests 테이블까지 필터링해야 하므로 쿼리가 더 복잡해집니다.

SELECT COUNT(*)
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_requests"."author_id" IN
    (SELECT "users"."id"
     FROM "users"
     WHERE (LOWER("users"."username") IN (LOWER('ahegyi'))))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-12-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2023-01-01 00:00:00'

평균 머지 소요 시간을 계산하기 위해 머지 리퀘스트 생성 시각과 머지 시각 사이의 총 시간도 함께 쿼리합니다.

SELECT EXTRACT(epoch
               FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_requests"."author_id" IN
    (SELECT "users"."id"
     FROM "users"
     WHERE (LOWER("users"."username") IN (LOWER('ahegyi'))))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-08-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2022-09-01 00:00:00'
  AND "merge_request_metrics"."merged_at" > "merge_request_metrics"."created_at"
LIMIT 1

ClickHouse에 머지 리퀘스트 데이터 저장#

ClickHouse에 머지 리퀘스트 데이터를 저장하고 쿼리하는 사용 사례는 이 밖에도 여러 가지가 있습니다. 이 문서에서는 이 기능 하나에 집중합니다.

핵심 데이터는 merge_request_metrics와 merge_requests 데이터베이스 테이블에 있습니다. 일부 필터는 추가 테이블 조인이 필요합니다.

  • banned_users: 차단된 사용자가 생성한 머지 리퀘스트를 걸러냅니다.
  • labels: 머지 리퀘스트에는 레이블이 하나 이상 지정될 수 있습니다.
  • assignees: 머지 리퀘스트에는 담당자가 한 명 이상 지정될 수 있습니다.
  • merged_at: merged_at 칼럼은 merge_request_metrics 테이블에 있습니다.

merge_requests 테이블에는 직접 필터링할 수 있는 데이터가 들어 있습니다.

  • Author: author_id 칼럼을 사용합니다.
  • Milestone: milestone_id 칼럼을 사용합니다.
  • Source branch.
  • Target branch.
  • Project: project_id 칼럼을 사용합니다.

ClickHouse 데이터를 최신 상태로 유지#

아쉽게도 merge_requests 테이블을 복제하거나 동기화하는 것만으로는 충분하지 않습니다. 비정규화된 merge_requests 행 하나를 ClickHouse 데이터베이스에 삽입하려면 연관 테이블에 대한 별도 쿼리가 필요합니다.

변경 감지는 구현이 간단하지 않습니다. 다음과 같이 범위를 줄일 여지는 있습니다.

  • 이 기능은 GitLab Premium 및 GitLab Ultimate 고객이 사용할 수 있습니다. 따라서 모든 데이터를 동기화할 필요 없이, 라이선스가 적용된 그룹에 속한 merge_requests 레코드만 동기화하면 됩니다.
  • 데이터 변경은 (대체로) MergeRequest 서비스를 통해 일어나며, 여기서는 updated_at 타임스탬프 칼럼이 대체로 일관되게 갱신됩니다. 일종의 증분 동기화 프로세스를 구현할 수 있습니다.
  • 쿼리해야 하는 대상은 머지된 머지 리퀘스트뿐입니다. 머지 후에는 레코드가 거의 바뀌지 않습니다.

데이터베이스 테이블 구조#

데이터베이스 테이블 구조는 비정규화를 사용해 필요한 모든 칼럼을 하나의 데이터베이스 테이블에 담습니다. 덕분에 JOINs 이 필요 없습니다.

CREATE TABLE merge_requests
(
    `id` UInt64,
    `project_id` UInt64 DEFAULT 0 NOT NULL,
    `author_id` UInt64 DEFAULT 0 NOT NULL,
    `milestone_id` UInt64 DEFAULT 0 NOT NULL,
    `label_ids` Array(UInt64) DEFAULT [] NOT NULL,
    `assignee_ids` Array(UInt64) DEFAULT [] NOT NULL,
    `source_branch` String DEFAULT '' NOT NULL,
    `target_branch` String DEFAULT '' NOT NULL,
    `merged_at` DateTime64(6, 'UTC') NOT NULL,
    `created_at` DateTime64(6, 'UTC') DEFAULT now64(6, 'UTC') NOT NULL,
    `updated_at` DateTime64(6, 'UTC') DEFAULT now64(6, 'UTC') NOT NULL
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY (project_id, merged_at, id);

활동 데이터 예시와 마찬가지로 ReplacingMergeTree 엔진을 사용합니다. 머지 리퀘스트 레코드의 여러 칼럼이 바뀔 수 있으므로, 테이블을 최신 상태로 유지하는 것이 중요합니다.

이 데이터베이스 테이블은 project_id, merged_at, id 칼럼 순으로 정렬됩니다. 이 정렬은 프로젝트 안에서 merged_at 칼럼을 쿼리하는 이번 사용 사례에 맞게 테이블 데이터를 최적화합니다.

카운트 쿼리 재작성#

먼저 테이블에 넣을 데이터를 생성합니다.

INSERT INTO merge_requests (id, project_id, author_id, milestone_id, label_ids, merged_at, created_at)
SELECT id, project_id, author_id, milestone_id, label_ids, merged_at, created_at
FROM generateRandom('id UInt64, project_id UInt8, author_id UInt8, milestone_id UInt8, label_ids Array(UInt8), merged_at DateTime64(6, \'UTC\'), created_at DateTime64(6, \'UTC\')')
LIMIT 1000000;
Note

일부 정수 데이터 유형을 UInt8로 캐스팅했으므로 서로 다른 행에서 같은 값이 나올 가능성이 매우 높습니다.

원래의 카운트 쿼리는 한 달치 데이터만 집계했습니다. ClickHouse에서는 1년치 데이터를 한 번에 집계해 볼 수 있습니다.

PostgreSQL 기반 카운트 쿼리는 다음과 같습니다.

SELECT COUNT(*)
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-12-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2023-01-01 00:00:00'

ClickHouse 쿼리는 다음과 같습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  project_id = 200
  AND merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'
GROUP BY year, month

이 쿼리가 처리한 행 수는 생성한 데이터에 비해 훨씬 적었습니다. ORDER BY 절(기본 키)이 쿼리 실행에 도움이 됩니다.

11 rows in set. Elapsed: 0.010 sec.
Processed 8.19 thousand rows, 131.07 KB (783.45 thousand rows/s., 12.54 MB/s.)

평균 머지 소요 시간 쿼리 재작성#

이 쿼리는 평균 머지 소요 시간을 duration(created_at, merged_at) / merge_request_count로 계산합니다. 계산은 두 단계로 나뉘어 진행됩니다.

  1. 월별 건수와 월별 소요 시간 값을 요청합니다.
  2. 건수를 합산해 연간 건수를 구합니다.
  3. 소요 시간을 합산해 연간 소요 시간을 구합니다.
  4. 소요 시간을 건수로 나눕니다.

ClickHouse에서는 평균 머지 소요 시간을 쿼리 한 번으로 계산할 수 있습니다.

SELECT
  SUM(
    dateDiff('second', merged_at, created_at) / 3600 / 24
  ) / COUNT(*) AS mean_time_to_merge -- mean_time_to_merge is in days
FROM merge_requests
WHERE
  project_id = 200
  AND merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'

필터링#

위의 데이터베이스 쿼리는 기본 쿼리로 사용할 수 있습니다. 여기에 필터를 더 추가할 수 있습니다. 예를 들어 레이블과 마일스톤으로 필터링하면 다음과 같습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  project_id = 200
  AND milestone_id = 15
  AND has(label_ids, 118)
  AND -- array includes 118
  merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'
GROUP BY year, month

특정 필터의 최적화는 보통 데이터베이스 인덱스로 처리합니다. 이 쿼리는 8,000행을 읽습니다.

1 row in set. Elapsed: 0.016 sec.
Processed 8.19 thousand rows, 589.99 KB (505.38 thousand rows/s., 36.40 MB/s.)

milestone_id에 인덱스를 추가하면 다음과 같습니다.

ALTER TABLE merge_requests
ADD
  INDEX milestone_id_index milestone_id TYPE minmax GRANULARITY 10;
ALTER TABLE
  merge_requests MATERIALIZE INDEX milestone_id_index;

생성한 데이터에서는 인덱스를 추가해도 성능이 개선되지 않았습니다.

차단된 사용자 필터#

GitLab에 최근 추가된 기능은 관리자가 차단한 사용자가 작성자인 머지 리퀘스트를 걸러냅니다. 차단된 사용자는 인스턴스 수준에서 banned_users 데이터베이스 테이블에 기록됩니다.

아이디어 1: 차단된 사용자 ID 열거#

이 방식에서는 ClickHouse 데이터베이스 스키마를 구조적으로 바꿀 필요가 없습니다. 프로젝트에서 차단된 사용자를 쿼리한 뒤 쿼리 시점에 그 값을 걸러내면 됩니다.

차단된 사용자를 가져옵니다(PostgreSQL).

SELECT user_id FROM banned_users

ClickHouse에서는 다음과 같습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  author_id NOT IN (1, 2, 3, 4) AND -- banned users
  project_id = 200
  AND milestone_id = 15
  AND has(label_ids, 118) AND -- array includes 118
  merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'
GROUP BY year, month

이 방식의 문제는 차단된 사용자 수가 크게 늘어나면 쿼리가 길어지고 느려진다는 점입니다.

아이디어 2: banned_users 테이블 복제#

banned_users 테이블이 수백만 행까지 커지지 않는다고 가정하면, 테이블 전체를 주기적으로 ClickHouse로 동기화해 볼 수 있습니다. 이 방식을 쓰면 대체로 일관된 상태의 banned_users 테이블을 ClickHouse 데이터베이스 쿼리에서 사용할 수 있습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  author_id NOT IN (SELECT user_id FROM banned_users) AND
  project_id = 200 AND
  milestone_id = 15 AND
  has(label_ids, 118) AND -- array includes 118
  merged_at BETWEEN '2022-01-01 00:00:00' AND '2023-01-01 00:00:00'
GROUP BY year, month

또는 쿼리 성능을 더 높이기 위해 banned_users 테이블을 딕셔너리로 저장할 수도 있습니다.

아이디어 3: 기능 변경#

분석 목적의 계산에서는 이 필터를 아예 제외하는 방안도 받아들일 수 있습니다. 이 방식은 차단된 사용자의 머지 리퀘스트를 포함해도 통계가 크게 왜곡되지 않는다는 전제를 둡니다.

ClickHouse를 활용한 머지 리퀘스트 애널리틱스

GitLab v19.4
원문 보기

요약

머지 리퀘스트 애널리틱스 기능은 프로젝트에서 머지된 머지 리퀘스트에 대한 통계를 보여 주고 레코드 수준의 메타데이터도 함께 제공합니다. 차트 아래에서는 페이지당 12개월 단위로 페이지가 나뉜 머지 리퀘스트 목록을 확인할 수 있습니다.

머지 리퀘스트 애널리틱스 기능은 프로젝트에서 머지된 머지 리퀘스트에 대한 통계를 보여 주고 레코드 수준의 메타데이터도 함께 제공합니다. 집계 항목은 다음과 같습니다.

  • 평균 머지 소요 시간: 생성 시각과 머지 시각 사이의 기간입니다.
  • 월별 집계: 머지된 머지 리퀘스트를 12개월치로 보여 주는 차트입니다.

차트 아래에서는 페이지당 12개월 단위로 페이지가 나뉜 머지 리퀘스트 목록을 확인할 수 있습니다.

다음 기준으로 필터링할 수 있습니다.

  • Author
  • Assignee
  • Labels
  • Milestone
  • Source branch
  • Target branch

현재 성능 문제#

  • 집계 쿼리에는 전용 인덱스가 필요하며, 이로 인해 디스크 공간이 추가로 듭니다(인덱스 전용 스캔).
  • 12개월 전체를 쿼리하면 느립니다(문 실행 시간 초과). 그래서 프론트엔드가 월 단위로 데이터를 요청합니다(데이터베이스 쿼리 12회).
  • 전용 인덱스를 사용하더라도 머지 리퀘스트 물량이 많아 그룹 수준에서 이 기능을 제공하기는 어렵습니다.

예시 쿼리#

특정 월에 머지된 머지 리퀘스트 수를 가져옵니다.

SELECT COUNT(*)
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-12-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2023-01-01 00:00:00'

merge_request_metrics 테이블은 첫 페이지 로드 시간을 개선하기 위해 (target_project_id를 추가하는 방식으로) 비정규화했습니다. 이 쿼리는 날짜 범위가 좁을 때는 잘 동작하지만, 범위가 넓어지면 시간 초과가 발생할 수 있습니다.

필터를 하나 더 추가하면 merge_requests 테이블까지 필터링해야 하므로 쿼리가 더 복잡해집니다.

SELECT COUNT(*)
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_requests"."author_id" IN
    (SELECT "users"."id"
     FROM "users"
     WHERE (LOWER("users"."username") IN (LOWER('ahegyi'))))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-12-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2023-01-01 00:00:00'

평균 머지 소요 시간을 계산하기 위해 머지 리퀘스트 생성 시각과 머지 시각 사이의 총 시간도 함께 쿼리합니다.

SELECT EXTRACT(epoch
               FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_requests"."author_id" IN
    (SELECT "users"."id"
     FROM "users"
     WHERE (LOWER("users"."username") IN (LOWER('ahegyi'))))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-08-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2022-09-01 00:00:00'
  AND "merge_request_metrics"."merged_at" > "merge_request_metrics"."created_at"
LIMIT 1

ClickHouse에 머지 리퀘스트 데이터 저장#

ClickHouse에 머지 리퀘스트 데이터를 저장하고 쿼리하는 사용 사례는 이 밖에도 여러 가지가 있습니다. 이 문서에서는 이 기능 하나에 집중합니다.

핵심 데이터는 merge_request_metrics와 merge_requests 데이터베이스 테이블에 있습니다. 일부 필터는 추가 테이블 조인이 필요합니다.

  • banned_users: 차단된 사용자가 생성한 머지 리퀘스트를 걸러냅니다.
  • labels: 머지 리퀘스트에는 레이블이 하나 이상 지정될 수 있습니다.
  • assignees: 머지 리퀘스트에는 담당자가 한 명 이상 지정될 수 있습니다.
  • merged_at: merged_at 칼럼은 merge_request_metrics 테이블에 있습니다.

merge_requests 테이블에는 직접 필터링할 수 있는 데이터가 들어 있습니다.

  • Author: author_id 칼럼을 사용합니다.
  • Milestone: milestone_id 칼럼을 사용합니다.
  • Source branch.
  • Target branch.
  • Project: project_id 칼럼을 사용합니다.

ClickHouse 데이터를 최신 상태로 유지#

아쉽게도 merge_requests 테이블을 복제하거나 동기화하는 것만으로는 충분하지 않습니다. 비정규화된 merge_requests 행 하나를 ClickHouse 데이터베이스에 삽입하려면 연관 테이블에 대한 별도 쿼리가 필요합니다.

변경 감지는 구현이 간단하지 않습니다. 다음과 같이 범위를 줄일 여지는 있습니다.

  • 이 기능은 GitLab Premium 및 GitLab Ultimate 고객이 사용할 수 있습니다. 따라서 모든 데이터를 동기화할 필요 없이, 라이선스가 적용된 그룹에 속한 merge_requests 레코드만 동기화하면 됩니다.
  • 데이터 변경은 (대체로) MergeRequest 서비스를 통해 일어나며, 여기서는 updated_at 타임스탬프 칼럼이 대체로 일관되게 갱신됩니다. 일종의 증분 동기화 프로세스를 구현할 수 있습니다.
  • 쿼리해야 하는 대상은 머지된 머지 리퀘스트뿐입니다. 머지 후에는 레코드가 거의 바뀌지 않습니다.

데이터베이스 테이블 구조#

데이터베이스 테이블 구조는 비정규화를 사용해 필요한 모든 칼럼을 하나의 데이터베이스 테이블에 담습니다. 덕분에 JOINs 이 필요 없습니다.

CREATE TABLE merge_requests
(
    `id` UInt64,
    `project_id` UInt64 DEFAULT 0 NOT NULL,
    `author_id` UInt64 DEFAULT 0 NOT NULL,
    `milestone_id` UInt64 DEFAULT 0 NOT NULL,
    `label_ids` Array(UInt64) DEFAULT [] NOT NULL,
    `assignee_ids` Array(UInt64) DEFAULT [] NOT NULL,
    `source_branch` String DEFAULT '' NOT NULL,
    `target_branch` String DEFAULT '' NOT NULL,
    `merged_at` DateTime64(6, 'UTC') NOT NULL,
    `created_at` DateTime64(6, 'UTC') DEFAULT now64(6, 'UTC') NOT NULL,
    `updated_at` DateTime64(6, 'UTC') DEFAULT now64(6, 'UTC') NOT NULL
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY (project_id, merged_at, id);

활동 데이터 예시와 마찬가지로 ReplacingMergeTree 엔진을 사용합니다. 머지 리퀘스트 레코드의 여러 칼럼이 바뀔 수 있으므로, 테이블을 최신 상태로 유지하는 것이 중요합니다.

이 데이터베이스 테이블은 project_id, merged_at, id 칼럼 순으로 정렬됩니다. 이 정렬은 프로젝트 안에서 merged_at 칼럼을 쿼리하는 이번 사용 사례에 맞게 테이블 데이터를 최적화합니다.

카운트 쿼리 재작성#

먼저 테이블에 넣을 데이터를 생성합니다.

INSERT INTO merge_requests (id, project_id, author_id, milestone_id, label_ids, merged_at, created_at)
SELECT id, project_id, author_id, milestone_id, label_ids, merged_at, created_at
FROM generateRandom('id UInt64, project_id UInt8, author_id UInt8, milestone_id UInt8, label_ids Array(UInt8), merged_at DateTime64(6, \'UTC\'), created_at DateTime64(6, \'UTC\')')
LIMIT 1000000;
Note

일부 정수 데이터 유형을 UInt8로 캐스팅했으므로 서로 다른 행에서 같은 값이 나올 가능성이 매우 높습니다.

원래의 카운트 쿼리는 한 달치 데이터만 집계했습니다. ClickHouse에서는 1년치 데이터를 한 번에 집계해 볼 수 있습니다.

PostgreSQL 기반 카운트 쿼리는 다음과 같습니다.

SELECT COUNT(*)
FROM "merge_requests"
INNER JOIN "merge_request_metrics" ON "merge_request_metrics"."merge_request_id" = "merge_requests"."id"
WHERE (NOT EXISTS
         (SELECT 1
          FROM "banned_users"
          WHERE (merge_requests.author_id = banned_users.user_id)))
  AND "merge_request_metrics"."target_project_id" = 278964
  AND "merge_request_metrics"."merged_at" >= '2022-12-01 00:00:00'
  AND "merge_request_metrics"."merged_at" <= '2023-01-01 00:00:00'

ClickHouse 쿼리는 다음과 같습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  project_id = 200
  AND merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'
GROUP BY year, month

이 쿼리가 처리한 행 수는 생성한 데이터에 비해 훨씬 적었습니다. ORDER BY 절(기본 키)이 쿼리 실행에 도움이 됩니다.

11 rows in set. Elapsed: 0.010 sec.
Processed 8.19 thousand rows, 131.07 KB (783.45 thousand rows/s., 12.54 MB/s.)

평균 머지 소요 시간 쿼리 재작성#

이 쿼리는 평균 머지 소요 시간을 duration(created_at, merged_at) / merge_request_count로 계산합니다. 계산은 두 단계로 나뉘어 진행됩니다.

  1. 월별 건수와 월별 소요 시간 값을 요청합니다.
  2. 건수를 합산해 연간 건수를 구합니다.
  3. 소요 시간을 합산해 연간 소요 시간을 구합니다.
  4. 소요 시간을 건수로 나눕니다.

ClickHouse에서는 평균 머지 소요 시간을 쿼리 한 번으로 계산할 수 있습니다.

SELECT
  SUM(
    dateDiff('second', merged_at, created_at) / 3600 / 24
  ) / COUNT(*) AS mean_time_to_merge -- mean_time_to_merge is in days
FROM merge_requests
WHERE
  project_id = 200
  AND merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'

필터링#

위의 데이터베이스 쿼리는 기본 쿼리로 사용할 수 있습니다. 여기에 필터를 더 추가할 수 있습니다. 예를 들어 레이블과 마일스톤으로 필터링하면 다음과 같습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  project_id = 200
  AND milestone_id = 15
  AND has(label_ids, 118)
  AND -- array includes 118
  merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'
GROUP BY year, month

특정 필터의 최적화는 보통 데이터베이스 인덱스로 처리합니다. 이 쿼리는 8,000행을 읽습니다.

1 row in set. Elapsed: 0.016 sec.
Processed 8.19 thousand rows, 589.99 KB (505.38 thousand rows/s., 36.40 MB/s.)

milestone_id에 인덱스를 추가하면 다음과 같습니다.

ALTER TABLE merge_requests
ADD
  INDEX milestone_id_index milestone_id TYPE minmax GRANULARITY 10;
ALTER TABLE
  merge_requests MATERIALIZE INDEX milestone_id_index;

생성한 데이터에서는 인덱스를 추가해도 성능이 개선되지 않았습니다.

차단된 사용자 필터#

GitLab에 최근 추가된 기능은 관리자가 차단한 사용자가 작성자인 머지 리퀘스트를 걸러냅니다. 차단된 사용자는 인스턴스 수준에서 banned_users 데이터베이스 테이블에 기록됩니다.

아이디어 1: 차단된 사용자 ID 열거#

이 방식에서는 ClickHouse 데이터베이스 스키마를 구조적으로 바꿀 필요가 없습니다. 프로젝트에서 차단된 사용자를 쿼리한 뒤 쿼리 시점에 그 값을 걸러내면 됩니다.

차단된 사용자를 가져옵니다(PostgreSQL).

SELECT user_id FROM banned_users

ClickHouse에서는 다음과 같습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  author_id NOT IN (1, 2, 3, 4) AND -- banned users
  project_id = 200
  AND milestone_id = 15
  AND has(label_ids, 118) AND -- array includes 118
  merged_at BETWEEN '2022-01-01 00:00:00'
  AND '2023-01-01 00:00:00'
GROUP BY year, month

이 방식의 문제는 차단된 사용자 수가 크게 늘어나면 쿼리가 길어지고 느려진다는 점입니다.

아이디어 2: banned_users 테이블 복제#

banned_users 테이블이 수백만 행까지 커지지 않는다고 가정하면, 테이블 전체를 주기적으로 ClickHouse로 동기화해 볼 수 있습니다. 이 방식을 쓰면 대체로 일관된 상태의 banned_users 테이블을 ClickHouse 데이터베이스 쿼리에서 사용할 수 있습니다.

SELECT
  toYear(merged_at) AS year,
  toMonth(merged_at) AS month,
  COUNT(*)
FROM merge_requests
WHERE
  author_id NOT IN (SELECT user_id FROM banned_users) AND
  project_id = 200 AND
  milestone_id = 15 AND
  has(label_ids, 118) AND -- array includes 118
  merged_at BETWEEN '2022-01-01 00:00:00' AND '2023-01-01 00:00:00'
GROUP BY year, month

또는 쿼리 성능을 더 높이기 위해 banned_users 테이블을 딕셔너리로 저장할 수도 있습니다.

아이디어 3: 기능 변경#

분석 목적의 계산에서는 이 필터를 아예 제외하는 방안도 받아들일 수 있습니다. 이 방식은 차단된 사용자의 머지 리퀘스트를 포함해도 통계가 크게 왜곡되지 않는다는 전제를 둡니다.