첫 번째 매터리얼라이즈드 뷰 만들기
첫 번째 매터리얼라이즈드 뷰 만들기
uk_price_paid 테이블을 (postcode, addr1, addr2) 순으로 정렬했기 때문에 town이나 county로 조회하면 전체 테이블 스캔이 필요해요. 이 퀵스타트에서는 매터리얼라이즈드 뷰(materialized view) 를 만들어서 원본 테이블은 바꾸지 않고도 town으로 빠르게 조회하는 방법을 배워요.
본문
사전 요구사항 (Prerequisites)
이 가이드를 성공적으로 따라 하려면 다음이 필요해요:
- 실행 중인 ClickHouse Cloud 서비스. 아직 없다면 ClickHouse Cloud 퀵 스타트를 먼저 완료하세요.
또한 첫 번째 MergeTree 테이블 만들기 퀵스타트를 완료했어야 해요. 이 가이드는 거기서 만든 uk_price_paid 테이블을 직접 기반으로 하거든요.
만들게 될 것 (What you'll build)
MergeTree 퀵스타트에서 uk_price_paid를 town이나 county로 조회하려면 전체 테이블 스캔이 필요하다는 걸 봤어요. 테이블이 (postcode, addr1, addr2)로 정렬되어 있기 때문이죠. 이 퀵스타트에서는 동일한 데이터를 (town, date) 순으로 저장하는 매터리얼라이즈드 뷰를 만들어서, 원본 테이블을 바꾸지 않고도 town으로 빠르게 조회할 수 있게 할 거예요. 끝나면 매터리얼라이즈드 뷰가 삽입 트리거(insert trigger)로 어떻게 동작하는지, 기존 데이터를 어떻게 backfill하는지, 데이터를 두 번 저장하는 디스크 공간 트레이드오프를 이해하게 돼요.
1. 매터리얼라이즈드 뷰가 왜 필요한지 이해하기
여러분의 uk_price_paid 테이블은 (postcode, addr1, addr2)로 정렬되어 있어요. 이 말은 postcode, addr1, addr2로 필터링하면 ClickHouse가 큰 데이터 블록을 건너뛸 수 있지만, town으로 필터링하는 쿼리는 모든 행 — 3천만 개 전부 — 을 스캔해야 한다는 뜻이에요.
다른 ORDER BY를 가진 두 번째 테이블을 만들 수도 있지만, 그러면 새 데이터가 올 때마다 두 테이블 모두에 삽입하는 걸 잊지 말아야 해요. 매터리얼라이즈드 뷰는 이를 자동화해요: 소스 테이블에 삽입되는 것을 감시하고, 행을 변환해서 destination 테이블에 자동으로 써 줍니다.
매터리얼라이즈드 뷰를 삽입 트리거로 생각해 보세요 — 소스 테이블에 행이 삽입될 때마다, MV의 SELECT 쿼리가 새 행 블록에 대해 실행되고 그 결과가 destination 테이블에 삽입돼요.
2. Destination 테이블 만들기
매터리얼라이즈드 뷰는 출력을 저장할 곳이 필요해요. 이건 그냥 일반적인 MergeTree 테이블이에요 — 스키마, ORDER BY, PARTITION BY를 완전히 제어할 수 있죠. town 기반 쿼리에 필요한 컬럼만 가진 (town, date) 정렬 테이블을 만들어 볼게요:
CREATE TABLE uk_price_paid_by_town
(
town LowCardinality(String),
date Date,
price UInt32,
type Enum8('terraced' = 1, 'semi-detached' = 2, 'detached' = 3, 'flat' = 4, 'other' = 0)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(date)
ORDER BY (town, date);
이 테이블에는 특별한 게 없어요 — 표준 MergeTree 테이블이죠. 다음에 만들 매터리얼라이즈드 뷰가 단순히 데이터를 여기로 라우팅할 거예요. 테이블이 만들어졌는지 확인해 보세요:
SHOW CREATE TABLE uk_price_paid_by_town;
3. 매터리얼라이즈드 뷰 만들기
이제 소스 테이블(uk_price_paid)과 destination 테이블(uk_price_paid_by_town)을 연결하는 매터리얼라이즈드 뷰를 만들어요:
CREATE MATERIALIZED VIEW uk_price_paid_by_town_mv
TO uk_price_paid_by_town
AS SELECT
town,
date,
price,
type
FROM uk_price_paid;
TO uk_price_paid_by_town 절은 ClickHouse에게 SELECT의 출력을 destination 테이블에 쓰라고 지시해요. 이제부터 uk_price_paid에 행이 삽입될 때마다 이 MV가 발동해서 변환된 행을 uk_price_paid_by_town에 삽입해요.
중요한 주의사항이 하나 있어요: 매터리얼라이즈드 뷰는 삽입(insert)에만 발동해요. 소스 테이블에서 행을 삭제하거나 업데이트하면 destination 테이블은 그걸 알지 못해요 — MV는 삭제나 업데이트와 동기화되지 않거든요. 그런 종류의 동기화가 필요하다면 projections을 사용하는 걸 고려해 보세요.
4. 기존 데이터 Backfill하기
매터리얼라이즈드 뷰는 이후의 삽입만 처리해요. uk_price_paid에 이미 있는 3천만 행은 MV가 존재하기 전에 삽입된 것이므로 destination 테이블은 현재 비어 있어요. 수동으로 backfill해 볼게요:
INSERT INTO uk_price_paid_by_town
SELECT
town,
date,
price,
type
FROM uk_price_paid;
이건 destination 테이블에 직접 삽입해요 — 이 단계에서 MV는 관여하지 않아요. 완료되면 행 수가 일치하는지 확인해 보세요:
SELECT
'uk_price_paid' AS table,
count() AS rows
FROM uk_price_paid
UNION ALL
SELECT
'uk_price_paid_by_town' AS table,
count() AS rows
FROM uk_price_paid_by_town;
두 테이블 모두 같은 수의 행을 가져야 해요.
5. 매터리얼라이즈드 뷰 destination 테이블 쿼리하기
이제 destination 테이블에서 town으로 필터링하는 쿼리를 실행하고, 소스 테이블을 직접 쿼리하는 것과 비교해 보세요. 먼저 소스 테이블을 쿼리해 볼게요:
SELECT
toYear(date) AS year,
round(avg(price)) AS avg_price,
count() AS sales
FROM uk_price_paid
WHERE town = 'LONDON'
GROUP BY year
ORDER BY year DESC;
쿼리 통계를 확인해 보세요 — 소스 테이블의 ORDER BY에 town이 없기 때문에 3천만 개 행 전부가 읽혀요. 이제 매터리얼라이즈드 뷰의 destination 테이블에서 같은 쿼리를 실행해 보세요:
SELECT
toYear(date) AS year,
round(avg(price)) AS avg_price,
count() AS sales
FROM uk_price_paid_by_town
WHERE town = 'LONDON'
GROUP BY year
ORDER BY year DESC;
쿼리 통계를 다시 확인해 보세요 — destination 테이블이 (town, date)로 정렬되어 있기 때문에 ClickHouse가 LONDON과 일치하지 않는 모든 데이터를 건너뛸 수 있어서 훨씬 적은 행이 읽혀요.
SHOW TABLES를 실행해서 무엇이 만들어졌는지 확인해 보세요:
SHOW TABLES;
uk_price_paid_by_town(destination 테이블)과 uk_price_paid_by_town_mv(뷰) 둘 다 보일 거예요. CREATE MATERIALIZED VIEW ... TO를 사용했기 때문에 destination 테이블 이름을 제어할 수 있어요. TO 절을 생략하면 ClickHouse가 암시적으로 이름 지어진 destination 테이블(.inner.xxx)을 만들고, 이는 직접 다루기 어려워요. 그래서 매터리얼라이즈드 뷰는 TO 절로 만드는 걸 권장해요.
6. 데이터가 두 번 저장되는 것 관찰하기
매터리얼라이즈드 뷰는 추가 디스크 공간을 대가로 더 빠른 읽기를 제공해요. system.parts를 쿼리해서 각 테이블이 얼마나 많은 공간을 쓰는지 확인해 볼게요:
SELECT
table,
count() AS parts,
sum(rows) AS total_rows,
formatReadableSize(sum(bytes_on_disk)) AS compressed_size
FROM system.parts
WHERE table IN ('uk_price_paid', 'uk_price_paid_by_town')
AND active = true
GROUP BY table;
데이터는 물리적으로 두 번 저장돼요 — 한 번은 (postcode, addr1, addr2)로 정렬된 uk_price_paid에, 한 번은 (town, date)로 정렬된 uk_price_paid_by_town에. 이것이 근본적인 트레이드오프예요: 다른 접근 패턴에서 더 빠른 읽기를 얻는 대가로 더 많은 디스크 공간을 쓰는 거죠. destination 테이블은 컬럼이 더 적고 (town, date) 정렬 순서가 원본과 다르게 압축될 수 있어서 디스크에서 더 작을 수 있어요.
다음 단계 (Next steps)
이 퀵스타트에서는 UK 부동산 판매 데이터를 다른 정렬 순서로 저장하는 매터리얼라이즈드 뷰를 만들어서, 원본 테이블을 수정하지 않고 town으로 빠르게 조회할 수 있게 했어요. MV가 삽입 트리거로 작동하고, 기존 데이터는 수동으로 backfill해야 하며, 트레이드오프는 추가 디스크 공간이라는 것을 배웠어요. 다음 퀵스타트를 계속 진행해 보세요:
또는 레퍼런스 문서로 더 깊이 들어가 보세요: