캐스케이딩 머티얼라이즈드 뷰

캐스케이딩 머티얼라이즈드 뷰 (Cascading materialized views)

이 예제는 머티얼라이즈드 뷰(Materialized View)를 만들고, 그 위에 두 번째 머티얼라이즈드 뷰를 캐스케이드(연쇄)하는 방법을 보여 줍니다. 하나의 머티얼라이즈드 뷰를 소스로 삼아 다른 머티얼라이즈드 뷰를 만들면 다양한 사용 사례를 해결할 수 있어요.

출처: 문서

본문

이 예제는 머티얼라이즈드 뷰를 만든 다음, 그 위에 두 번째 머티얼라이즈드 뷰를 어떻게 캐스케이드하는지 보여 줍니다. 이 페이지에서 그 방법, 가능한 여러 경우, 그리고 제한사항을 살펴봅니다. 머티얼라이즈드 뷰를 소스로 사용하는 두 번째 머티얼라이즈드 뷰를 만들어 다양한 사용 사례를 해결할 수 있습니다.

예제:

도메인 이름 그룹에 대한 시간별 조회 수를 담은 가상 데이터셋을 사용하겠습니다.

우리의 목표

  • 각 도메인 이름에 대해 월별로 집계된 데이터가 필요하고,
  • 각 도메인 이름에 대해 연별로 집계된 데이터도 필요합니다.

다음 옵션 중 하나를 선택할 수 있습니다:

  • SELECT 요청 중에 데이터를 읽고 집계하는 쿼리를 작성
  • 수집(ingest) 시점에 데이터를 새 형식으로 준비
  • 수집 시점에 특정 집계로 데이터를 준비

머티얼라이즈드 뷰로 데이터를 준비하면 ClickHouse가 해야 하는 데이터 양과 계산을 줄일 수 있어 SELECT 요청이 더 빨라집니다.

머티얼라이즈드 뷰용 소스 테이블

소스 테이블을 만듭니다. 우리의 목표가 개별 행이 아니라 집계된 데이터를 보고하는 것이므로, 데이터를 파싱해 머티얼라이즈드 뷰에 전달하고 실제 들어오는 데이터는 버릴 수 있습니다. 이렇게 하면 목표를 충족하면서 스토리지를 절약할 수 있으므로 Null 테이블 엔진을 사용합니다.

CREATE DATABASE IF NOT EXISTS analytics;
CREATE TABLE analytics.hourly_data
(
    `domain_name` String,
    `event_time` DateTime,
    `count_views` UInt64
)
ENGINE = Null

Null 테이블에 머티얼라이즈드 뷰를 만들 수 있습니다. 그래서 테이블에 쓰인 데이터가 뷰에 영향을 주지만, 원본 원시 데이터는 여전히 버려집니다.

월별 집계 테이블과 머티얼라이즈드 뷰

첫 번째 머티얼라이즈드 뷰를 위해 Target 테이블을 만들어야 합니다. 이 예제에서는 analytics.monthly_aggregated_data이고, 월별·도메인 이름별 조회 수의 합을 저장합니다.

CREATE TABLE analytics.monthly_aggregated_data
(
    `domain_name` String,
    `month` Date,
    `sumCountViews` AggregateFunction(sum, UInt64)
)
ENGINE = AggregatingMergeTree
ORDER BY (domain_name, month)

데이터를 타깃 테이블로 전달할 머티얼라이즈드 뷰는 다음과 같습니다:

CREATE MATERIALIZED VIEW analytics.monthly_aggregated_data_mv
TO analytics.monthly_aggregated_data
AS
SELECT
    toDate(toStartOfMonth(event_time)) AS month,
    domain_name,
    sumState(count_views) AS sumCountViews
FROM analytics.hourly_data
GROUP BY
    domain_name,
    month

연별 집계 테이블과 머티얼라이즈드 뷰

이제 이전 타깃 테이블 monthly_aggregated_data에 연결될 두 번째 머티얼라이즈드 뷰를 만듭니다.

먼저 각 도메인 이름에 대해 연별로 집계된 조회 수의 합을 저장할 새 타깃 테이블을 만듭니다.

CREATE TABLE analytics.year_aggregated_data
(
    `domain_name` String,
    `year` UInt16,
    `sumCountViews` UInt64
)
ENGINE = SummingMergeTree()
ORDER BY (domain_name, year)

