구체화된 뷰

구체화된 뷰 (Materialized Views)

반복적인 시간-윈도우 집계를 위해 오프라인(offline) Pinot 구체화된 뷰(Materialized View, MV)를 만들고 관리해요. 자주 쓰는 쿼리 모양을 미리 집계해 두면 반복 계산을 피할 수 있어서 성능이 좋아져요.

출처: 문서

본문

Pinot의 구체화된 뷰는 반복되는 쿼리 모양에 대한 사전 집계 결과를 저장하는 오프라인 테이블이에요. 컨트롤러가 관리하는 SQL DDL을 POST /sql/ddl을 통해 사용해 만들고, 조회하고, 나열하고, 삭제할 수 있어요. Pinot는 구체화된 뷰를 만들 때 그 정의를 검증하고, MaterializedViewTask 미니언(minion) 워크플로로 새로고침하며, 컨트롤러 UI와 REST API에서 발견(discovery) 및 런타임 상태를 노출해요. 또한 broker rewrite 기능이 활성화된 경우 Pinot는 적격한 single-stage 베이스 테이블 쿼리를 구체화된 뷰로 투명하게 재작성(rewrite)할 수도 있어요.

참고: 투명한 구체화된 뷰 재작성은 적격한 Single-Stage Engine (SSE) 쿼리에만 제공돼요. 테이블 이름을 명시적으로 제어하려거나 broker rewrite가 비활성화된 경우에는 MV 테이블을 직접 쿼리하세요.

현재 범위

  • 시간-윈도우 구체화된 뷰만 지원해요.
  • MV 테이블 자체는 반드시 OFFLINE이어야 해요.
  • 컨트롤러 SQL DDL을 통해 MV를 만들고 관리해요: CREATE MATERIALIZED VIEW, SHOW MATERIALIZED VIEWS, SHOW CREATE MATERIALIZED VIEW, DROP MATERIALIZED VIEW.
  • 투명한 재작성은 적격한 SSE 쿼리에만 적용돼요.
  • broker에서 pinot.broker.query.enable.materialized.view.rewrite=true를 설정하기 전까지 broker 쪽 재작성은 기본적으로 꺼져 있어요.
  • CREATE MATERIALIZED VIEW는 전체 컬럼 목록 또는 컬럼 목록 없음을 받아요. 컬럼 목록을 생략하면 Pinot는 AS SELECT 프로젝션에서 MV 스키마를 추론해요.
  • 하나의 소스 테이블에 대한 단순한 SELECT를 사용해요. Pinot는 MV 테이블이 생성될 때 SQL, 스키마 매핑, 버킷 정의, 집계 집합을 검증해요.
  • 소스 테이블은 append-only여야 해요. Pinot는 realtime, upsert, dedup, dimension, REFRESH-push 소스 테이블을 거부해요.
  • 소스 테이블 시간 컬럼과 MV 시간 컬럼은 모두 TIMESTAMP dateTimeFieldSpecs여야 해요.
  • MV 시간 컬럼은 TIMESTAMP dateTimeFieldSpec이어야 해요.
  • 오늘날 definedSQL에서 지원되는 MV 집계는 SUM, COUNT, MIN, MAX, DISTINCTCOUNTRAWHLL, DISTINCTCOUNTRAWHLLPLUS, DISTINCTCOUNTRAWTHETASKETCH예요.

이런 제한은 내장된 OSS 구체화된 뷰 경로를 설명해요. Pinot는 여전히 단일-소스 SSE 정의를 검증하고 여전히 MaterializedViewTask 아래에 테이블을 라우팅하는 기본 MaterializedViewDdlHandler를 등록해요. 다운스트림 배포판은 컨트롤러 시작 시 그 핸들러를 교체해 다른 엔진이나 작업 타입을 대상으로 할 수 있지만, 이는 기본 OSS 동작의 변경이 아니라 확장 지점이에요.

Pinot는 MV 시간 컬럼을 만드는 표현식도 검증해요. 오늘날 지원되는 형태는 직접적인 TIMESTAMP 패스스루 또는 bucketTimePeriod와 단위가 일치하는 DATETRUNC(...)이에요.

만들기 전에

  • 최소한 하나의 Minion을 실행해요.
  • controller.task.scheduler.enabled=true로 컨트롤러 작업 스케줄링을 활성화해요.
  • 베이스 테이블을 위에서 설명한 검증된 append-only OFFLINE 경로로 유지해요.
  • Pinot가 SELECT 목록에서 MV 스키마를 추론하게 할지, 아니면 추론된 타입이나 역할을 재정의하기 위해 전체 명시적 컬럼 목록을 제공해야 할지 결정해요.

