InfoGrab DocsInfoGrab Docs

집계 엔진

요약

Aggregation 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 메트릭#

groupBitmapStatearrayIntersect를 사용하여 현재 기간과 이전 기간 모두에 나타나는 값을 집계합니다. 기능 유지율 또는 재방문 사용자 수에는 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_countretained_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](여기서 timestampgranularity: '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_idUser

  • GraphQL type: UserTypes::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
              }
            }
          }
        }
      }
    }
  }
}

관련 문서#

집계 엔진

GitLab v19.1
원문 보기
요약

Aggregation 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 메트릭#

groupBitmapStatearrayIntersect를 사용하여 현재 기간과 이전 기간 모두에 나타나는 값을 집계합니다. 기능 유지율 또는 재방문 사용자 수에는 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_countretained_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](여기서 timestampgranularity: '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_idUser

  • GraphQL type: UserTypes::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
              }
            }
          }
        }
      }
    }
  }
}

관련 문서#