Aggregate Function Combinators

Aggregate Function Combinators (집계 함수 컴비네이터)

집계 함수 이름에 접미사를 붙이면 그 함수가 동작하는 방식을 바꿀 수 있어요. 이런 접미사를 가리켜 컴비네이터(combinator)라고 해요.

출처: 문서

본문

집계 함수 이름에 접미사를 붙일 수 있어요. 이렇게 하면 집계 함수가 동작하는 방식이 바뀌어요.

-If

-If 접미사는 아무 집계 함수 이름에나 붙일 수 있어요. 이 경우 집계 함수는 추가 인자 하나 — 조건(UInt8 타입)을 받아요. 집계 함수는 조건을 트리거하는 행만 처리해요. 조건이 한 번도 트리거되지 않으면 기본값(보통 0 또는 빈 문자열)을 반환해요.

예시: sumIf(column, cond), countIf(cond), avgIf(x, cond), quantilesTimingIf(level1, level2)(x, cond), argMinIf(arg, val, cond) 등.

조건부 집계 함수를 쓰면 서브쿼리와 JOIN 없이도 여러 조건에 대한 집계를 한 번에 계산할 수 있어요. 예를 들어 조건부 집계 함수로 세그먼트 비교 기능을 구현할 수 있어요.

-Array

-Array 접미사는 아무 집계 함수에나 붙일 수 있어요. 이 경우 집계 함수는 T 타입 인자 대신 Array(T) 타입(배열) 인자를 받아요. 집계 함수가 여러 인자를 받는다면, 이들은 모두 같은 길이의 배열이어야 해요. 배열을 처리할 때 집계 함수는 모든 배열 요소에 걸쳐 원래 집계 함수처럼 동작해요.

예시 1: sumArray(arr) — 모든 arr 배열의 모든 요소를 합산해요. 이 예시는 더 간단하게 sum(arraySum(arr))로 쓸 수 있었어요.

예시 2: uniqArray(arr) — 모든 arr 배열에서 고유한 요소 수를 세요. 이건 더 쉬운 방법으로 uniq(arrayJoin(arr))로 할 수 있지만, 쿼리에 arrayJoin을 항상 추가할 수 있는 것은 아니에요.

-If-Array는 결합할 수 있어요. 다만 Array가 먼저 오고 그다음 If가 와야 해요. 예시: uniqArrayIf(arr, cond), quantilesTimingArrayIf(level1, level2)(arr, cond). 이 순서 때문에 cond 인자는 배열이 아니에요.

-Map

-Map 접미사는 아무 집계 함수에나 붙일 수 있어요. 그러면 Map 타입을 인자로 받고, 지정된 집계 함수로 맵의 각 키 값을 각각 집계하는 집계 함수가 만들어져요. 결과도 Map 타입이에요.

예시

CREATE TABLE map_map(
    date Date,
    timeslot DateTime,
    status Map(String, UInt64)
) ENGINE = MergeTree
ORDER BY ();

INSERT INTO map_map VALUES
    ('2000-01-01', '2000-01-01 00:00:00', (['a', 'b', 'c'], [10, 10, 10])),
    ('2000-01-01', '2000-01-01 00:00:00', (['c', 'd', 'e'], [10, 10, 10])),
    ('2000-01-01', '2000-01-01 00:01:00', (['d', 'e', 'f'], [10, 10, 10])),
    ('2000-01-01', '2000-01-01 00:01:00', (['f', 'g', 'g'], [10, 10, 10]));

SELECT
    timeslot,
    sumMap(status),
    avgMap(status),
    minMap(status)
FROM map_map
GROUP BY timeslot;

┌────────────timeslot─┬─sumMap(status)───────────────────────┬─avgMap(status)───────────────────────┬─minMap(status)───────────────────────┐
│ 2000-01-01 00:00:00 │ {'a':10,'b':10,'c':20,'d':10,'e':10} │ {'a':10,'b':10,'c':10,'d':10,'e':10} │ {'a':10,'b':10,'c':10,'d':10,'e':10} │
│ 2000-01-01 00:01:00 │ {'d':10,'e':10,'f':20,'g':20}        │ {'d':10,'e':10,'f':10,'g':10}        │ {'d':10,'e':10,'f':10,'g':10}        │
└─────────────────────┴──────────────────────────────────────┴──────────────────────────────────────┴──────────────────────────────────────┘

