이 가이드는 사용자가 이미 증분형 materialized view와 s3 및 gcs 같은 테이블 함수를 사용한 데이터 로딩 개념에 익숙하다고 가정합니다. 또한 이 가이드 전반에서 수행하는 삽입에 적용할 수 있는 권장 사항을 담은 객체 스토리지에서의 삽입 성능 최적화 가이드도 함께 읽어 보시기를 권장합니다.
예시 데이터셋
pip와 같은 도구를 사용한 Python 패키지 다운로드 1건을 나타냅니다.
예를 들어, 이 하위 집합은 2024-12-17 하루의 데이터를 포함하며, https://datasets-documentation.s3.eu-west-3.amazonaws.com/pypi/2024-12-17/에서 공개적으로 제공됩니다. 다음과 같이 쿼리할 수 있습니다:
1조 개가 넘는 행으로 이루어진 전체 PyPI 데이터셋은 공개 데모 환경 clickpy.clickhouse.com에서 제공됩니다. 성능 향상을 위해 데모에서 materialized view를 어떻게 활용하는지와 데이터가 매일 어떻게 적재되는지 등, 이 데이터셋에 대한 자세한 내용은 여기를 참조하십시오.
백필 시나리오
- 기존 데이터 수집이 진행 중인 상태에서 데이터 백필 - 새 데이터가 로드되고 있으며, 과거 데이터에 대한 백필이 필요합니다. 이 과거 데이터는 이미 식별되어 있습니다.
- 기존 테이블에 materialized view 추가 - 과거 데이터가 이미 채워져 있고 데이터 스트리밍이 이미 진행 중인 환경에 새로운 materialized view를 추가해야 합니다.
중복 테이블과 뷰 사용
pypi 테이블과 materialized view가 있으며, Python 프로젝트별 다운로드 횟수를 계산합니다:
{101..200} 범위의 또 다른 하위 집합을 로드하려 한다고 가정해 보겠습니다. pypi에 직접 삽입할 수도 있지만, 중복 테이블(duplicate table)을 생성하면 이 백필(backfill) 작업을 기존 테이블과 분리하여 독립적으로 수행할 수 있습니다.
백필(backfill)이 실패하더라도 기본 테이블에는 영향을 미치지 않으므로, 중복 테이블(duplicate table)을 TRUNCATE한 후 작업을 반복하면 됩니다.
이러한 뷰의 새 복사본을 생성하려면 _v2 접미사를 붙여 CREATE TABLE AS 절을 사용하면 됩니다:
pypi_v2와 pypi_downloads_v2를 간단히 truncate한 뒤 데이터 적재를 다시 수행할 수 있습니다.
데이터 적재가 완료되었으므로, ALTER TABLE MOVE PARTITION 절을 사용해 복제 테이블의 데이터를 메인 테이블로 이동할 수 있습니다.
파티션 이름위의
MOVE PARTITION 호출은 파티션 이름 ()를 사용합니다. 이는 이 테이블(파티션되지 않은 테이블)의 단일 파티션을 나타냅니다. 파티션된 테이블의 경우 각 파티션마다 하나씩 여러 번 MOVE PARTITION을 호출해야 합니다. 현재 파티션 이름은 system.parts 테이블에서 확인할 수 있습니다. 예: SELECT DISTINCT partition FROM system.parts WHERE (table = 'pypi_v2').pypi와 pypi_downloads에 전체 데이터가 포함되어 있음을 확인할 수 있습니다. pypi_downloads_v2와 pypi_v2는 안전하게 삭제할 수 있습니다.
MOVE PARTITION 작업은 경량(하드 링크 활용)이며 원자적입니다. 즉, 중간 상태 없이 성공하거나 실패합니다.
아래의 백필 시나리오에서는 이 과정을 적극적으로 활용합니다.
이 과정에서는 각 삽입 작업의 크기를 사용자가 직접 정해야 한다는 점에 유의하십시오.
삽입 크기가 클수록, 즉 행 수가 많을수록 필요한 MOVE PARTITION 작업 수는 줄어듭니다. 하지만 네트워크 중단 등으로 삽입이 실패할 경우 복구 비용도 함께 고려해 균형을 맞춰야 합니다. 위험을 줄이기 위해 파일 배칭으로 이 과정을 보완할 수 있습니다. 이는 범위 쿼리(예: WHERE timestamp BETWEEN 2024-12-17 09:00:00 AND 2024-12-17 10:00:00) 또는 글롭 패턴을 사용해 수행할 수 있습니다. 예를 들면 다음과 같습니다.
ClickPipes는 객체 스토리지에서 데이터를 로드할 때 이 방식을 사용하며, 대상 테이블과 해당 materialized view의 복제본을 자동으로 생성해 사용자가 위 단계를 직접 수행할 필요가 없도록 합니다. 또한 여러 worker 스레드가 각기 다른 하위 집합을 처리하고(글롭 패턴 사용), 각각 자체 복제 테이블을 사용하므로 데이터를 빠르게 로드하면서도 정확히 한 번 처리 의미 체계를 보장할 수 있습니다. 자세한 내용은 이 블로그에서 확인할 수 있습니다.
시나리오 1: 기존 데이터 수집 중 데이터 백필
- 체크포인트를 식별합니다. 과거 데이터를 복원해야 하는 기준 시점의 타임스탬프 또는 컬럼 값입니다.
- 주 테이블과 materialized view의 대상 테이블 복제본을 생성합니다.
- (2)단계에서 생성한 대상 테이블을 가리키도록 materialized view의 복제본을 생성합니다.
- (2)단계에서 생성한 주 테이블 복제본에 삽입합니다.
- 복제 테이블의 모든 파티션을 원래 테이블로 이동합니다. 그런 다음 복제 테이블을 삭제합니다.
2024-12-17 09:00:00 이전의 데이터를 적재해야 한다는 것을 알 수 있습니다. 앞서와 같은 방식으로 복제 테이블과 뷰를 생성하고, timestamp에 필터를 적용해 해당 부분집합을 적재합니다.
Parquet에서 timestamp 컬럼으로 필터링하면 매우 효율적일 수 있습니다. ClickHouse는 로드할 전체 데이터 범위를 식별하기 위해 timestamp 컬럼만 읽으므로 네트워크 트래픽을 최소화합니다. min-max와 같은 Parquet 인덱스도 ClickHouse 쿼리 엔진에서 활용할 수 있습니다.
ClickHouse Cloud에서는 ClickPipes를 사용하십시오ClickHouse Cloud를 사용 중이고 데이터를 별도 버킷으로 분리할 수 있어 필터가 필요하지 않다면, 과거 백업을 복원할 때 ClickPipes를 사용하는 것이 좋습니다. ClickPipes는 여러 워커를 통해 로드를 병렬화하여 적재 시간을 줄일 뿐만 아니라, 위 과정을 자동화하고 기본 테이블과 materialized view 모두에 대한 복제 테이블도 생성합니다.
시나리오 2: 기존 테이블에 materialized view 추가하기
POPULATE 사용 피하기작은 데이터셋에서 수집을 일시 중단한 경우를 제외하면, materialized view를 백필하기 위해
POPULATE 명령을 사용하는 것은 권장하지 않습니다. 이 연산자는 원본 테이블에 삽입되는 행 일부를 놓칠 수 있으며, 이는 populate 해시가 완료된 뒤 materialized view가 생성되기 때문입니다. 또한 이 populate는 전체 데이터를 대상으로 실행되므로, 대규모 데이터셋에서는 중단이나 메모리 제한의 영향을 받기 쉽습니다.Timestamp 또는 단조 증가하는 컬럼 사용 가능
- 가까운 미래의 임의 시점보다 큰 행만 고려하도록 필터를 적용한 materialized view를 생성합니다.
INSERT INTO SELECT쿼리를 실행해 materialized view의 대상 테이블에 데이터를 삽입합니다. 이때 뷰의 집계 쿼리를 사용해 원본 테이블에서 데이터를 읽습니다.
SELECT 절을 수정해 가까운 미래의 임의 시각보다 큰 행만 고려하는 필터를 포함합니다. 여기서는 2024-12-17 09:00:00이 몇 분 뒤의 시각이라고 가정합니다.
INSERT INTO SELECT를 통해 그 결과를 뷰의 대상 테이블에 삽입하는 것입니다. 예를 들어, 위의 뷰는 다음과 같습니다:
위 예시에서 대상 테이블은 SummingMergeTree입니다. 이 경우 원래 사용한 집계 쿼리를 그대로 사용하면 됩니다. AggregatingMergeTree를 활용하는 더 복잡한 사용 사례에서는 집계 함수에
-State 함수를 사용합니다. 이에 대한 예시는 이 통합 가이드에서 확인할 수 있습니다.pypi_downloads_per_day_v2 같은 섀도 대상 테이블을 만든 뒤 여기에 삽입하고, 생성된 파티션을 pypi_downloads_per_day에 ATTACH하면 됩니다.
materialized view의 쿼리는 더 복잡한 경우가 많고(그렇지 않다면 사용자가 굳이 뷰를 사용할 이유가 없을 것입니다!) 리소스도 더 많이 사용할 수 있습니다. 드문 경우에는 이 쿼리에 필요한 리소스가 서버 용량을 초과하기도 합니다. 이는 ClickHouse materialized view의 장점 중 하나를 잘 보여줍니다. materialized view는 증분 방식으로 동작하므로 전체 데이터셋을 한 번에 처리하지 않습니다!
이 경우 사용자는 몇 가지 선택지를 가질 수 있습니다:
- 쿼리를 수정하여 범위를 backfill합니다. 예:
WHERE timestamp BETWEEN 2024-12-17 08:00:00 AND 2024-12-17 09:00:00,WHERE timestamp BETWEEN 2024-12-17 07:00:00 AND 2024-12-17 08:00:00등입니다. - materialized view를 채우기 위해 Null table engine을 사용합니다. 이 방법은 materialized view가 일반적으로 점진적으로 채워지는 방식을 재현하며, 데이터 블록(크기 조정 가능) 단위로 해당 쿼리를 실행합니다.
materialized view를 채우기 위한 Null table engine 사용
/dev/null이라고 생각하면 됩니다). 언뜻 모순적으로 보일 수 있지만, materialized view는 이 테이블 엔진에 삽입된 데이터에 대해서도 계속 실행됩니다. 따라서 원본 데이터를 저장하지 않고도 materialized view를 구성할 수 있어 I/O와 그에 따른 스토리지 사용을 피할 수 있습니다.
중요한 점은, 이 테이블 엔진에 연결된 모든 materialized view가 데이터가 삽입될 때 여전히 데이터 블록(block) 단위로 실행되며, 그 결과를 대상 테이블로 보낸다는 것입니다. 이러한 블록의 크기는 구성할 수 있습니다. 블록이 클수록 더 효율적일 수 있고 처리 속도도 더 빨라질 수 있지만, 더 많은 리소스(주로 메모리)를 사용합니다. 이 테이블 엔진을 사용하면 materialized view를 점진적으로, 즉 한 번에 하나의 블록씩 구성할 수 있으므로 전체 집계를 메모리에 유지할 필요가 없습니다.
다음 예시를 살펴보겠습니다:
pypi_v2,를 생성해 materialized view를 구성하는 데 사용할 행을 받습니다. 필요한 컬럼만 포함하도록 스키마를 제한한 점에 유의하십시오. 이 materialized view는 이 테이블에 삽입된 행을 대상으로(한 번에 하나의 block씩) 집계를 수행하고, 그 결과를 대상 테이블인 pypi_downloads_per_day로 보냅니다.
여기서는
pypi_downloads_per_day를 대상 테이블로 사용했습니다. 복원력을 더 높이려면 pypi_downloads_per_day_v2라는 복제 테이블을 생성하고, 앞선 예시에서처럼 이를 뷰의 대상 테이블로 사용할 수 있습니다. 삽입이 완료되면 pypi_downloads_per_day_v2의 파티션을 다시 pypi_downloads_per_day로 이동할 수 있습니다. 이렇게 하면 메모리 문제나 서버 중단으로 삽입이 실패하더라도 복구할 수 있습니다. 즉, pypi_downloads_per_day_v2를 TRUNCATE하고, 설정을 조정한 뒤 다시 시도하면 됩니다.pypi에서 백필할 관련 데이터를 pypi_v2에 삽입하면 됩니다.
639.47 MiB입니다.
성능 및 리소스 튜닝
- 읽기 병렬성 - 읽기에 사용하는 스레드 수입니다.
max_threads로 제어됩니다. ClickHouse Cloud에서는 인스턴스 크기에 따라 결정되며, 기본값은 vCPU 수입니다. 이 값을 늘리면 메모리 사용량이 증가하는 대신 읽기 성능이 향상될 수 있습니다. - 삽입 병렬성 - 삽입 작업에 사용되는 삽입 스레드 수입니다.
max_insert_threads로 제어됩니다. 참고: 이 값은max_threads를 초과할 수 없으므로, 실제 삽입 병렬성은min(max_insert_threads, max_threads)입니다. ClickHouse Cloud에서는 인스턴스 크기(2~4)에 따라 결정되며, OSS에서는 1로 설정됩니다. 이 값을 늘리면 메모리 사용량이 증가하는 대신 성능이 향상될 수 있습니다. - 삽입 블록 크기 - 데이터는 파티셔닝 키를 기준으로 가져오고 파싱하여 메모리상의 삽입 블록으로 구성하는 루프에서 처리됩니다. 이렇게 구성된 블록은 정렬, 최적화, 압축을 거친 뒤 새로운 데이터 파트로 저장소에 기록됩니다. 설정
min_insert_block_size_rows및min_insert_block_size_bytes(비압축)로 제어되는 삽입 블록의 크기는 메모리 사용량과 디스크 I/O에 영향을 줍니다. 블록이 클수록 더 많은 메모리를 사용하지만 생성되는 파트 수는 줄어들어 I/O와 백그라운드 머지가 감소합니다. 이 설정은 최소 임계값을 의미하며(먼저 도달하는 값이 플러시를 트리거합니다). - materialized view 블록 크기 - 기본 삽입에 적용되는 위 메커니즘과 마찬가지로, materialized view에 삽입하기 전에도 더 효율적으로 처리할 수 있도록 블록을 더 큰 단위로 병합합니다. 이러한 블록의 크기는 설정
min_insert_block_size_bytes_for_materialized_views및min_insert_block_size_rows_for_materialized_views에 따라 결정됩니다. 블록이 클수록 처리 효율은 높아지지만, 그만큼 메모리 사용량도 증가합니다. 기본적으로 이러한 설정은 각각 소스 테이블 설정min_insert_block_size_rows및min_insert_block_size_bytes의 값을 따릅니다.
단순한 INSERT SELECT 쿼리 팁: 복잡한 변환 없이
INSERT INTO t1 SELECT * FROM t2 형태의 단순한 쿼리라면 optimize_trivial_insert_select=1 활성화를 고려하세요. 이 설정은(버전 24.7부터 기본적으로 비활성화됨) SELECT 병렬성을 max_insert_threads에 맞게 자동으로 조정하여 리소스 사용량과 생성되는 파트 수를 줄입니다. 이는 특히 테이블 간 대량 데이터 마이그레이션에 유용합니다.min_insert_block_size_bytes_for_materialized_views 및 min_insert_block_size_rows_for_materialized_views를 별도로 수정할 필요는 없습니다. 이 값들을 수정하는 경우, min_insert_block_size_rows 및 min_insert_block_size_bytes에 대해 설명된 모범 사례를 동일하게 적용하십시오.
메모리 사용량을 최소화하려면 이러한 설정을 실험해 보십시오. 단, 성능이 저하될 수 있습니다. 앞서 사용한 쿼리를 기반으로 아래에 예시를 제시합니다.
max_insert_threads를 1로 낮추면 메모리 오버헤드가 감소합니다.
max_threads 설정을 1로 줄이면 메모리 사용량을 더욱 낮출 수 있습니다.
min_insert_block_size_rows를 0으로 설정해 블록 크기를 결정하는 기준에서 제외하고, min_insert_block_size_bytes를 10485760(10MiB)으로 설정하면 메모리 사용량을 더 줄일 수 있습니다.
타임스탬프 또는 단조 증가하는 컬럼이 없는 경우
- 기본 테이블에 대한 삽입을 일시 중지합니다.
CREATE AS구문을 사용해 기본 대상 테이블의 복제본을 생성합니다.ALTER TABLE ATTACH를 사용해 원본 대상 테이블의 파티션을 복제본에 ATTACH합니다. 참고: 이 attach 작업은 앞에서 사용한 move와는 다릅니다. 하드 링크를 사용하지만 원본 테이블의 데이터는 그대로 유지됩니다.- 새 materialized view를 생성합니다.
- 삽입을 다시 시작합니다. 참고: 삽입은 대상 테이블만 업데이트하며 복제본은 업데이트하지 않습니다. 복제본은 원본 데이터만 참조합니다.
- 타임스탬프가 있는 데이터에 대해 위에서 사용한 것과 동일한 프로세스를 적용해 materialized view를 백필하며, 이때 복제 테이블을 소스로 사용합니다.
pypi_downloads_per_day를 사용하는 예시입니다(타임스탬프는 사용할 수 없다고 가정합니다):
INSERT INTO SELECT 방식으로 pypi_downloads_per_day를 백필합니다. 또한 위에서 설명한 Null 테이블 방식으로 이를 개선할 수 있으며, 복원력을 높이기 위해 필요에 따라 복제 테이블을 사용할 수도 있습니다.
이 작업을 수행하려면 삽입 작업을 일시 중지해야 하지만, 중간 단계의 작업은 일반적으로 빠르게 완료되므로 데이터 중단을 최소화할 수 있습니다.