이 단계가 캐스케이드를 정의합니다. FROM 문은 monthly_aggregated_data 테이블을 사용하므로 데이터 흐름은 다음과 같습니다:

  • 데이터가 hourly_data 테이블로 옵니다.
  • ClickHouse는 받은 데이터를 첫 번째 머티얼라이즈드 뷰 monthly_aggregated_data 테이블로 전달합니다.
  • 마지막으로 2단계에서 받은 데이터가 year_aggregated_data로 전달됩니다.
CREATE MATERIALIZED VIEW analytics.year_aggregated_data_mv
TO analytics.year_aggregated_data
AS
SELECT
    toYear(toStartOfYear(month)) AS year,
    domain_name,
    sumMerge(sumCountViews) AS sumCountViews
FROM analytics.monthly_aggregated_data
GROUP BY
    domain_name,
    year

머티얼라이즈드 뷰를 다룰 때 흔한 오해는 테이블에서 데이터를 읽는다는 것입니다. 그러나 Materialized views는 그렇게 동작하지 않습니다. 전달되는 데이터는 삽입된 블록(inserted block)이지 테이블의 최종 결과가 아닙니다. 이 예제에서 monthly_aggregated_data가 사용하는 엔진이 CollapsingMergeTree라고 상상해 봅시다. 두 번째 머티얼라이즈드 뷰 year_aggregated_data_mv로 전달되는 데이터는 콜랩스된 테이블의 최종 결과가 아니라, SELECT ... GROUP BY에서 정의된 필드들의 데이터 블록입니다. CollapsingMergeTree, ReplacingMergeTree, 또는 SummingMergeTree를 사용하면서 캐스케이드 머티얼라이즈드 뷰를 만들 계획이라면 여기에 설명된 제한사항을 이해해야 합니다.

샘플 데이터

이제 캐스케이드 머티얼라이즈드 뷰를 테스트하기 위해 데이터를 삽입할 시간입니다:

INSERT INTO analytics.hourly_data (domain_name, event_time, count_views)
VALUES ('clickhouse.com', '2019-01-01 10:00:00', 1),
       ('clickhouse.com', '2019-02-02 00:00:00', 2),
       ('clickhouse.com', '2019-02-01 00:00:00', 3),
       ('clickhouse.com', '2020-01-01 00:00:00', 6);

analytics.hourly_data의 내용을 SELECT하면 테이블 엔진이 Null이기 때문에 다음처럼 보이지만, 데이터는 처리되었습니다.

SELECT * FROM analytics.hourly_data
Ok.

0 rows in set. Elapsed: 0.002 sec.

흐름이 작은 데이터셋에서 맞는지 따라가고 기대한 결과와 비교할 수 있도록 작은 데이터셋을 사용했습니다. 흐름이 작은 데이터셋에서 올바르다면 큰 데이터로 넘어가면 됩니다.

결과

sumCountViews 필드를 선택해 타깃 테이블을 쿼리하면 값이 숫자가 아니라 AggregateFunction 타입으로 저장되므로(일부 터미널에서) 이진 표현이 보입니다. 집계의 최종 결과를 얻으려면 -Merge 접미사를 사용해야 합니다.

AggregateFunction에 저장된 특수 문자는 이 쿼리로 볼 수 있습니다:

SELECT sumCountViews FROM analytics.monthly_aggregated_data
┌─sumCountViews─┐
│               │
│               │
│               │
└───────────────┘

3 rows in set. Elapsed: 0.003 sec.

대신 Merge 접미사를 사용해 sumCountViews 값을 얻어 봅시다:

SELECT
   sumMerge(sumCountViews) AS sumCountViews
FROM analytics.monthly_aggregated_data;
┌─sumCountViews─┐
│            12 │
└───────────────┘

1 row in set. Elapsed: 0.003 sec.

AggregatingMergeTree에서 AggregateFunctionsum으로 정의했으므로 sumMerge를 사용할 수 있습니다. AggregateFunctionavg 함수를 쓰면 avgMerge를 사용하고, 이런 식으로 이어집니다.

SELECT
    month,
    domain_name,
    sumMerge(sumCountViews) AS sumCountViews
FROM analytics.monthly_aggregated_data
GROUP BY
    domain_name,
    month

이제 머티얼라이즈드 뷰가 정의한 목표를 충족하는지 검토할 수 있습니다.

이제 타깃 테이블 monthly_aggregated_data에 데이터가 저장되었으므로, 각 도메인 이름에 대해 월별로 집계된 데이터를 얻을 수 있습니다:

SELECT
   month,
   domain_name,
   sumMerge(sumCountViews) AS sumCountViews
FROM analytics.monthly_aggregated_data
GROUP BY
   domain_name,
   month