-SimpleState

이 컴비네이터를 적용하면 집계 함수가 같은 값을 반환하지만 타입이 달라져요. 이것은 SimpleAggregateFunction(…)으로, AggregatingMergeTree 테이블과 함께 작업하기 위해 테이블에 저장할 수 있어요.

구문 (Syntax)

<aggFunction>SimpleState(x)

인자 (Arguments)

  • x — 집계 함수 파라미터.

반환 값 (Returned values)

SimpleAggregateFunction(...) 타입을 가진 집계 함수의 값.

예시

Query

WITH anySimpleState(number) AS c SELECT toTypeName(c), c FROM numbers(1);

Response

┌─toTypeName(c)────────────────────────┬─c─┐
│ SimpleAggregateFunction(any, UInt64) │ 0 │
└──────────────────────────────────────┴───┘

-State

이 컴비네이터를 적용하면 집계 함수가 결과 값(예를 들어 uniq 함수의 고유 값 수)을 반환하는 대신, 집계의 중간 상태를 반환해요 (uniq의 경우 고유 값 수를 계산하기 위한 해시 테이블). 이것은 이후 추가 처리에 사용하거나 나중에 집계를 마치기 위해 테이블에 저장할 수 있는 AggregateFunction(...)이에요.

참고로 -MapState는 중간 상태에서 데이터의 순서가 바뀔 수 있기 때문에 같은 데이터에 대해 불변(invariant)이 아니에요. 다만 이 데이터의 수집(ingestion)에는 영향을 주지 않아요.

이런 상태로 작업하려면 다음을 사용하세요:

  • AggregatingMergeTree 테이블 엔진.
  • finalizeAggregation 함수.
  • runningAccumulate 함수.
  • -Merge 컴비네이터.
  • -MergeState 컴비네이터.

-Merge

이 컴비네이터를 적용하면 집계 함수가 중간 집계 상태를 인자로 받아, 상태를 결합해 집계를 마치고 결과 값을 반환해요.

-MergeState

-Merge 컴비네이터와 같은 방식으로 중간 집계 상태를 병합해요. 다만 결과 값을 반환하지 않고, -State 컴비네이터와 비슷하게 중간 집계 상태를 반환해요.

-ForEach

테이블용 집계 함수를 배열용 집계 함수로 변환해요. 배열의 해당 항목을 집계하고 결과의 배열을 반환해요. 예를 들어 배열 [1, 2], [3, 4, 5], [6, 7]에 대한 sumForEach는 해당 배열 항목들을 더한 결과 [10, 13, 5]를 반환해요.

-Tuple

-Tuple 접미사는 아무 집계 함수에나 붙일 수 있어요. 결합된 함수는 기반 집계 함수의 인자 하나마다 Tuple 타입 인자 하나를 받아요. 모든 튜플은 같은 수의 요소를 가져야 해요. 집계는 각 요소 위치에서 독립적으로 적용되며, 모든 Tuple에서 해당 요소를 받아 결과의 Tuple을 반환해요.

첫 번째 입력 Tuple에 명시적 요소 이름이 있으면 그 이름이 결과에 보존돼요.

NULL 값을 직접 처리하는 집계 함수(anyRespectNulls, anyLastRespectNulls, RESPECT NULLS 수정자)는 인자로 Nullable(Tuple(...)) 타입을 지원하지 않아요. 대신 Nullable 요소를 사용하세요.

구문 (Syntax)

<aggFunction>Tuple(tuple1[, tuple2, ...])

인자 (Arguments)

  • tuple1[, tuple2, ...] — 기반 집계 함수의 인자 하나당 하나씩, 모두 같은 수의 요소를 가진 Tuple 타입 컬럼. 각 요소는 해당 인자 위치에서 기반 집계 함수가 지원하는 타입이어야 해요.

반환 값 (Returned values)

  • 각 요소에 대해 집계 함수를 독립적으로 적용한 결과를 담은 Tuple.

타입: Tuple(aggFunction(element1), aggFunction(element2), ...).

예시

Query:

