집계 프레임워크(Aggregations Framework)
집계 프레임워크는 서로 다른 데이터베이스 백엔드에서 분석 쿼리를 구성하기 위한 통합 인터페이스를 제공합니다. ActiveRecord 엔진(Gitlab::Database::Aggregation::ActiveRecord::Engine)은 ActiveRecord의 쿼리 인터페이스를 사용하여 PostgreSQL 쿼리를 생성합니다.
집계 프레임워크는 서로 다른 데이터베이스 백엔드에서 분석 쿼리를 구성하기 위한 통합 인터페이스를 제공합니다. PostgreSQL(ActiveRecord를 통한)과 ClickHouse를 모두 지원하며, 개발자가 메트릭, 차원, 필터가 있는 재사용 가능한 집계 엔진을 정의할 수 있도록 합니다.
ActiveRecord 엔진 정의#
ActiveRecord 엔진(Gitlab::Database::Aggregation::ActiveRecord::Engine)은 ActiveRecord의 쿼리 인터페이스를 사용하여 PostgreSQL 쿼리를 생성합니다.
ActiveRecord 엔진 예시#
class IssueAggregationEngine < Gitlab::Database::Aggregation::ActiveRecord::Engine
filters do
exact_match :project_id, :integer, description: 'Filter by project ID'
exact_match :state, :string, description: 'Filter by issue state'
end
dimensions do
column :author_id, :integer, description: 'Group by author'
date_bucket :created_at, :datetime,
parameters: { granularity: { in: %i[daily weekly monthly yearly], type: :string } },
description: 'Group by creation date'
end
metrics do
count description: 'Total number of issues'
mean :weight, :float, description: 'Average issue weight'
end
end
ActiveRecord 엔진은 단일 수준 SQL 쿼리를 생성합니다:
SELECT
"issues"."author_id" AS aeq_author_id,
date_trunc('month', "issues"."created_at") AS aeq_created_at,
COUNT(*) AS aeq_total_count,
AVG("issues"."weight") AS aeq_mean_weight
FROM "issues"
WHERE "issues"."project_id" IN (1, 2, 3)
AND "issues"."state" IN ('opened')
GROUP BY aeq_author_id, aeq_created_at
ORDER BY aeq_author_id, aeq_created_at
주요 특징:
- 모든 컬럼은
aeq_(Aggregation Engine Query)로 접두사가 붙습니다. 이 접두사는AggregationResult객체에 의해 제거됩니다. - 필터는
WHERE또는HAVING절로 적용됩니다 - 차원은
GROUP BY컬럼이 됩니다 - 메트릭은 집계 함수(
COUNT,AVG)를 사용합니다
사용 가능한 구성 요소#
count 메트릭#
COUNT(*)를 사용하여 행을 카운트합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 아니오 | 카운트 메트릭 이름. 기본값: 'total'. 식별자는 :{name}_count |
type |
Symbol | 아니오 | 데이터 타입. 기본값: :integer |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
mean 메트릭#
AVG()를 사용하여 평균값을 계산합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 평균을 구할 컬럼 이름. 식별자는 :mean_{name} |
type |
Symbol | 아니오 | 데이터 타입. 기본값: :float |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 Arel 표현식 |
scope_proc |
Proc | 아니오 | ActiveRecord 스코프 수정 (예: JOIN용) |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
column 차원#
컬럼 값으로 결과를 그룹화합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 컬럼 이름 또는 식별자 |
type |
Symbol | 예 | 데이터 타입 (:string, :integer, :datetime 등) |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 Arel 표현식 |
scope_proc |
Proc | 아니오 | ActiveRecord 스코프 수정 (예: JOIN용) |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
date_bucket 차원#
PostgreSQL의 date_trunc() 함수를 사용하여 시간 간격으로 결과를 그룹화합니다. 매개변수를 지원합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 날짜/datetime 컬럼 이름 |
type |
Symbol | 예 | 데이터 타입 (:date 또는 :datetime) |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 Arel 표현식 |
scope_proc |
Proc | 아니오 | ActiveRecord 스코프 수정 |
parameters |
Hash | 아니오 | 매개변수 구성 (아래 참조) |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
지원되는 매개변수:
| 매개변수 | 타입 | 값 | 기본값 | 설명 |
|---|---|---|---|---|
granularity |
String | daily, weekly, monthly, yearly |
monthly |
그룹화를 위한 시간 간격 |
exact_match 필터#
WHERE column IN (...)을 사용하여 정확한 값 일치로 행을 필터링합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 필터링할 컬럼 이름 |
type |
Symbol | 예 | 필터 값의 데이터 타입 |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 Arel 표현식 |
max_size |
Integer | 아니오 | 필터에 허용되는 최대 값 수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
ClickHouse 엔진 정의#
ClickHouse 엔진(Gitlab::Database::Aggregation::ClickHouse::Engine)은 ClickHouse의 컬럼형 데이터베이스에 최적화된 쿼리를 생성합니다.
ClickHouse 엔진 예시#
class SessionAnalyticsEngine < Gitlab::Database::Aggregation::ClickHouse::Engine
self.table_name = 'sessions'
filters do
exact_match :flow_type, :string, description: 'Filter by flow type'
range :created_at, :datetime, description: 'Filter by creation date'
end
dimensions do
column :flow_type, :string, description: 'Group by flow type'
date_bucket :created_at, :datetime,
parameters: { granularity: { in: %i[daily weekly monthly], type: :string } },
description: 'Group by date'
end
metrics do
count description: 'Total sessions'
count :completed, :integer,
expression: -> { Arel.sql('1') },
if: -> { Arel.sql('finished_at IS NOT NULL') },
description: 'Completed sessions'
mean :duration, :float,
expression: -> { Arel.sql('finished_at - created_at') },
if: -> { Arel.sql('finished_at IS NOT NULL') },
description: 'Average session duration'
rate :completion,
numerator_if: -> { Arel.sql('finished_at IS NOT NULL') },
description: 'Session completion rate'
quantile :duration, :float,
expression: -> { Arel.sql('finished_at - created_at') },
parameters: { quantile: { type: :float, description: 'Quantile value (0.0-1.0)' } },
description: 'Duration percentile'
end
end
ClickHouse 엔진은 최적의 성능을 위해 2단계 중첩 쿼리를 생성합니다. 전체 구조는 다음과 같이 표현할 수 있습니다:
-- 쿼리 구조를 강조하는 메타코드 쿼리
SELECT dimensions, metrics
FROM (
SELECT
primary_key_columns,
dimensions_expressions,
metrics_expressions,
FROM source_table
WHERE filters
GROUP BY ALL
) ch_aggregation_inner_query
GROUP BY ALL
ORDER BY orders
내부 쿼리는 소스 테이블의 각 기본 키에 대한 데이터를 사전 계산합니다. 외부 쿼리는 내부 쿼리를 기반으로 메트릭과 차원을 계산합니다.
전체 쿼리 예시:
SELECT
`ch_aggregation_inner_query`.`aeq_flow_type` AS aeq_flow_type,
toStartOfInterval(
`ch_aggregation_inner_query`.`aeq_created_at`,
INTERVAL 1 month
) AS aeq_created_at,
COUNT(*) AS aeq_total_count,
countIf(`ch_aggregation_inner_query`.`aeq_completed_secondary` = 1) AS aeq_completed_count,
avgIf(
`ch_aggregation_inner_query`.`aeq_mean_duration`,
`ch_aggregation_inner_query`.`aeq_mean_duration_secondary` = 1
) AS aeq_mean_duration,
countIf(`ch_aggregation_inner_query`.`aeq_completion_rate` = 1) / COUNT(*) AS aeq_completion_rate,
quantile(0.5)(`ch_aggregation_inner_query`.`aeq_duration_quantile`) AS aeq_duration_quantile
FROM (
SELECT
`sessions`.`flow_type` AS aeq_flow_type,
`sessions`.`created_at` AS aeq_created_at,
finished_at IS NOT NULL AS aeq_completed_secondary,
finished_at - created_at AS aeq_mean_duration,
finished_at IS NOT NULL AS aeq_mean_duration_secondary,
finished_at IS NOT NULL AS aeq_completion_rate,
finished_at - created_at AS aeq_duration_quantile,
`sessions`.`user_id`,
`sessions`.`session_id`
FROM `sessions`
WHERE `sessions`.`created_at` BETWEEN '2024-01-01' AND '2024-12-31'
GROUP BY ALL
) ch_aggregation_inner_query
GROUP BY ALL
ORDER BY aeq_flow_type, aeq_created_at
주요 특징:
- 2단계 쿼리 구조 (내부 쿼리 + 외부 집계)
- 내부 쿼리는 행 수준 계산 및 기본 키 그룹화를 처리합니다. 외부 쿼리는 최종 집계를 수행합니다. 이 접근 방식을 통해
*Merge컬럼과*If집계를 쉽게 사용할 수 있습니다. - 조건부 메트릭은
*If함수를 사용합니다 - 모든 컬럼은
aeq_(Aggregation Engine Query)로 접두사가 붙습니다. 이 접두사는AggregationResult객체에 의해 제거됩니다. - 컬럼 필터는 내부 쿼리에서
WHERE또는HAVING절로 적용됩니다 - 메트릭 필터는 외부 쿼리에서
HAVING절로 적용됩니다 - 차원은 외부 쿼리에서
GROUP BY컬럼이 됩니다 - 메트릭은 외부 쿼리에서 집계 함수를 사용합니다
사용 가능한 구성 요소#
count 메트릭#
countIf()를 사용하여 고유 카운트와 조건부 카운트를 지원합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 아니오 | 카운트 메트릭 이름. 기본값: 'total'. 식별자는 :{name}_count |
type |
Symbol | 아니오 | 데이터 타입. 기본값: :integer |
expression |
Proc | 아니오 | 특정 값을 카운트하기 위한 커스텀 표현식 |
if |
Proc | 아니오 | 조건부 카운트를 위한 조건 표현식 (countIf) |
distinct |
Boolean | 아니오 | 고유 카운트 활성화. 기본값: false |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
mean 메트릭#
avgIf()를 사용하여 조건부 평균을 지원하며 평균값을 계산합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 컬럼 이름 또는 식별자. 식별자는 :mean_{name} |
type |
Symbol | 아니오 | 데이터 타입. 기본값: :float |
expression |
Proc | 아니오 | 평균을 구할 값에 대한 커스텀 표현식 |
if |
Proc | 아니오 | 조건부 평균을 위한 조건 표현식 (avgIf) |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
rate 메트릭#
분자 조건에 일치하는 행과 분모 조건에 일치하는 행(또는 전체 행) 사이의 비율을 계산합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 식별자 이름. 식별자는 :{name}_rate |
type |
Symbol | 아니오 | 데이터 타입. 기본값: :float |
numerator_if |
Proc | 예 | 분자 조건 (카운트할 행) |
denominator_if |
Proc | 아니오 | 분모 조건. 제공되지 않으면 전체 카운트 사용 |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
quantile 메트릭#
ClickHouse의 quantile() 함수를 사용하여 백분위수를 계산합니다. 매개변수를 지원합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 컬럼 이름 또는 식별자. 식별자는 :{name}_quantile |
type |
Symbol | 아니오 | 데이터 타입. 기본값: :float |
expression |
Proc | 아니오 | 값에 대한 커스텀 표현식 |
parameters |
Hash | 아니오 | 매개변수 구성 (아래 참조) |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
지원되는 매개변수:
| 매개변수 | 타입 | 값 | 기본값 | 설명 |
|---|---|---|---|---|
quantile |
Float | 0.0 - 1.0 |
0.5 |
분위수 값 (0.5 = 중앙값, 0.9 = p90, 0.99 = p99) |
column 차원#
컬럼 값으로 결과를 그룹화합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 컬럼 이름 또는 식별자 |
type |
Symbol | 예 | 데이터 타입 (:string, :integer, :datetime 등) |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 표현식 |
formatter |
Proc | 아니오 | 결과에 적용되는 서식 함수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
association |
Boolean | 아니오 | true이면 _id 접미사 없이도 객체로 차원에 접근 가능합니다. 기본값: false. |
date_bucket 차원#
ClickHouse의 toStartOfInterval() 함수를 사용하여 시간 간격으로 결과를 그룹화합니다. 매개변수를 지원합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 날짜/datetime 컬럼 이름 |
type |
Symbol | 예 | 데이터 타입 (:date 또는 :datetime) |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 표현식 |
parameters |
Hash | 아니오 | 매개변수 구성 (아래 참조) |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
지원되는 매개변수:
| 매개변수 | 타입 | 값 | 기본값 | 설명 |
|---|---|---|---|---|
granularity |
String | daily, weekly, monthly, yearly |
monthly |
그룹화를 위한 시간 간격 |
exact_match 필터#
정확한 값 일치로 행을 필터링합니다. 일반 컬럼 또는 머지 컬럼(사전 집계된 데이터)에서 필터링을 지원합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 필터링할 컬럼 이름 |
type |
Symbol | 예 | 필터 값의 데이터 타입 |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 표현식 |
merge_column |
Boolean | 아니오 | true이면 WHERE 대신 HAVING을 사용하여 필터 적용 |
max_size |
Integer | 아니오 | 필터에 허용되는 최대 값 수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
range 필터#
BETWEEN을 사용하여 값 범위로 행을 필터링합니다. 일반 컬럼 또는 머지 컬럼에서 필터링을 지원합니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 필터링할 컬럼 이름 |
type |
Symbol | 예 | 필터 값의 데이터 타입 (:datetime, :integer 등) |
expression |
Proc | 아니오 | 컬럼 대신 사용하는 커스텀 표현식 |
merge_column |
Boolean | 아니오 | true이면 WHERE 대신 HAVING을 사용하여 필터 적용 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
metric_exact_match 필터#
집계된 메트릭 값에 대한 정확한 일치로 그룹을 필터링합니다. 집계 후 HAVING 절로 적용됩니다.
| 옵션 | 타입 | 필수 | 설명 |
|---|---|---|---|
name |
Symbol | 예 | 필터링할 메트릭의 식별자. 동일한 엔진에 정의된 메트릭과 일치해야 합니다. |
type |
Symbol | 예 | 필터 값의 데이터 타입 |
max_size |
Integer | 아니오 | 필터에 허용되는 최대 값 수 |
description |
String | 아니오 | 사람이 읽을 수 있는 설명 |
참조된 메트릭은 동일한 Request에서도 요청되어야 합니다. 매개변수화된 메트릭의 경우, 필터 parameters는 요청된 메트릭 인스턴스의 매개변수와 일치해야 합니다.
예시:
filters do
metric_exact_match :total_count, :integer
end
Gitlab::Database::Aggregation::Request.new(
filters: [{ identifier: :total_count, values: [1, 2] }],
dimensions: [{ identifier: :user_id }],
metrics: [{ identifier: :total_count }]
)
metric_range 필터#
BETWEEN을 사용하여 집계된 메트릭의 값 범위로 그룹을 필터링합니다. 집계 후 HAVING 절로 적용됩니다.
Transient 컬럼#
Transient 컬럼은 한 번 정의하여 dimensions, metrics, filters 블록 전체에서 참조할 수 있는 이름 있는 SQL 표현식 별칭입니다. 최종 쿼리 결과에는 투영되지 않습니다. 복잡한 SQL 표현식의 중복을 제거하기 위해 transient 컬럼을 사용합니다.
Transient 컬럼 정의#
이름과 Arel 표현식을 반환하는 블록을 사용하여 클래스 수준에서 transient를 호출합니다. 참조하기 전에 transient 컬럼을 정의합니다.
transient(:duration) do
sql("dateDiff('seconds', anyIfMerge(created_event_at), anyIfMerge(finished_event_at))")
end
transient(:is_finished) { sql('anyIfMerge(finished_event_at) IS NOT NULL') }
Transient 컬럼 참조#
dimensions, metrics, filters 블록 내에서 transient(:name)을 호출하여 저장된 표현식을 삽입합니다. 람다 표현식이 허용되는 모든 곳에 반환값을 전달할 수 있습니다: 위치 인수 또는 키워드 인수 값으로.
metrics do
mean :duration, :float, transient(:duration),
description: 'Average session duration in seconds'
count :finished, if: transient(:is_finished),
description: 'Number of finished sessions'
end
프레임워크 사용#
집계 요청 생성#
request = Gitlab::Database::Aggregation::Request.new(
filters: [
{ identifier: :project_id, values: [1, 2, 3] },
{ identifier: :state, values: ['opened'] }
],
dimensions: [
{ identifier: :author_id },
{ identifier: :created_at, parameters: { granularity: 'monthly' } },
{ identifier: :created_at, parameters: { granularity: 'weekly' } },
],
metrics: [
{ identifier: :total_count },
{ identifier: :mean_weight }
],
order: [
{ identifier: :total_count, direction: :desc } # 순서 식별자는 차원 또는 메트릭을 참조해야 합니다.
]
)
엔진으로 요청 실행#
engine = IssueAggregationEngine.new(context: { scope: Issue.all })
response = engine.execute(request)
if response.success?
puts "Success: #{response.payload[:data].to_a.inspect}"
else
puts "Errors: #{response.errors}"
end
- 엔진은 기본 스코프를 제공해야 합니다. 사용 사례에 따라 현재 프로젝트, 네임스페이스, 사용자 등에 이미 사전 필터링된 스코프를 제공할 수 있습니다.
- 모든 요청 필터는 제공된 기본 스코프에 적용됩니다.
아키텍처 개요#
프레임워크는 여러 핵심 구성 요소로 이루어집니다:
- Engine: 특정 데이터 소스에 대해 사용 가능한 메트릭, 차원, 필터를 정의하는 핵심 클래스
- Request: 선택된 메트릭, 차원, 필터, 정렬이 포함된 쿼리 요청을 나타냅니다
- QueryPlan: 요청을 검증하고 실행 가능한 쿼리 부분으로 변환합니다
- AggregationResult: 쿼리 실행 및 결과 서식을 처리합니다
┌────────────────────────────────────────────────────┐
│ Request │
│ (metrics, dimensions, filters, order) │
└────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ QueryPlan │
│ (validates request, builds plan parts) │
└────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ Engine │
│ (executes query plan, returns AggregationResult) │
└────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ AggregationResult │
│ implements Enumerable to access formatted results │
└────────────────────────────────────────────────────┘
유효성 검사#
프레임워크는 실행 전에 요청을 검증합니다:
- 최소한 하나의 메트릭이 필요합니다
- 참조된 모든 식별자는 엔진 정의에 존재해야 합니다
- 매개변수는 선언된 검증을 충족해야 합니다. 예:
granularity: { in: %i[daily weekly monthly], type: :string }는 granularity 값이 제공된 3개의 문자열 중 하나여야 합니다.
GraphQL 통합#
집계 프레임워크는 Mounter 모듈을 통해 원활한 GraphQL 통합을 제공합니다. 자세한 문서는
GraphQL 통합을 참조하세요.