┌──────month─┬─domain_name────┬─sumCountViews─┐
│ 2020-01-01 │ clickhouse.com │             6 │
│ 2019-01-01 │ clickhouse.com │             1 │
│ 2019-02-01 │ clickhouse.com │             5 │
└────────────┴────────────────┴───────────────┘

3 rows in set. Elapsed: 0.004 sec.

각 도메인 이름에 대해 연별로 집계된 데이터:

SELECT
   year,
   domain_name,
   sum(sumCountViews)
FROM analytics.year_aggregated_data
GROUP BY
   domain_name,
   year
┌─year─┬─domain_name────┬─sum(sumCountViews)─┐
│ 2019 │ clickhouse.com │                  6 │
│ 2020 │ clickhouse.com │                  6 │
└──────┴────────────────┴────────────────────┘

2 rows in set. Elapsed: 0.004 sec.

여러 소스 테이블을 하나의 타깃 테이블로 결합

머티얼라이즈드 뷰는 여러 소스 테이블을 같은 대상 테이블로 결합하는 데도 사용할 수 있습니다. UNION ALL 로직과 비슷한 머티얼라이즈드 뷰를 만드는 데 유용합니다.

먼저 서로 다른 메트릭 집합을 나타내는 소스 테이블 두 개를 만듭니다:

CREATE TABLE analytics.impressions
(
    `event_time` DateTime,
    `domain_name` String
) ENGINE = MergeTree ORDER BY (domain_name, event_time)
;

CREATE TABLE analytics.clicks
(
    `event_time` DateTime,
    `domain_name` String
) ENGINE = MergeTree ORDER BY (domain_name, event_time)
;

그런 다음 결합된 메트릭 집합으로 Target 테이블을 만듭니다:

CREATE TABLE analytics.daily_overview
(
    `on_date` Date,
    `domain_name` String,
    `impressions` SimpleAggregateFunction(sum, UInt64),
    `clicks` SimpleAggregateFunction(sum, UInt64)
) ENGINE = AggregatingMergeTree ORDER BY (on_date, domain_name)

같은 Target 테이블을 가리키는 머티얼라이즈드 뷰 두 개를 만듭니다. 빠진 컬럼을 명시적으로 포함할 필요는 없습니다:

CREATE MATERIALIZED VIEW analytics.daily_impressions_mv
TO analytics.daily_overview
AS
SELECT
    toDate(event_time) AS on_date,
    domain_name,
    count() AS impressions,
    0 clicks         ---<<<--- if you omit this, it will be the same 0
FROM
    analytics.impressions
GROUP BY
    toDate(event_time) AS on_date,
    domain_name
;

CREATE MATERIALIZED VIEW analytics.daily_clicks_mv
TO analytics.daily_overview
AS
SELECT
    toDate(event_time) AS on_date,
    domain_name,
    count() AS clicks,
    0 impressions    ---<<<--- if you omit this, it will be the same 0
FROM
    analytics.clicks
GROUP BY
    toDate(event_time) AS on_date,
    domain_name
;

이제 값을 삽입하면 그 값들이 Target 테이블의 각 컬럼으로 집계됩니다:

INSERT INTO analytics.impressions (domain_name, event_time)
VALUES ('clickhouse.com', '2019-01-01 00:00:00'),
       ('clickhouse.com', '2019-01-01 12:00:00'),
       ('clickhouse.com', '2019-02-01 00:00:00'),
       ('clickhouse.com', '2019-03-01 00:00:00')
;

INSERT INTO analytics.clicks (domain_name, event_time)
VALUES ('clickhouse.com', '2019-01-01 00:00:00'),
       ('clickhouse.com', '2019-01-01 12:00:00'),
       ('clickhouse.com', '2019-03-01 00:00:00')
;

Target 테이블에 impressions와 clicks를 합친 결과:

SELECT
    on_date,
    domain_name,
    sum(impressions) AS impressions,
    sum(clicks) AS clicks
FROM
    analytics.daily_overview
GROUP BY
    on_date,
    domain_name
;

이 쿼리는 대략 다음과 같은 출력이어야 합니다:

┌────on_date─┬─domain_name────┬─impressions─┬─clicks─┐
│ 2019-01-01 │ clickhouse.com │           2 │      2 │
│ 2019-03-01 │ clickhouse.com │           1 │      1 │
│ 2019-02-01 │ clickhouse.com │           1 │      0 │
└────────────┴────────────────┴─────────────┴────────┘

3 rows in set. Elapsed: 0.018 sec.

더 알아보기 (Learn more)