SELECT sumTuple(t) FROM
(
    SELECT tuple(toInt64(1), toFloat64(2.5)) AS t
    UNION ALL
    SELECT tuple(toInt64(3), toFloat64(4.5))
    UNION ALL
    SELECT tuple(toInt64(5), toFloat64(6.5))
);

Result:

┌─sumTuple(t)─┐
│ (9,13.5)    │
└─────────────┘

GROUP BY와 함께 사용:

SELECT
    k,
    avgTuple(t)
FROM
(
    SELECT
        number % 2 AS k,
        tuple(toInt64(number), toFloat64(number) * 1.5) AS t
    FROM numbers(6)
)
GROUP BY k
ORDER BY k;
┌─k─┬─avgTuple(t)─┐
│ 0 │ (2,3)       │
│ 1 │ (3,4.5)     │
└───┴─────────────┘

다중 인자 집계 함수와 함께 사용: 각 Tuple 인자가 기반 함수의 인자 하나를 공급하고, 요소들은 위치에 따라 짝지어져요:

corrTuple((a1, a2), (b1, b2)) = (corr(a1, b1), corr(a2, b2))
SELECT corrTuple((a1, a2), (b1, b2))
FROM
(
    SELECT
        toFloat64(number) AS a1,
        toFloat64(number * 2) AS a2,
        toFloat64(100 - number) AS b1,
        toFloat64(number * 3) AS b2
    FROM numbers(10)
);
┌─corrTuple((a1, a2), (b1, b2))─┐
│ (-1,1)                        │
└───────────────────────────────┘

a1b1은 역상관(anticorrelated)이고, a2b2는 비례하므로 결과는 (-1, 1)이에요.

-Tuple-If 같은 다른 컴비네이터와 결합할 수 있어요. 예: sumTupleIf(tuple_column, cond).

-Distinct

인자의 어떤 고유한 조합도 한 번만 집계돼요. 반복되는 값은 무시돼요.

예시: sum(DISTINCT x)(또는 sumDistinct(x)), groupArray(DISTINCT x)(또는 groupArrayDistinct(x)), corrStable(DISTINCT x, y)(또는 corrStableDistinct(x, y)) 등.

-OrDefault

집계 함수의 동작을 바꿔요.

집계 함수에 입력 값이 없으면, 이 컴비네이터는 반환 데이터 타입의 기본값을 반환해요. 빈 입력 데이터를 받을 수 있는 집계 함수에 적용돼요. -OrDefault는 다른 컴비네이터와 함께 사용할 수 있어요.

구문 (Syntax)

<aggFunction>OrDefault(x)

인자 (Arguments)

  • x — 집계 함수 파라미터.

반환 값 (Returned values)

집계할 것이 없으면 집계 함수의 반환 타입 기본값을 반환해요. 타입은 사용된 집계 함수에 따라 달라져요.

예시

Query

SELECT avg(number), avgOrDefault(number) FROM numbers(0)

Response

┌─avg(number)─┬─avgOrDefault(number)─┐
│         nan │                    0 │
└─────────────┴──────────────────────┘

-OrDefault는 다른 컴비네이터와 함께 사용할 수도 있어요. 집계 함수가 빈 입력을 받지 못할 때 유용해요.

Query

SELECT avgOrDefaultIf(x, x > 10)
FROM
(
    SELECT toDecimal32(1.23, 2) AS x
)

Response

┌─avgOrDefaultIf(x, greater(x, 10))─┐
│                              0.00 │
└───────────────────────────────────┘

-OrNull

집계 함수의 동작을 바꿔요.

이 컴비네이터는 집계 함수의 결과를 Nullable 데이터 타입으로 변환해요. 집계 함수가 계산할 값이 없으면 NULL을 반환해요. -OrNull은 다른 컴비네이터와 함께 사용할 수 있어요.

구문 (Syntax)

<aggFunction>OrNull(x)

인자 (Arguments)

  • x — 집계 함수 파라미터.

반환 값 (Returned values)

  • 집계 함수의 결과를 Nullable 데이터 타입으로 변환한 값.
  • 집계할 것이 없으면 NULL.

타입: Nullable(aggregate function return type).

예시

집계 함수 끝에 -orNull을 추가해요.

Query

SELECT sumOrNull(number), toTypeName(sumOrNull(number)) FROM numbers(10) WHERE number > 10

