Hive의 구체화 뷰
Hive의 구체화 뷰 (Materialized views in Hive)
데이터 웨어하우스에서 쿼리 성능을 높이는 강력한 기법인 구체화 뷰(materialized view)를 Hive에서 만드는 방법과, 이를 이용한 자동 쿼리 재작성(rewriting)에 대해 다루는 문서예요. LLAP 가속이나 Druid 같은 외부 저장소 연동까지 아우릅니다.
출처: 문서
본문
목표 (Objectives)
전통적으로 데이터 웨어하우스에서 쿼리 처리를 가속하는 가장 강력한 기법 중 하나는 관련 요약이나 구체화 뷰를 미리 계산하는 것입니다.
초기 구현은 구체화 뷰를 도입하고, 그 구체화를 기반으로 자동 쿼리 재작성을 하는 데 초점을 맞춥니다. 특히 구체화 뷰는 Hive에 네이티브로 저장하거나, 커스텀 저장 핸들러를 사용해 Druid 같은 다른 시스템에 저장할 수 있고, LLAP 가속 같은 Hive의 새로운 흥미로운 기능을 자연스럽게 활용할 수 있습니다. 옵티마이저는 Apache Calcite를 사용해 프로젝션(projection), 필터(filter), 조인(join), 집계(aggregation) 연산으로 구성된 많은 쿼리 표현식에 대해 전체·부분 재작성을 자동으로 생성합니다.
이 문서에서 Hive의 구체화 뷰 생성·관리 세부 사항, 몇 가지 예시와 함께 재작성 알고리즘의 현재 커버리지, 그리고 Hive가 구체화 뷰의 데이터 신선도(freshness) 같은 수명 주기의 중요한 측면을 어떻게 제어하는지 설명합니다.
Hive에서 구체화 뷰 관리 (Management of materialized views in Hive)
이 절에서는 현재 Hive가 구체화 뷰 관리를 위해 제공하는 주요 연산을 소개합니다.
구체화 뷰 생성 (Materialized views creation)
Hive에서 구체화 뷰를 만드는 구문은 CTAS 문 구문과 매우 유사하며, 파티션 컬럼, 커스텀 저장 핸들러, 테이블 속성 전달 같은 공통 기능을 지원합니다.
CREATE MATERIALIZED VIEW [IF NOT EXISTS] [db_name.]materialized_view_name
[DISABLE REWRITE]
[COMMENT materialized_view_comment]
[PARTITIONED ON (col_name, ...)]
[CLUSTERED ON (col_name, ...) | DISTRIBUTED ON (col_name, ...) SORTED ON (col_name, ...)]
[ [ROW FORMAT row_format] [STORED AS file_format] | STORED BY '[storage.handler.class.name]'
[WITH SERDEPROPERTIES (...)]
]
[LOCATION hdfs_path]
[TBLPROPERTIES (property_name=property_value, ...)]
AS
<query>;
구체화 뷰가 생성되면 그 내용은 문에 있는 쿼리를 실행한 결과로 자동으로 채워집니다. 구체화 뷰 생성 문은 원자적(atomic)이라, 모든 쿼리 결과가 채워질 때까지 다른 사용자에게 구체화 뷰가 보이지 않습니다.
기본적으로 구체화 뷰는 옵티마이저의 쿼리 재작성에 사용할 수 있으며, DISABLE REWRITE 옵션으로 구체화 뷰 생성 시점에 이 동작을 바꿀 수 있습니다.
구체화 뷰 생성 문에서 SerDe와 저장 형식을 지정하지 않으면(선택 사항) 기본값은 각각 hive.materializedview.serde와 hive.materializedview.fileformat 구성 속성으로 지정됩니다.
구체화 뷰는 커스텀 저장 핸들러를 사용해 Druid 같은 외부 시스템에 저장할 수 있습니다. 예를 들어 다음 문은 Druid에 저장되는 구체화 뷰를 만듭니다: 예제(Example):
CREATE MATERIALIZED VIEW druid_wiki_mv
STORED AS 'org.apache.hadoop.hive.druid.DruidStorageHandler'
AS
SELECT __time, page, user, c_added, c_removed
FROM src;
구체화 뷰 관리를 위한 기타 연산 (Other operations for materialized view management)
현재 Hive에서 구체화 뷰 관리를 돕는 다음 연산을 지원합니다:
-- 구체화 뷰를 삭제한다
DROP MATERIALIZED VIEW [db_name.]materialized_view_name;
-- 구체화 뷰를 보여준다 (선택적 필터 포함)
SHOW MATERIALIZED VIEWS [IN database_name] ['identifier_with_wildcards'];
-- 특정 구체화 뷰에 대한 정보를 보여준다
DESCRIBE [EXTENDED | FORMATTED] [db_name.]materialized_view_name;
이 연산들의 기능은 향후 확장되고 더 많은 연산이 추가될 수 있습니다.
구체화 뷰 기반 쿼리 재작성 (Materialized view-based query rewriting)
구체화 뷰가 생성되면 옵티마이저는 그 정의 의미를 이용해 들어오는 쿼리를 구체화 뷰로 자동 재작성할 수 있으므로 쿼리 실행을 가속합니다.
재작성 알고리즘은 hive.materializedview.rewriting 구성 속성(기본값 true)으로 전역 활성화/비활성화할 수 있습니다. 또한 사용자는 구체화 뷰를 재작성에 선택적으로 활성화/비활성화할 수 있습니다. 기본적으로 구체화 뷰는 생성 시점에 재작성에 활성화된다는 점을 기억하세요. 이 동작을 바꾸려면 다음 문을 사용할 수 있습니다:
ALTER MATERIALIZED VIEW [db_name.]materialized_view_name ENABLE|DISABLE REWRITE;
재작성 알고리즘은 Apache Calcite의 일부이며 TableScan, Project, Filter, Join, Aggregate 연산자를 포함한 쿼리를 지원합니다. 재작성 커버리지에 대한 더 많은 정보는 여기에서 확인할 수 있습니다. 아래에서는 다양한 재작성을 간략히 보여주는 몇 가지 예시를 소개합니다.
예제 1 (Example 1)
다음 DDL 문으로 만든 데이터베이스 스키마를 고려해 봅시다:
CREATE TABLE emps (
empid INT,
deptno INT,
name VARCHAR(256),
salary FLOAT,
hire_date TIMESTAMP)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
CREATE TABLE depts (
deptno INT,
deptname VARCHAR(256),
locationid INT)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
2016년 이후 다양한 기간 세분성(granularity)으로 고용된 직원과 그 부서에 대한 정보를 자주 가져오고 싶다고 가정해 봅시다. 다음 구체화 뷰를 만들 수 있습니다:
CREATE MATERIALIZED VIEW mv1
AS
SELECT empid, deptname, hire_date
FROM emps JOIN depts
ON (emps.deptno = depts.deptno)
WHERE hire_date >= '2016-01-01';
그런 다음 2018년 Q1에 고용된 직원에 대한 정보를 추출하는 다음 쿼리를 Hive에 제출합니다:
SELECT empid, deptname
FROM emps
JOIN depts
ON (emps.deptno = depts.deptno)
WHERE hire_date >= '2018-01-01' AND hire_date <= '2018-03-31';
Hive는 구체화 뷰를 사용해 들어오는 쿼리를 재작성할 수 있으며, 구체화 스캔 위에 보상 술어(compensation predicate)를 포함합니다. 재작성이 대수(algebraic) 수준에서 일어나긴 하지만, 예시를 설명하기 위해 Hive가 들어오는 쿼리에 답할 때 사용하는 mv 재작성과 동등한 SQL 문을 포함합니다:
SELECT empid, deptname
FROM mv1
WHERE hire_date >= '2018-01-01' AND hire_date <= '2018-03-31';
예제 2 (Example 2)
두 번째 예시로, SSB 벤치마크 기반 스타 스키마를 만드는 다음 DDL 문을 고려해 봅시다:
CREATE TABLE customer(
c_custkey BIGINT,
c_name STRING,
c_address STRING,
c_city STRING,
c_nation STRING,
c_region STRING,
c_phone STRING,
c_mktsegment STRING,
PRIMARY KEY (c_custkey) DISABLE RELY)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
CREATE TABLE dates(
d_datekey BIGINT,
d_date STRING,
d_dayofweek STRING,
d_month STRING,
d_year INT,
d_yearmonthnum INT,
d_yearmonth STRING,
d_daynuminweek INT,
d_daynuminmonth INT,
d_daynuminyear INT,
d_monthnuminyear INT,
d_weeknuminyear INT,
d_sellingseason STRING,
d_lastdayinweekfl INT,
d_lastdayinmonthfl INT,
d_holidayfl INT,
d_weekdayfl INT,
PRIMARY KEY (d_datekey) DISABLE RELY)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
CREATE TABLE part(
p_partkey BIGINT,
p_name STRING,
p_mfgr STRING,
p_category STRING,
p_brand1 STRING,
p_color STRING,
p_type STRING,
p_size INT,
p_container STRING,
PRIMARY KEY (p_partkey) DISABLE RELY)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
CREATE TABLE supplier(
s_suppkey BIGINT,
s_name STRING,
s_address STRING,
s_city STRING,
s_nation STRING,
s_region STRING,
s_phone STRING,
PRIMARY KEY (s_suppkey) DISABLE RELY)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
CREATE TABLE lineorder(
lo_orderkey BIGINT,
lo_linenumber int,
lo_custkey BIGINT not null DISABLE RELY,
lo_partkey BIGINT not null DISABLE RELY,
lo_suppkey BIGINT not null DISABLE RELY,
lo_orderdate BIGINT not null DISABLE RELY,
lo_ordpriority STRING,
lo_shippriority STRING,
lo_quantity DOUBLE,
lo_extendedprice DOUBLE,
lo_ordtotalprice DOUBLE,
lo_discount DOUBLE,
lo_revenue DOUBLE,
lo_supplycost DOUBLE,
lo_tax DOUBLE,
lo_commitdate BIGINT,
lo_shipmode STRING,
PRIMARY KEY (lo_orderkey) DISABLE RELY,
CONSTRAINT fk1 FOREIGN KEY (lo_custkey) REFERENCES customer_n1(c_custkey) DISABLE RELY,
CONSTRAINT fk2 FOREIGN KEY (lo_orderdate) REFERENCES dates_n0(d_datekey) DISABLE RELY,
CONSTRAINT fk3 FOREIGN KEY (lo_partkey) REFERENCES ssb_part_n0(p_partkey) DISABLE RELY,
CONSTRAINT fk4 FOREIGN KEY (lo_suppkey) REFERENCES supplier_n0(s_suppkey) DISABLE RELY)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
볼 수 있듯이 데이터베이스에 여러 무결성 제약을 선언하는데, RELY 키워드를 사용해 옵티마이저가 볼 수 있게 합니다. 이제 데이터베이스 내용을 비정규화하는 구체화를 만들고 싶다고 가정해 봅시다(dims는 자주 조회할 차원 집합이라고 생각해 보세요):
CREATE MATERIALIZED VIEW mv2
AS
SELECT <dims>,
lo_revenue,
lo_extendedprice * lo_discount AS d_price,
lo_revenue - lo_supplycost
FROM customer, dates, lineorder, part, supplier
WHERE lo_orderdate = d_datekey
AND lo_partkey = p_partkey
AND lo_suppkey = s_suppkey
AND lo_custkey = c_custkey;
위 구체화 뷰는 데이터베이스의 서로 다른 테이블 간 조인을 수행하는 쿼리를 가속할 수 있습니다. 예를 들어 다음 쿼리를 고려해 봅시다:
SELECT SUM(lo_extendedprice * lo_discount)
FROM lineorder, dates
WHERE lo_orderdate = d_datekey
AND d_year = 2013
AND lo_discount between 1 and 3;
쿼리가 구체화 뷰에 있는 모든 테이블을 사용하지 않지만, mv2의 조인이 lineorder 테이블의 모든 행을 보존하므로(무결성 제약 때문에 알 수 있음) 구체화 뷰로 답할 수 있습니다. 따라서 알고리즘이 만든 구체화 뷰 기반 재작성은 다음과 같습니다:
SELECT SUM(d_price)
FROM mv2
WHERE d_year = 2013
AND lo_discount between 1 and 3;
예제 3 (Example 3)
세 번째 예시로, 특정 웹사이트가 만든 편집 이벤트를 저장하는 단일 테이블 데이터베이스 스키마를 고려해 봅시다:
CREATE TABLE wiki (
time TIMESTAMP,
page STRING,
user STRING,
characters_added BIGINT,
characters_removed BIGINT)
STORED AS ORC
TBLPROPERTIES ('transactional'='true');
이 예시에서는 Druid를 사용해 구체화 뷰를 저장하겠습니다. 테이블에 대해 쿼리를 실행하고 싶지만, 분(minute)보다 높은 시간 세분성의 이벤트 정보에는 관심이 없다고 가정해 봅시다. 이벤트를 분 단위로 롤업하는 다음 구체화 뷰를 만들 수 있습니다:
CREATE MATERIALIZED VIEW mv3
STORED BY 'org.apache.hadoop.hive.druid.DruidStorageHandler'
AS
SELECT floor(time to minute) as __time, page,
SUM(characters_added) AS c_added,
SUM(characters_removed) AS c_removed
FROM wiki
GROUP BY floor(time to minute), page;
그런 다음 월별 추가된 문자 수를 추출하는 다음 쿼리에 답해야 한다고 가정해 봅시다:
SELECT floor(time to month),
SUM(characters_added) AS c_added
FROM wiki
GROUP BY floor(time to month);
Hive는 mv3을 사용해 구체화 뷰의 데이터를 월 세분성으로 롤업하고 쿼리 결과에 필요한 정보를 프로젝션해 들어오는 쿼리를 재작성할 수 있습니다:
SELECT floor(time to month),
SUM(c_added)
FROM mv3
GROUP BY floor(time to month);
구체화 뷰 유지보수 (Materialized view maintenance)
구체화 뷰가 사용하는 소스 테이블의 데이터가 변경되면(예: 새 데이터 삽입 또는 기존 데이터 수정) 그 변경에 맞춰 구체화 뷰 내용을 새로고침해야 합니다. 현재 구체화 뷰의 리빌드(rebuild) 연산은 사용자가 트리거해야 합니다. 특히 다음 문을 실행해야 합니다:
ALTER MATERIALIZED VIEW [db_name.]materialized_view_name REBUILD;
Hive는 증분 뷰 유지보수(incremental view maintenance), 즉 원본 소스 테이블의 변경에 영향을 받은 데이터만 새로고침하는 것을 지원합니다. 증분 뷰 유지보수는 리빌드 단계 실행 시간을 줄여줍니다. 또한 구체화 뷰의 기존 데이터에 대한 LLAP 캐시를 보존합니다.
기본적으로 Hive는 구체화 뷰를 증분으로 리빌드하려 시도하고, 불가능하면 전체 리빌드로 폴백합니다. 현재 구현은 소스 테이블에 INSERT 연산이 있을 때만 증분 리빌드를 지원하며, UPDATE와 DELETE 연산은 구체화 뷰의 전체 리빌드를 강제합니다.
증분 유지보수를 실행하려면 다음 조건을 충족해야 합니다:
- 구체화 뷰는 micromanaged 또는 ACID인 트랜잭션 테이블만 사용해야 합니다.
- 구체화 뷰 정의에 Group By 절이 있다면 구체화 뷰는 ACID 테이블에 저장되어야 합니다(MERGE 연산을 지원해야 하므로). Scan-Project-Filter-Join으로 구성된 구체화 뷰 정의에는 이 제한이 없습니다.
리빌드 연산은 구체화 뷰에 대해 배타적 쓰기 잠금(exclusive write lock)을 획득합니다. 즉, 주어진 구체화 뷰에 대해 한 번에 하나의 리빌드 연산만 실행할 수 있습니다.
구체화 뷰 수명 주기 (Materialized view lifecycle)
기본적으로 구체화 뷰 내용이 낡은(stale) 상태가 되면 구체화 뷰는 자동 쿼리 재작성에 사용되지 않습니다.
하지만 어떤 경우에는 낡은 데이터를 받아들여도 괜찮을 수 있습니다. 예를 들어 구체화 뷰가 비트랜잭션 테이블을 사용해 내용이 오래됐는지 확인할 수 없지만 여전히 자동 재작성을 사용하고 싶다면, 주기적으로(예: 5분마다) 리빌드 연산을 실행하고 hive.materializedview.rewriting.time.window 구성 파라미터로 구체화 뷰 데이터의 필요한 신선도를 정의할 수 있습니다. 예를 들어:
SET hive.materializedview.rewriting.time.window=10min;
이 파라미터 값은 구체화 생성 시 테이블 속성으로 설정해 특정 구체화 뷰로 재정의할 수도 있습니다.
더 알아보기 (Learn more)
구체화 뷰는 자주 쓰는 조인·집계 결과를 미리 계산해 쿼리 성능을 높여요. CREATE MATERIALIZED VIEW로 만들고, hive.materializedview.rewriting으로 자동 재작성을 켜며, REBUILD로 데이터를 새로고침합니다. Druid 같은 외부 저장소나 LLAP 캐시와도 잘 어울립니다.