SQL DDL로 MV 만들기

MV DDL은 broker 쿼리 API가 아니라 컨트롤러 엔드포인트 POST /sql/ddl을 통해 실행해요.

컨트롤러는 다음 MV 문장을 받아요.

  • CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db.]name [(...)] [REFRESH [INTERVAL] EVERY ...] PROPERTIES (...) AS <select>
  • SHOW MATERIALIZED VIEWS [FROM db]
  • SHOW CREATE MATERIALIZED VIEW [db.]name
  • DROP MATERIALIZED VIEW [IF EXISTS] [db.]name

컬럼 목록을 생략하면 Pinot는 SELECT 프로젝션에서 MV 스키마를 추론해요. 컬럼 목록을 제공하면 모든 프로젝션된 컬럼을 선언하고 목적지 컬럼 이름과 일치하도록 모든 계산된 표현식이나 집계에 별칭을 지정해야 해요.

스키마 추론은 내장된 단일-소스 핸들러의 일부예요. 사용자 정의 핸들러는 대신 명시적 컬럼 목록을 요구할 수 있어요.

DDL에는 최소한 다음 속성이 필요해요.

  • timeColumnName: Pinot가 워터마크 진행을 추적하는 데 사용하는 MV 컬럼.
  • bucketTimePeriod: 실체화 윈도우 크기 (예: 1h 또는 1d).
  • stalenessThresholdMs: broker rewrite의 선택적 신선도 SLO. 0은 SLO 검사를 비활성화해요.

REFRESH EVERY는 선택 사항이에요. 제공하면 Pinot는 분(minute), 시(hour), 일(day) 단위를 사용하는 MV별 스케줄을 저장해요. 생략하면 MV는 클러스터 전체 MaterializedViewTask 스케줄로 실행돼요.

예시 DDL:

CREATE MATERIALIZED VIEW salesByHourMv
REFRESH EVERY 1 HOUR
PROPERTIES (
  'timeColumnName' = 'bucket_start_ts',
  'bucketTimePeriod' = '1h',
  'stalenessThresholdMs' = '900000',
  'replication' = '1'
)
AS
SELECT DATETRUNC('HOUR', event_ts) AS bucket_start_ts,
       region,
       SUM(revenue) AS sum_revenue,
       COUNT(*) AS row_count
FROM sales
GROUP BY DATETRUNC('HOUR', event_ts), region;

컨트롤러를 통해 문장을 제출해요:

curl -X POST "http://localhost:9000/sql/ddl" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{"sql":"CREATE MATERIALIZED VIEW salesByHourMv REFRESH EVERY 1 HOUR PROPERTIES ('timeColumnName' = 'bucket_start_ts', 'bucketTimePeriod' = '1h', 'stalenessThresholdMs' = '900000', 'replication' = '1') AS SELECT DATETRUNC('HOUR', event_ts) AS bucket_start_ts, region, SUM(revenue) AS sum_revenue, COUNT(*) AS row_count FROM sales GROUP BY DATETRUNC('HOUR', event_ts), region"}
EOF

Pinot는 MaterializedViewTask를 통해 MV 세그먼트를 생성해요. 컨트롤러 작업 관리자는 이러한 작업을 자동으로 스케줄링하거나 수동으로 트리거할 수 있어요.

POST /tasks/schedule?taskType=MaterializedViewTask&tableName=<mvTable>_OFFLINE

복구와 튜닝 이해하기

보존 삭제(retention delete) 또는 빈 새로고침(empty refresh)이 덮힌 MV 버킷을 정리하면 Pinot는 그 버킷을 완전히 버리는 대신 빈 덮힌 파티션으로 계속 추적해요. 나중에 소스 데이터가 같은 시간 윈도우로 백필되면 컨트롤러 일관성 관리자는 그 버킷을 다시 STALE로 표시하고 다음 덮어쓰기 주기가 다시 빌드해요.

기본적으로 일관성 관리자는 이 빈 버킷 복구 스윕을 매 300000 ms(5분)마다 실행해요. 이 간격은 컨트롤러를 재시작하지 않고 클러스터 구성 키 pinot.materialized.view.consistency.empty.sweep.interval.ms를 통해 실시간으로 변경할 수 있어요. pinot-admin.sh ClusterConfig 또는 컨트롤러 /cluster/configs 엔드포인트로 설정해요. 양수가 아닌 값은 5분 기본값으로 되돌아가요.