Response

┌─sumOrNull(number)─┬─toTypeName(sumOrNull(number))─┐
│              ᴺᵁᴸᴸ │ Nullable(UInt64)              │
└───────────────────┴───────────────────────────────┘

-OrNull은 다른 컴비네이터와 함께 사용할 수도 있어요. 집계 함수가 빈 입력을 받지 못할 때 유용해요.

Query

SELECT avgOrNullIf(x, x > 10)
FROM
(
    SELECT toDecimal32(1.23, 2) AS x
)

Response

┌─avgOrNullIf(x, greater(x, 10))─┐
│                           ᴺᵁᴸᴸ │
└────────────────────────────────┘

-Resample

데이터를 그룹으로 나눈 다음, 그 그룹들에서 데이터를 각각 집계할 수 있게 해줘요. 그룹은 한 컬럼의 값을 구간(interval)으로 나눠서 만들어져요.

<aggFunction>Resample(start, end, step)(<aggFunction_params>, resampling_key)

인자 (Arguments)

  • startresampling_key 값에 대한 전체 필요 구간의 시작 값.
  • stopresampling_key 값에 대한 전체 필요 구간의 끝 값. 전체 구간은 stop 값을 포함하지 않아요 [start, stop). start보다 커야 해요. 하위 구간을 하나도 포함하지 않는 범위는 예외를 던져요.
  • step — 전체 구간을 하위 구간으로 나누는 단계. aggFunction은 각 하위 구간에서 독립적으로 실행돼요.
  • resampling_key — 데이터를 구간으로 나누는 데 사용되는 컬럼.
  • aggFunction_paramsaggFunction 파라미터.

반환 값 (Returned values)

  • 각 하위 구간에 대한 aggFunction 결과의 배열.

예시

다음 데이터를 가진 people 테이블을 생각해 봐요:

┌─name───┬─age─┬─wage─┐
│ John   │  16 │   10 │
│ Alice  │  30 │   15 │
│ Mary   │  35 │    8 │
│ Evelyn │  48 │ 11.5 │
│ David  │  62 │  9.9 │
│ Brian  │  60 │   16 │
└────────┴─────┴──────┘

나이가 [30,60)[60,75) 구간에 속하는 사람들의 이름을 구해 볼게요. 나이를 정수로 표현하므로, [30, 59][60,74] 구간의 나이를 얻게 돼요.

이름을 배열로 집계하려면 groupArray 집계 함수를 사용해요. 이 함수는 인자를 하나 받아요. 우리 경우에는 name 컬럼이에요. groupArrayResample 함수는 나이별로 이름을 집계하기 위해 age 컬럼을 사용해야 해요. 필요한 구간을 정의하려면 groupArrayResample 함수에 30, 75, 30 인자를 전달해요.

SELECT groupArrayResample(30, 75, 30)(name, age) FROM people
┌─groupArrayResample(30, 75, 30)(name, age)─────┐
│ [['Alice','Mary','Evelyn'],['David','Brian']] │
└───────────────────────────────────────────────┘

결과를 살펴볼게요. John은 너무 어려서 샘플에서 빠졌어요. 다른 사람들은 지정된 나이 구간에 따라 분포돼요.

이제 지정된 나이 구간에서 총 인원 수와 평균 임금을 세어 볼게요.

SELECT
    countResample(30, 75, 30)(name, age) AS amount,
    avgResample(30, 75, 30)(wage, age) AS avg_wage
FROM people
┌─amount─┬─avg_wage──────────────────┐
│ [3,2]  │ [11.5,12.949999809265137] │
└────────┴───────────────────────────┘

-ArgMin

-ArgMin 접미사는 아무 집계 함수 이름에나 붙일 수 있어요. 이 경우 집계 함수는 추가 인자 하나를 받는데, 이 인자는 비교 가능한(comparable) 표현식이어야 해요. 집계 함수는 지정된 추가 표현식이 최소값을 갖는 행만 처리해요.

예시: sumArgMin(column, expr), countArgMin(expr), avgArgMin(x, expr) 등.

-ArgMax

-ArgMin 접미사와 비슷하지만, 지정된 추가 표현식이 최대값을 갖는 행만 처리해요.

더 알아보기 (Learn more)