첫 번째 Projection 만들기

첫 번째 Projection 만들기

uk_price_paid 테이블은 (postcode, addr1, addr2)로 정렬되어 있어서 town으로 조회하면 전체 스캔이 필요해요. 이 퀵스타트에서는 projection — 같은 테이블 안에 저장되는 추가 정렬 표현 — 을 만들어 이 문제를 해결해요. 매터리얼라이즈드 뷰와 달리 별도 테이블이 필요 없고 mutation과 동기화되며 쿼리 최적화기가 투명하게 선택해요.

출처: Create your first projection

본문

사전 요구사항 (Prerequisites)

이 가이드를 성공적으로 따라 하려면 다음이 필요해요:

또한 이 가이드가 uk_price_paid 테이블과 거기서 소개한 개념을 직접 기반으로 하므로, 다음 퀵스타트도 완료했어야 해요:

만들게 될 것 (What you'll build)

MergeTree 퀵스타트에서 uk_price_paidtown이나 county로 조회하려면 테이블이 (postcode, addr1, addr2)로 정렬되어 있어 전체 테이블 스캔이 필요하다는 걸 봤어요. 이 퀵스타트에서는 projection — 동일한 테이블 안에 저장되는, 데이터의 추가 정렬 표현 — 을 만들어 이 문제를 해결해요.

매터리얼라이즈드 뷰와 달리 projection은 별도의 destination 테이블이 필요 없고, mutation(삭제·업데이트)과 동기화를 유지하며, 쿼리 최적화기가 투명하게 사용해요 — 계속 같은 테이블 이름을 쿼리하면 돼요. 끝나면 projection을 추가·materialize하는 방법, ClickHouse가 자동으로 선택하는 방법, 그리고 언제 projection을 매터리얼라이즈드 뷰 대신 선택해야 하는지를 이해하게 돼요.

1. projection이 왜 필요한지 이해하기

uk_price_paid 테이블은 (postcode, addr1, addr2)로 정렬되어 있어요. postcode, addr1, addr2로 필터링하면 큰 블록을 건너뛸 수 있지만, town으로 필터링하는 쿼리는 모든 행 — 3천만 개 — 을 스캔해야 해요.

projection은 (일부 또는 모든) 컬럼의 추가 정렬 복사본을 같은 테이블 안에 저장해요. 테이블을 쿼리하면 쿼리 최적화기가 projection에서 읽는 것이 기본 데이터보다 더 적은 granule을 건드릴지 자동으로 확인하고, 그렇다면 투명하게 사용해요.

매터리얼라이즈드 뷰와의 핵심 차이점:

  • 별도 테이블 없음 — projection은 uk_price_paid 자체 안에 살아 있어요
  • 투명한 쿼리 최적화 — 평소처럼 uk_price_paid를 쿼리하면 ClickHouse가 projection을 자동으로 선택해요
  • mutation과 동기화 유지 — 테이블에 적용된 삭제·업데이트가 projection에도 반영돼요

자세한 내용은 projections 레퍼런스 문서에서 확인하세요.

2. 테이블에 projection 추가하기

town, date, price, type(town, date)로 정렬해서 저장하는 projection을 uk_price_paid에 정의해요:

ALTER TABLE uk_price_paid
    ADD PROJECTION uk_price_paid_by_town
    (
        SELECT town, date, price, type
        ORDER BY (town, date)
    );

이렇게 하면 projection이 테이블 메타데이터에 등록되지만 기존 데이터에 대해서는 materialize되지 않아요 — 앞으로의 삽입만 채워질 거예요. 테이블 정의에 projection이 나타나는지 확인해 보세요:

SHOW CREATE TABLE uk_price_paid;

출력에 PROJECTION uk_price_paid_by_town 블록이 보일 거예요.

3. 기존 데이터에 대해 projection materialize하기

매터리얼라이즈드 뷰와 마찬가지로, 새로 추가된 projection은 앞으로의 삽입에만 적용돼요. 이미 테이블에 있는 3천만 행으로 채우려면 명시적으로 materialize해요:

ALTER TABLE uk_price_paid
    MATERIALIZE PROJECTION uk_price_paid_by_town;

이것은 백그라운드 mutation으로 실행돼요. 진행 상황을 확인할 수 있어요:

SELECT
    mutation_id,
    command,
    is_done
FROM system.mutations
WHERE table = 'uk_price_paid'
ORDER BY create_time DESC
LIMIT 5;

is_done = 1이 되면 projection이 완전히 materialize된 거예요. system.projection_parts로 확인할 수도 있어요:

SELECT
    name,
    count() AS parts,
    sum(rows) AS total_rows,
    formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.projection_parts
WHERE table = 'uk_price_paid'
  AND active = true
GROUP BY name;

4. 테이블을 쿼리하고 자동 projection 사용 관찰하기

이제 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;

쿼리 통계를 확인해 보세요 — projection이 존재하기 전보다 훨씬 적은 행이 읽혀요. ClickHouse가 기본 데이터를 스캔하는 대신 uk_price_paid_by_town projection에서 자동으로 읽기로 선택했기 때문이에요.

EXPLAIN으로 projection이 사용됐는지 확인할 수 있어요:

EXPLAIN
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;

출력에서 projection 이름을 참조하는 ReadFromMergeTree를 찾아보세요. 성능을 명시적으로 비교하고 싶다면 단일 쿼리에 대해 projection 최적화를 비활성화할 수 있어요:

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
SETTINGS optimize_use_projections = 0;

이렇게 하면 전체 테이블 스캔이 강제되어 읽힌 행 수의 차이를 볼 수 있어요.

5. projection과 매터리얼라이즈드 뷰 비교하기

Projection과 매터리얼라이즈드 뷰는 같은 문제 — 대안적 접근 패턴에서 더 빠른 읽기 — 를 해결하지만 서로 다른 트레이드오프를 가져요. 요약하면, 같은 데이터에 다른 정렬 순서만 필요하면 projection이 최고이고, 데이터를 다른 스키마로 변환·집계·라우팅해야 할 때는 매터리얼라이즈드 뷰가 더 유연해요. 자세한 비교는 Materialized views versus projections를 참고하세요.

6. 저장 공간 오버헤드 관찰하기

Projection은 선택된 컬럼의 두 번째 복사본을 같은 테이블 안에 저장해서 디스크 사용을 늘려요. system.parts를 쿼리해서 (이제 projection 데이터를 포함하는) uk_price_paid의 총 크기를 확인해 보세요:

SELECT
    table,
    count() AS parts,
    sum(rows) AS total_rows,
    formatReadableSize(sum(bytes_on_disk)) AS compressed_size
FROM system.parts
WHERE table = 'uk_price_paid'
  AND active = true
GROUP BY table;

projection 전용 저장 공간도 확인할 수 있어요:

SELECT
    name,
    count() AS parts,
    sum(rows) AS total_rows,
    formatReadableSize(sum(bytes_on_disk)) AS projection_size
FROM system.projection_parts
WHERE table = 'uk_price_paid'
  AND active = true
GROUP BY name;

이것은 매터리얼라이즈드 뷰와 동일한 근본적인 트레이드오프예요: 더 빠른 읽기를 얻는 대가로 더 많은 디스크 공간. projection은 선택된 네 개 컬럼만 포함하고 압축이 정렬 순서에 따라 달라지므로 전체 복사본보다 작을 수 있어요.

다음 단계 (Next steps)

이 퀵스타트에서 uk_price_paid(town, date)로 정렬된 데이터를 저장하는 projection을 추가해서, 별도 테이블을 만들지 않고 town으로 빠르게 조회할 수 있게 했어요. projection이 쿼리 최적화기에 의해 투명하게 선택되고, mutation과 동기화되며, 디스크 공간을 읽기 성능과 맞바꾼다는 걸 배웠어요. 다음 퀵스타트를 계속 진행하거나 레퍼런스 문서로 더 깊이 들어가 보세요:

더 알아보기 (Learn more)