JSON API를 계속 사용해야 할 때

기존 POST /schemas, POST /tables, PUT /tables/{tableName} API는 구체화된 뷰에서도 여전히 동작해요. 자동화가 이미 원시 Pinot 메타데이터 페이로드에 의존하거나, REFRESH EVERY <N> MINUTES|HOURS|DAYS 또는 '<N>m|h|d'로 표현할 수 없는 수작업 MaterializedViewTask cron이 필요할 때 계속 사용해요.

다른 MV 작업 타입이나 쿼리-엔진 계약이 필요한 다운스트림 확장을 만드는 경우 Plugins를 참고해요. 사용자 정의 MaterializedViewDdlHandler 구현이 그 대체 작업 배선과 엔진별 검증을 담당해요.

SSE 쿼리를 위한 투명한 재작성 활성화

broker가 적격한 베이스 테이블 쿼리를 구체화된 뷰로 재작성하도록 하려면 이 broker 구성을 설정해요:

pinot.broker.query.enable.materialized.view.rewrite=true

이 스위치가 켜져 있어도 Pinot는 MV에 사용 가능한 범위(coverage)가 없으면 베이스 테이블로 대체해요. 실제로 MV는 0이 아닌 워터마크가 있어야 하고, stalenessThresholdMs를 설정했다면 MV가 여전히 그 신선도 경계 안에 있어야 해요.

경고: 오늘날 Pinot는 워터마크로만 MV 재작성을 나눠요. 워터마크 아래의 버킷이 이미 STALE로 표시되었거나 재실체화를 기다리는 빈 덮힌 버킷으로 추적 중이라면, Pinot는 다음 덮어쓰기 주기가 완료될 때까지 그 시간 범위를 여전히 MV로 라우팅할 수 있어요. 노출 윈도우는 일관성-관리자 디바운스(debounce)에 스케줄링 주기 하나를 더한 값으로 제한되며, 삭제/백필 경쟁은 빈 버킷 복구 스윕 간격 하나까지 추가할 수 있어요.

Pinot는 현재 MV에 포함되는(subsumed) 적격한 SSE 쿼리 모양을 재작성해요: 정확한 일치, 프로젝션-부분집합 스캔 쿼리, 지원되는 집계 롤업(rollup)이 포함돼요. DATETRUNC, UPPER, LOWER, SUBSTR 같은 그룹화 키로 사용되는 스칼라 표현식은 MV의 해당 프로젝션 표현식과 일치할 수 있어요. 집계 표현식은 여전히 지원되는 집계 롤업 규칙이 필요해요. 재작성이 발생하면 broker 응답에 쿼리를 제공한 MV 테이블 이름과 함께 materializedViewQueried가 포함돼요.

다른 모든 것에서 broker-side 재작성을 활성화하면서 특정 적격 쿼리는 베이스 테이블에 유지해야 한다면 해당 쿼리에 enableMaterializedViewRewrite=false를 설정해요:

SET enableMaterializedViewRewrite = false;
SELECT region, SUM(revenue) AS total_revenue, COUNT(*) AS total_rows
FROM sales
GROUP BY region
ORDER BY total_revenue DESC
LIMIT 20;

이 옵션은 기본값이 true이므로 쿼리는 false로 설정할 때만 옵트아웃해요. 이 옵션은 해당 쿼리에만 MV 재작성을 비활성화하고 일반 베이스 테이블 경로를 강제해요. Pinot는 MaterializedViewTask 실체화 쿼리에 대해서도 내부적으로 같은 옵션을 사용하므로 미니언이 MV 위로 재작성하는 대신 항상 베이스 테이블에서 읽어요.

예를 들어 MV가 빌드되고 broker 스위치가 켜지면 다음 베이스 테이블 쿼리는 SQL을 바꾸지 않고 MV가 서비스할 수 있어요:

SELECT region, SUM(revenue) AS total_revenue, COUNT(*) AS total_rows
FROM sales
GROUP BY region
ORDER BY total_revenue DESC
LIMIT 20;

재작성이 성공하면 응답에 MV 테이블 이름이 포함돼요:

{
  "materializedViewQueried": "salesByHourMv_OFFLINE"
}

