집계 엔진
GitLab v19.1Aggregation Framework는 서로 다른 데이터베이스 백엔드에서 분석 쿼리를 구축하기 위한 통합 인터페이스를 제공합니다. ActiveRecord 엔진(Gitlab::Database::Aggregation::ActiveRecord::Engine)은 ActiveRecord의 쿼리 인터페이스를 사용하여 PostgreSQL 쿼리를 생성합니다.
Aggregation Framework는 서로 다른 데이터베이스 백엔드에서 분석 쿼리를 구축하기 위한 통합 인터페이스를 제공합니다. 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(*)를 사용하여 행을 집계합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | No | count 메트릭의 이름. 기본값: 'total'. 식별자는 :{name}_count가 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :integer |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
mean 메트릭#
AVG()를 사용하여 평균값을 계산합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 평균을 낼 칼럼 이름. 식별자는 :mean_{name}이 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :float |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 Arel 표현식 |
| scope_proc | Proc | No | ActiveRecord 스코프를 수정합니다(예: JOIN에 사용) |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
column 차원#
칼럼 값으로 결과를 그룹화합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 칼럼 이름 또는 식별자 |
| type | Symbol | Yes | 데이터 타입(:string, :integer, :datetime 등) |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 Arel 표현식 |
| scope_proc | Proc | No | ActiveRecord 스코프를 수정합니다(예: JOIN에 사용) |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
date_bucket 차원#
PostgreSQL의 date_trunc() 함수를 사용하여 시간 간격으로 결과를 그룹화합니다. 파라미터를 지원합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 날짜/datetime 칼럼 이름 |
| type | Symbol | Yes | 데이터 타입(:date 또는 :datetime) |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 Arel 표현식 |
| scope_proc | Proc | No | ActiveRecord 스코프를 수정합니다 |
| parameters | Hash | No | 파라미터 설정(아래 참조) |
| description | String | No | 사람이 읽을 수 있는 설명 |
지원되는 파라미터:
| Parameter | Type | Values | Default | Description |
|---|---|---|---|---|
| granularity | String | daily, weekly, monthly, yearly | monthly | 그룹화를 위한 시간 간격 |
exact_match 필터#
WHERE column IN (...)을 사용하여 정확한 값으로 행을 필터링합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 필터링할 칼럼 이름 |
| type | Symbol | Yes | 필터 값의 데이터 타입 |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 Arel 표현식 |
| max_size | Integer | No | 필터에서 허용되는 최대 값 수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
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 엔진은 최적의 성능을 위해 두 단계의 중첩 쿼리를 생성합니다. 전체 구조는 다음과 같이 표현할 수 있습니다:
-- metacode query to emphasize on query structure
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
주요 특성:
-
두 단계 쿼리 구조(내부 쿼리 + 외부 집계)
-
내부 쿼리는 행 수준 계산 및 기본 키 그룹화를 처리합니다. 외부 쿼리는 최종 집계를 수행합니다. 이 방식으로
*Merge칼럼과*If집계를 쉽게 활용할 수 있습니다. -
조건부 메트릭은
*If함수를 사용합니다. -
모든 칼럼은
aeq_접두사(Aggregation Engine Query)가 붙습니다. 이 접두사는AggregationResult객체에 의해 제거됩니다. -
칼럼 필터는 내부 쿼리의
WHERE또는HAVING절로 적용됩니다. -
메트릭 필터는 외부 쿼리의
HAVING절로 적용됩니다. -
차원은 외부 쿼리의
GROUP BY칼럼이 됩니다. -
메트릭은 외부 쿼리의 집계 함수를 사용합니다.
사용 가능한 구성 요소#
count 메트릭#
countIf()를 사용하여 고유 집계 및 조건부 집계를 지원하며 행을 집계합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | No | count 메트릭의 이름. 기본값: 'total'. 식별자는 :{name}_count가 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :integer |
| expression | Proc | No | 특정 값을 집계하기 위한 커스텀 표현식 |
| if | Proc | No | 조건부 집계를 위한 조건 표현식(countIf) |
| distinct | Boolean | No | 고유 집계 활성화. 기본값: false |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
mean 메트릭#
avgIf()를 사용하여 조건부 평균을 지원하며 평균값을 계산합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 칼럼 이름 또는 식별자. 식별자는 :mean_{name}이 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :float |
| expression | Proc | No | 평균낼 값에 대한 커스텀 표현식 |
| if | Proc | No | 조건부 평균을 위한 조건 표현식(avgIf) |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
rate 메트릭#
분자 조건을 충족하는 행과 분모 조건(또는 전체 행)을 충족하는 행 간의 비율을 계산합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 식별자 이름. 식별자는 :{name}_rate가 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :float |
| numerator_if | Proc | Yes | 분자에 대한 조건(집계할 행) |
| denominator_if | Proc | No | 분모에 대한 조건. 제공하지 않으면 전체 집계 수를 사용합니다 |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
quantile 메트릭#
ClickHouse의 quantile() 함수를 사용하여 백분위수를 계산합니다. 파라미터를 지원합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 칼럼 이름 또는 식별자. 식별자는 :{name}_quantile이 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :float |
| expression | Proc | No | 값에 대한 커스텀 표현식 |
| parameters | Hash | No | 파라미터 설정(아래 참조) |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
지원되는 파라미터:
| Parameter | Type | Values | Default | Description |
|---|---|---|---|---|
| quantile | Float | 0.0 - 1.0 | 0.5 | 분위수 값(0.5 = 중앙값, 0.9 = p90, 0.99 = p99) |
retained_count 메트릭#
groupBitmapState와 arrayIntersect를 사용하여 현재 기간과 이전 기간 모두에 나타나는 값을 집계합니다.
기능 유지율 또는 재방문 사용자 수에는 retained_count를 사용하세요.
over:가 참조하는 차원은 쿼리에서 요청되어야 합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 식별자 이름. 식별자는 :{name}_count가 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :integer |
| expression | Proc | No | 중복 제거할 값에 대한 표현식(예: user_id) |
| over | Symbol | Yes | 기간을 정의하는 차원. 엔진의 차원이어야 합니다 |
| lag_offset | Integer | No | 비교할 기간 수. 기본값: 1 |
| description | String | No | 사람이 읽을 수 있는 설명 |
예시:
metrics do
retained_count :returning_users, :integer, -> { sql('user_id') }, over: :timestamp,
description: 'Users present in both the current and previous period'
end
lagged_count 메트릭#
lagInFrame과 함께 uniqExact를 사용하여 이전 기간의 고유 값 집계를 반환합니다.
보존율(재방문 ÷ 이전)을 계산하려면 lagged_count와 retained_count를 함께 사용하세요.
over:가 참조하는 차원은 쿼리에서 요청되어야 합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 식별자 이름. 식별자는 :{name}_count가 됩니다 |
| type | Symbol | No | 데이터 타입. 기본값: :integer |
| expression | Proc | No | 중복 제거할 값에 대한 표현식 |
| over | Symbol | Yes | 기간을 정의하는 차원 |
| lag_offset | Integer | No | 되돌아볼 기간 수. 기본값: 1 |
| description | String | No | 사람이 읽을 수 있는 설명 |
예시:
metrics do
lagged_count :previous_period_users, :integer, -> { sql('user_id') }, over: :timestamp,
description: 'Distinct users in the previous period'
end
요청에 over: 외에 추가 차원이 포함된 경우, 프레임워크는 추가 차원으로 lag 윈도우를 파티셔닝합니다.
각 조합은 독립적인 시퀀스를 가지므로 값이 카테고리 간에 혼용되지 않습니다.
예를 들어, dimensions: [feature, timestamp](여기서 timestamp는 granularity: 'daily'인 date_bucket)이고
메트릭이 over: :timestamp를 사용하는 경우, 생성된 SQL에는
OVER (PARTITION BY aeq_feature ORDER BY aeq_timestamp_daily ASC)가 포함됩니다.
code_suggestions의 보존율과 chat의 보존율은 혼용되지 않습니다.
column 차원#
칼럼 값으로 결과를 그룹화합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 칼럼 이름 또는 식별자 |
| type | Symbol | Yes | 데이터 타입(:string, :integer, :datetime 등) |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 표현식 |
| formatter | Proc | No | 결과에 적용되는 포맷 함수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
| association | Boolean | No | true로 설정하면 _id 접미사 없이 객체로도 차원에 접근 가능합니다. 기본값: false. |
date_bucket 차원#
ClickHouse의 toStartOfInterval() 함수를 사용하여 시간 간격으로 결과를 그룹화합니다. 파라미터를 지원합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 날짜/datetime 칼럼 이름 |
| type | Symbol | Yes | 데이터 타입(:date 또는 :datetime) |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 표현식 |
| parameters | Hash | No | 파라미터 설정(아래 참조) |
| description | String | No | 사람이 읽을 수 있는 설명 |
지원되는 파라미터:
| Parameter | Type | Values | Default | Description |
|---|---|---|---|---|
| granularity | String | daily, weekly, monthly, yearly | monthly | 그룹화를 위한 시간 간격 |
exact_match 필터#
정확한 값으로 행을 필터링합니다. 일반 칼럼 또는 merge 칼럼(사전 집계된 데이터)에 대한 필터링을 지원합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 필터링할 칼럼 이름 |
| type | Symbol | Yes | 필터 값의 데이터 타입 |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 표현식 |
| merge_column | Boolean | No | true이면 WHERE 대신 HAVING을 사용하여 필터를 적용합니다 |
| max_size | Integer | No | 필터에서 허용되는 최대 값 수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
range 필터#
BETWEEN을 사용하여 값 범위로 행을 필터링합니다. 일반 칼럼 또는 merge 칼럼에 대한 필터링을 지원합니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 필터링할 칼럼 이름 |
| type | Symbol | Yes | 필터 값의 데이터 타입(:datetime, :integer 등) |
| expression | Proc | No | 칼럼 대신 사용할 커스텀 표현식 |
| merge_column | Boolean | No | true이면 WHERE 대신 HAVING을 사용하여 필터를 적용합니다 |
| description | String | No | 사람이 읽을 수 있는 설명 |
metric_exact_match 필터#
집계된 메트릭 값의 정확한 일치로 그룹을 필터링합니다. 집계 이후 HAVING 절로 적용됩니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 필터링할 메트릭의 식별자. 같은 엔진에 정의된 메트릭과 일치해야 합니다. |
| type | Symbol | Yes | 필터 값의 데이터 타입 |
| max_size | Integer | No | 필터에서 허용되는 최대 값 수 |
| description | String | No | 사람이 읽을 수 있는 설명 |
참조된 메트릭은 동일한 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
절로 적용됩니다.
| Option | Type | Required | Description |
|---|---|---|---|
| name | Symbol | Yes | 필터링할 메트릭의 식별자. 같은 엔진에 정의된 메트릭과 일치해야 합니다. |
| type | Symbol | Yes | 필터 값의 데이터 타입(:integer, :float 등) |
| description | String | No | 사람이 읽을 수 있는 설명 |
참조된 메트릭은 동일한 Request에서 요청되어야 합니다. 파라미터화된 메트릭의 경우,
필터의 parameters는 요청된 메트릭 인스턴스의 파라미터와 일치해야 올바른 메트릭 인스턴스를
타깃으로 합니다.
예시:
filters do
metric_range :total_count, :integer
metric_range :duration_quantile, :float
end
Gitlab::Database::Aggregation::Request.new(
filters: [
{ identifier: :duration_quantile, parameters: { quantile: 0.1 }, values: 200..nil }
],
dimensions: [{ identifier: :user_id }],
metrics: [{ identifier: :duration_quantile, parameters: { quantile: 0.1 } }]
)
임시 칼럼(Transient columns)#
임시 칼럼은 한 번 정의하고 dimensions, metrics, filters 블록 전반에 걸쳐 참조할 수 있는 명명된 SQL 표현식 별칭입니다.
최종 쿼리 결과에는 프로젝션되지 않습니다. 임시 칼럼을 사용하여 복잡한 SQL 표현식의 중복을 제거하세요.
임시 칼럼 정의#
클래스 수준에서 이름과 Arel 표현식을 반환하는 블록을 사용하여 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') }
임시 칼럼 참조#
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 } # order identifier must reference dimension or metric.
]
)
엔진으로 요청 실행#
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 통합#
Gitlab::Database::Aggregation::Graphql::Mounter 모듈을 사용하여 GraphQL API에 집계 엔진을 노출합니다.
GraphQL 통합은 자동으로 다음을 생성합니다:
-
Query field: 마운트된 엔진에 대한 쿼리 필드
-
Filter arguments: 엔진 필터 정의를 기반으로 한 필터 인수
-
Order argument: 엔진 차원 및 메트릭 정의를 기반으로 한 정렬 인수. 스네이크 케이스로 변환된 차원 및 메트릭 식별자를 정렬 식별자로 사용할 수 있습니다.
-
Response types: 차원과 메트릭을 필드로 포함하는 응답 타입
-
Parameterized fields: 파라미터가 있는 차원 및 메트릭을 위한 파라미터화된 필드
-
Pagination: 집계 결과는
OFFSET페이지네이션을 사용하여 자동으로 페이지 처리됩니다.
엔진 마운트#
GraphQL 타입에서 mount_aggregation_engine 메서드를 사용하여 집계 엔진을 노출합니다:
module Types
class ProjectType < BaseObject
extend Gitlab::Database::Aggregation::Graphql::Mounter
mount_aggregation_engine(
IssueAggregationEngine,
field_name: 'issue_analytics',
description: 'Issue analytics aggregation'
) do
# Define base aggregation scope. Build your own scope or inherit one from parent object.
def aggregation_scope
object.issues
end
end
end
end
모든 필터, 메트릭, 차원은 자동으로 노출됩니다.
Mounter 옵션#
| Option | Type | Description |
|---|---|---|
| field_name | String/Symbol | GraphQL 필드 이름. 기본값: :aggregation |
| types_prefix | String/Symbol | *AggregationResponse와 같은 모든 자식 타입의 접두사. 기본값: field_name |
| description | String | GraphQL 필드에 대한 설명 |
| authorize | Symbol | 필드에 접근하기 위해 필요한 권한(예: :read_project). GraphQL 필드 정의에 직접 전달됩니다 |
인가(Authorization)#
authorize 옵션을 사용하여 필드에 대한 접근을 제한합니다:
mount_aggregation_engine(
IssueAggregationEngine,
field_name: 'issue_analytics',
description: 'Issue analytics aggregation',
authorize: :read_project
) do
# authorize :read_project - this also supported.
def aggregation_scope
object.issues
end
end
authorize가 지정되지 않으면 인가를 수동으로 처리해야 합니다.
GraphQL 쿼리 예시#
생성된 GraphQL 서브트리는 두 단계 구조를 사용합니다:
-
외부 필드(
issueAnalytics)는 차원 및 비메트릭 필터 인수를 허용합니다. -
내부
aggregated필드는 메트릭 필터, 정렬, 페이지네이션 인수를 허용하고 페이지 처리된 커넥션을 반환합니다.
query IssueAnalytics($projectId: ID!) {
project(fullPath: $projectId) {
issueAnalytics(
state: ["opened", "closed"]
createdAtFrom: "2024-01-01"
createdAtTo: "2024-12-31"
) {
aggregated(
totalCountFrom: 5
orderBy: [{ identifier: "totalCount", direction: DESC }]
first: 10
) {
nodes {
dimensions {
createdAt(granularity: "monthly")
}
totalCount
meanWeight
highQuantile: durationQuantile(0.9)
medianQuantile: durationQuantile(0.5)
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
필터 배치#
필터 인수는 필터가 적용되는 시점을 기준으로 두 레벨에 분산됩니다:
-
비메트릭 필터(
exact_match또는range로 정의된 것)는 외부 필드(예:issueAnalytics)에 나타납니다. -
메트릭 필터(
metric_exact_match또는metric_range로 정의된 것)는 내부aggregated필드에 나타납니다.
커스텀 요청 유효성 검사#
GraphQL 스키마를 유지하면서 특정 집계 요청을 거부하는 커스텀 유효성 검사 로직을 추가합니다. 특정 요청에 커스텀 런타임 제약을 적용해야 할 때 유용합니다.
GraphQL::ExecutionError를 발생시켜 커스텀 에러 메시지와 함께 요청을 거부할 수 있습니다.
커스텀 유효성 검사를 추가하려면 마운팅 블록에서 validate_request! 메서드를 오버라이드합니다:
module Types
class ProjectType < BaseObject
extend Gitlab::Database::Aggregation::Graphql::Mounter
mount_aggregation_engine(IssueAggregationEngine) do
# Other configuration options...
# Custom validation logic
def validate_request!(engine_request)
if engine_request.dimensions.empty?
raise GraphQL::ExecutionError, 'At least one dimension must be specified'
end
end
end
end
end
validate_request! 메서드는 dimensions, metrics, filters, order 사양을 포함하는 Gitlab::Database::Aggregation::Request 객체를 받습니다.
ActiveRecord 연관을 위한 차원#
차원은 association: true 옵션을 사용하여 연관으로 표시할 수 있습니다. 이렇게 하면 차원이 GraphQL에 노출되는 방식이 변경되어, ID만 노출하는 대신 연관된 모델을 자동으로 리졸브합니다.
연관 차원 정의#
집계 엔진에서 association: true를 사용하여 차원을 선언합니다:
class AgentPlatformSessions < Gitlab::Database::Aggregation::ClickHouse::Engine
dimensions do
column :flow_type, :string, description: 'Type of session'
column :user_id, :integer, description: 'Session owner', association: true
end
end
GraphQL 스키마에 미치는 영향#
차원이 연관으로 표시되면, 원시 *_id 필드 대신 객체가 노출됩니다. 위의 차원은 GraphQL에서 ID로 배치 로딩하여 field :user, Types::UserType, ...으로 변환됩니다.
연관 이름에서 _id 접미사를 제거하여 연관 ID로 차원을 정렬할 수 있습니다(예: orderBy: [{ identifier: "user", direction: DESC }]).
연관 GraphQL 타입에 대한 모든 적절한 인가 검사를 보장해야 합니다(예: authorize :read_user).
커스텀 연관 설정#
기본적으로 연관 모델과 GraphQL 타입은 차원 이름으로부터 추론됩니다:
-
Model:
user_id→User -
GraphQL type:
User→Types::UserType
association 옵션에 해시를 전달하여 이 동작을 커스터마이징할 수 있습니다:
dimensions do
column :author_id, :integer,
description: 'Issue author',
association: { model: User }
# or model and GraphQL type
# association: { model: User, graphql_type: Types::CurrentUserType }
end
GraphQL 쿼리 예시#
다음은 연관 없이 사용하는 쿼리 예시입니다:
query {
project(fullPath: "gitlab-org/gitlab") {
aiUsage {
agentPlatformSessions {
aggregated {
nodes {
dimensions {
userId # Returns: 123 (integer)
}
}
}
}
}
}
}
다음은 연관을 사용하는 쿼리 예시입니다:
query {
project(fullPath: "gitlab-org/gitlab") {
aiUsage {
agentPlatformSessions(
userId: [1, 2] # Filter still uses original dimension identifier
) {
aggregated(
orderBy: [{ identifier: "user", direction: DESC }] # Order uses association name
) {
nodes {
dimensions {
user { # Returns: full User object
id
username
name
}
}
}
}
}
}
}
}