스칼라 그룹화 표현식도 재작성에 참여할 수 있어요. 예를 들어 DATETRUNC('DAY', event_ts)를 프로젝션된 그룹화 키로 정의한 MV는 다음 베이스 테이블 쿼리를 서비스할 수 있어요:

SELECT DATETRUNC('DAY', event_ts) AS event_day,
       SUM(revenue) AS total_revenue
FROM sales
GROUP BY DATETRUNC('DAY', event_ts);

스칼라 표현식은 프로젝션된 MV 표현식과 일치해야 해요. ROUND(SUM(revenue))처럼 집계를 다른 함수로 감싸는 것은 집계 표현식이며 Pinot에 호환되는 집계 등가 규칙이 있을 때만 재작성돼요.

MV 테이블 직접 쿼리

MV 테이블 이름을 직접 쿼리하고 필요에 따라 저장된 값을 다시 집계할 수도 있어요:

SELECT region, SUM(sum_revenue) AS total_revenue, SUM(row_count) AS total_rows
FROM salesByHourMv
WHERE bucket_start_ts BETWEEN 1746057600000 AND 1746144000000
GROUP BY region
ORDER BY total_revenue DESC
LIMIT 20;

MV가 원시 스케치 컬럼을 저장한다면 MV 테이블에서 일치하는 병합 함수로 쿼리해요:

  • DISTINCTCOUNTHLL(raw_hll_col)
  • DISTINCTCOUNTHLLPLUS(raw_hllplus_col)
  • DISTINCTCOUNTTHETASKETCH(raw_theta_col)

번들된 퀵스타트 사용해 보기

Pinot는 베이스 테이블을 로드하고, MV 테이블을 만들고, 미니언 작업을 실행하며, 베이스 테이블 답변과 MV 테이블 재집계를 비교하는 완전한 로컬 예시를 제공해요:

bin/pinot-admin.sh QuickStart -type MATERIALIZED_VIEW

퀵스타트는 airlineStatsMv를 만들고 MaterializedViewTask를 트리거하며 직접 MV 쿼리를 검증할 수 있는 로컬 설정을 제공해요. broker rewrite 스위치를 활성화한 후에는 투명한 재작성 동작을 테스트하는 편리한 방법이기도 해요.

구체화된 뷰 검사 및 관리

Data Explorer의 Data Sources에서 물리적 테이블과 구체화된 뷰를 모두 발견할 수 있어요.

  • Data Sources는 Tables와 Materialized Views 카드를 보여줘요.
  • Materialized Views는 각 MV를 베이스 테이블, 워터마크, VALID 및 STALE 파티션 수, 마지막 새로고침 시간, staleness SLO, 메타데이터 오류와 함께 나열해요.
  • MV를 클릭하면 저장된 definedSQL, split 스펙, 파티션 상태, 원시 런타임 메타데이터, 페이지 데이터를 새로 고치거나 MV를 삭제하는 컨트롤이 있는 상세 페이지가 열려요.

같은 컨트롤러 DDL 표면에서 SQL로 MV를 검사하고 제거할 수도 있어요:

SHOW MATERIALIZED VIEWS;
SHOW CREATE MATERIALIZED VIEW salesByHourMv;
DROP MATERIALIZED VIEW IF EXISTS salesByHourMv;

SHOW MATERIALIZED VIEWS는 _OFFLINE 접미사 없이 원시 MV 이름을 반환해요. SHOW CREATE MATERIALIZED VIEW는 저장된 MV 정의에 대한 정식(canonical) DDL을 출력하며, 원래 CREATE MATERIALIZED VIEW가 추론된 컬럼을 사용했더라도 명시적 컬럼 목록을 포함해요.

컨트롤러는 전용 MV 엔드포인트도 노출해요:

  • GET /materializedViews
  • GET /materializedViews/{materializedViewTableName}
  • DELETE /materializedViews/{materializedViewTableName}

이 페이지에서 다룬 내용

이 페이지는 Pinot의 현재 구체화된 뷰 기능 표면을 다뤘어요: 컨트롤러 SQL DDL로 MV를 만들고 관리하는 방법, 지원되는 소스 테이블과 집계, broker-side SSE 재작성 방식, MV 테이블을 직접 쿼리하는 방법, UI와 컨트롤러 API에서 MV를 검사하는 위치.

다음 단계

더 넓은 쿼리 경로는 Querying Pinot를, UI 둘러보기는 Pinot Data Explorer를 읽어 보세요.

관련 페이지

더 알아보기 (Learn more)