ClickHouse에서의 업데이트
ClickHouse에서의 업데이트 (Updates in ClickHouse)
업데이트를 다룰 때 분석 데이터베이스와 트랜잭션 데이터베이스는 근본적인 설계 철학과 대상 사용 사례로 인해 다른 접근 방식을 취합니다. ClickHouse에서 사용 가능한 업데이트 방법을 개요로 보여 주고 워크로드에 맞는 업데이트 전략을 고르는 데 도움을 줍니다.
출처: 문서
본문
업데이트를 처리할 때 분석 데이터베이스와 트랜잭션 데이터베이스는 근본적인 설계 철학과 대상 사용 사례로 인해 업데이트를 처리하는 다른 접근 방식을 취합니다. ClickHouse는 읽기 중심 분석과 고처리량 append-only 연산에 최적화된 컬럼 지향 데이터베이스입니다. 실제로 테이블은 삭제와 업데이트를 비동기적으로 및/또는 읽기 시간에 처리되는 append 연산으로 변환하도록 재구성되는 경우가 많으며, ClickHouse의 고처리량 데이터 수집 강점을 활용합니다. ClickHouse는 또한 견고한 업데이트와 삭제 연산을 지원합니다.
이 가이드는 ClickHouse에서 사용 가능한 업데이트 방법을 개요로 제공하고 워크로드에 맞는 올바른 업데이트 전략을 고르는 데 도움을 줍니다.
업데이트 전략 선택
ClickHouse에서 데이터를 업데이트하는 접근 방식은 크게 두 가지입니다:
- 삽입을 통해 업데이트를 처리하는 전문화된 테이블 엔진 사용
- UPDATE ... SET 또는 ALTER TABLE ... UPDATE 문 같은 선언적 업데이트 사용
위 두 범주 각각 안에는 데이터를 업데이트하는 여러 방법이 있습니다. 각각 장점과 성능 특성이 있으며, 데이터 모델과 업데이트하려는 데이터 양에 따라 적절한 방법을 선택해야 합니다.
전문화된 테이블 엔진을 언제 사용할까
업데이트 양이 많거나, 빈번한 행 수준 변경이 있거나, 지속적인 업데이트·삭제 이벤트 스트림을 처리해야 할 때 전문화된 테이블 엔진이 더 나은 선택입니다.
흔히 만나게 될 엔진들은 다음과 같습니다:
| 엔진 | 문법 | 언제 사용 |
|---|---|---|
| ReplacingMergeTree | ENGINE = ReplacingMergeTree | 대량의 데이터를 업데이트할 때. 이 테이블 엔진은 병합 시 데이터 중복 제거에 최적화되어 있습니다. |
| CoalescingMergeTree | ENGINE = CoalescingMergeTree | 데이터가 조각으로 도착하고 전체 행 교체가 아닌 컬럼 수준 병합(column-level coalescing)이 필요할 때. |
| CollapsingMergeTree | ENGINE = CollapsingMergeTree(Sign) | 개별 행을 자주 업데이트할 때, 또는 시간에 따라 변하는 객체의 최신 상태를 유지해야 하는 시나리오에서. 예: 사용자 활동 또는 기사 통계 추적. |
MergeTree 계열 테이블 엔진은 데이터 파트를 백그라운드에서 병합하므로 궁극적 일관성을 제공하며, 테이블을 쿼리할 때 중간 기간 동안 적절한 중복 제거를 보장하려면 FINAL 키워드를 사용해야 합니다. 다른 엔진 타입도 있지만 이들이 가장 흔히 사용되는 것들입니다.
선언적 업데이트를 언제 사용할까
선언적 UPDATE 문은 중복 제거 로직 관리의 복잡성 없이 단순한 업데이트 연산에 더 간단할 수 있지만, 일반적으로 전문화된 엔진보다 더 적은 수의 행을, 덜 자주 업데이트하는 데 더 적합합니다.
| 방법 | 문법 | 언제 사용 |
|---|---|---|
| 경량 업데이트 (Lightweight updates) | UPDATE [table] SET ... WHERE | 대부분의 시나리오에서, 특히 애플리케이션이나 워크플로의 일부로 잦은 작은 UPDATE(테이블의 ~10%까지)를 실행할 때. 예: 사용자가 자신의 이벤트 기록을 삭제하려 하고, 이벤트가 많은 사용자가 있는 멀티테넌트 테이블에 분산되어 있는 경우. 이 접근 방식은 전체 컬럼을 다시 쓰지 않고 즉시 가시성을 위한 패치 파트를 만듭니다. SELECT 쿼리에 오버헤드를 추가하지만 지연이 예측 가능합니다. |
| 업데이트 뮤테이션 (Update mutation) | ALTER TABLE [table] UPDATE | 더 큰 규모의 데이터 관리를 수행할 때, 특히 업데이트가 테이블 파티셔닝과 정렬될 때. 예: 월별로 파티셔닝된 테이블에서 한 달 내의 모든 행의 컬럼을 업데이트해야 하는 경우. |
전문화된 테이블 엔진으로 업데이트
ReplacingMergeTree
ReplacingMergeTree는 백그라운드 병합 중 같은 정렬 키를 가진 행을 중복 제거해 최신 버전만 유지합니다.
CREATE TABLE posts
(
Id UInt32,
Title String,
ViewCount UInt32,
Version UInt32
)
ENGINE = ReplacingMergeTree(Version)
ORDER BY Id
이 엔진은 안정적인 키로 식별되는 개별 행의 고빈도 업데이트에 이상적입니다. 벤치마크는 단일 행 업데이트에서 뮤테이션보다 최대 4,700배 빠를 수 있음을 보여 줍니다.
행을 업데이트하려면 같은 정렬 키 값과 더 높은 버전 번호로 새 버전을 삽입하기만 하면 됩니다. 오래된 버전은 백그라운드 병합 중 제거됩니다. 중복 제거가 궁극적(병합 중에만 발생)이므로, 올바르고 중복이 제거된 결과를 얻으려면 FINAL 수정자 또는 동등한 쿼리 로직을 사용해야 합니다. FINAL 수정자는 데이터에 따라 21–550%의 쿼리 오버헤드를 추가합니다.
ReplacingMergeTree는 정렬 키 값을 업데이트할 수 없습니다. 또한 논리적 삭제를 위한 Deleted 컬럼도 지원합니다.
더 읽어보기: ReplacingMergeTree 가이드 | ReplacingMergeTree 참조.
CoalescingMergeTree
CoalescingMergeTree는 병합 중 각 컬럼에 대해 최신 non-null 값을 유지해 희소 레코드를 통합합니다. 이것은 전체 행 교체 대신 컬럼 수준 upsert를 가능하게 합니다.
CREATE TABLE electric_vehicle_state
(
vin String, -- vehicle identification number
last_update DateTime64 Materialized now64(), -- optional (used with argMax)
battery_level Nullable(UInt8), -- in %
lat Nullable(Float64), -- latitude (°)
lon Nullable(Float64), -- longitude (°)
firmware_version Nullable(String),
cabin_temperature Nullable(Float32), -- in °C
speed_kmh Nullable(Float32) -- from sensor
)
ENGINE = CoalescingMergeTree
ORDER BY vin;
이 엔진은 데이터가 여러 소스에서 조각으로 도착하거나, 서로 다른 컬럼이 서로 다른 시점에 채워지는 시나리오를 위해 설계되었습니다. 흔한 사용 사례에는 단편화된 하위 시스템의 IoT 텔레메트리, 사용자 프로필 보강, 지연된 차원이 있는 ETL 파이프라인 등이 있습니다.
같은 정렬 키를 가진 행이 병합될 때 CoalescingMergeTree는 전체 행을 교체하는 대신 각 컬럼에 대해 최신 non-null 값을 유지합니다. 의도대로 동작하려면 비키 컬럼이 Nullable이어야 합니다. ReplacingMergeTree와 마찬가지로 올바른 병합 결과를 위해 FINAL을 사용하세요.
이 엔진은 ClickHouse 25.6부터 사용할 수 있습니다.
더 읽어보기: CoalescingMergeTree.
CollapsingMergeTree
업데이트는 비싸지만 삽입은 업데이트 수행에 활용될 수 있다는 아이디어에서 출발해, CollapsingMergeTree는 병합 중 행을 어떻게 처리할지 ClickHouse에 알려 주는 Sign 컬럼을 사용합니다. sign 컬럼에 -1이 삽입되면 일치하는 +1 행과 짝을 이룰 때 그 행이 콜랩스(삭제)됩니다. 업데이트할 행은 테이블을 만들 때 ORDER BY 절에 사용된 정렬 키를 기준으로 식별됩니다.
CREATE TABLE user_activity
(
UserID UInt64,
PageViews UInt8,
Duration UInt8,
Sign Int8
)
ENGINE = CollapsingMergeTree(Sign)
ORDER BY UserID
-- Initial state
INSERT INTO user_activity VALUES (4324182021466249494, 5, 146, 1)
-- Cancel old row and insert new state
INSERT INTO user_activity VALUES (4324182021466249494, 5, 146, -1)
INSERT INTO user_activity VALUES (4324182021466249494, 6, 185, 1)
-- Query with proper aggregation
SELECT
UserID,
sum(PageViews * Sign) AS PageViews,
sum(Duration * Sign) AS Duration
FROM user_activity
GROUP BY UserID
HAVING sum(Sign) > 0
┌──────────────UserID─┬─PageViews─┬─Duration─┐
│ 4324182021466249494 │ 6 │ 185 │
└─────────────────────┴───────────┴──────────┘
ReplacingMergeTree와 달리 CollapsingMergeTree는 정렬 키 값을 수정할 수 있게 합니다. 이것은 금융 트랜잭션이나 게임 상태 추적 같은 취소 의미론을 가진 가역 연산에 잘 맞습니다.
위의 업데이트 접근 방식은 취소 행을 삽입하기 위해 애플리케이션이 클라이언트 측에서 상태를 유지해야 합니다. 이것은 ClickHouse 관점에서 가장 효율적이지만 대규모에서는 다루기 복잡할 수 있습니다. 쿼리도 올바른 결과를 위해 sign 곱셈과 함께 집계가 필요합니다.
더 읽어보기: CollapsingMergeTree.
선언적 업데이트
이 방법들은 MergeTree 계열 엔진을 사용하는 테이블에서 동작합니다.
| 방법 | 문법 | 가장 좋은 용도 | 트레이드오프 |
|---|---|---|---|
| 뮤테이션 (Mutations) | ALTER TABLE ... UPDATE | 드문 벌크 업데이트, 업데이트가 테이블 파티셔닝과 정렬될 때 잘 맞음. | 무거운 I/O; 컬럼 재작성 |
| 경량 업데이트 (Lightweight updates) | UPDATE ... SET ... WHERE | 작은 업데이트(~행의 0.1-10%); 성능이 필요한 빈번한 업데이트 | SELECT 오버헤드 추가; 패치 파트가 한도에 포함 |
뮤테이션
뮤테이션(ALTER TABLE ... UPDATE)은 WHERE 표현식과 일치하는 행을 포함하는 모든 파트를 다시 씁니다.
ALTER TABLE posts UPDATE AnswerCount = AnswerCount + 1 WHERE AnswerCount = 0
뮤테이션은 I/O가 많으며 WHERE 표현식과 일치하는 모든 파트를 다시 씁니다. 이 과정에는 원자성이 없습니다. 파트가 준비되는 대로 뮤테이션된 파트로 대체되고, 뮤테이션 중에 실행을 시작한 SELECT 쿼리는 이미 뮤테이션된 파트의 데이터와 아직 뮤테이션되지 않은 파트의 데이터를 함께 보게 됩니다. 진행 상태는 system.mutations 테이블로 추적할 수 있습니다.
뮤테이션은 I/O 집약적이며 클러스터 SELECT 성능에 영향을 줄 수 있으므로 아껴 사용해야 합니다. 뮤테이션 큐가 처리보다 빠르게 쌓이면 쿼리 성능이 저하됩니다. system.mutations로 큐를 모니터링하세요.
더 읽어보기: ALTER TABLE UPDATE.
on-the-fly 뮤테이션
ALTER TABLE ... UPDATE로 뮤테이션할 때 변경된 값이 쿼리에 반영되는 것을 보려면 뮤테이션이 백그라운드 프로세스로 적용되기를 기다려야 할 수 있습니다. ClickHouse는 "on-the-fly 뮤테이션"을 통해 이 동작을 바꾸는 방법을 제공합니다. on-the-fly 뮤테이션이 활성화되면 업데이트된 행이 즉시 업데이트로 표시되고 이후 SELECT 쿼리가 변경된 값으로 자동 반환합니다.
on-the-fly 뮤테이션은 쿼리 수준 설정 apply_mutations_on_fly를 활성화해 MergeTree 계열 테이블에 대해 활성화할 수 있습니다.
SET apply_mutations_on_fly = 1;
예제. 테이블을 만들고 몇 가지 뮤테이션을 실행해 봅시다:
CREATE TABLE test_on_fly_mutations (id UInt64, v String)
ENGINE = MergeTree ORDER BY id;
-- Disable background materialization of mutations to showcase
-- default behavior when on-the-fly mutations are not enabled
SYSTEM STOP MERGES test_on_fly_mutations;
SET mutations_sync = 0;
-- Insert some rows in our new table
INSERT INTO test_on_fly_mutations VALUES (1, 'a'), (2, 'b'), (3, 'c');
-- Update the values of the rows
ALTER TABLE test_on_fly_mutations UPDATE v = 'd' WHERE id = 1;
ALTER TABLE test_on_fly_mutations DELETE WHERE v = 'd';
ALTER TABLE test_on_fly_mutations UPDATE v = 'e' WHERE id = 2;
ALTER TABLE test_on_fly_mutations DELETE WHERE v = 'e';
SELECT 쿼리로 업데이트 결과를 확인해 봅시다:
-- Explicitly disable on-the-fly-mutations
SET apply_mutations_on_fly = 0;
SELECT id, v FROM test_on_fly_mutations ORDER BY id;
새 테이블을 쿼리할 때 행 값이 아직 업데이트되지 않았음을 주목하세요:
┌─id─┬─v─┐
│ 1 │ a │
│ 2 │ b │
│ 3 │ c │
└────┴───┘
이제 on-the-fly 뮤테이션을 활성화하면 어떻게 되는지 봅시다:
-- Enable on-the-fly mutations
SET apply_mutations_on_fly = 1;
SELECT id, v FROM test_on_fly_mutations ORDER BY id;
SELECT 쿼리는 이제 뮤테이션이 적용되기를 기다리지 않고 즉시 올바른 결과를 반환합니다:
┌─id─┬─v─┐
│ 3 │ c │
└────┴───┘
on-the-fly 뮤테이션이 활성화되면 뮤테이션은 즉시 구체화되지 않고 SELECT 쿼리 중에만 적용됩니다. 그러나 뮤테이션은 여전히 백그라운드에서 비동기로 구체화되고 있음을 주의하세요. 이것은 무거운 프로세스입니다.
제출된 뮤테이션 수가 일정 시간 간격 동안 백그라운드에서 처리되는 뮤테이션 수를 지속적으로 초과하면, 적용해야 하는 구체화되지 않은 뮤테이션 큐가 계속 커집니다. 이것은 결국 SELECT 쿼리 성능 저하로 이어집니다.
apply_mutations_on_fly 설정을 number_of_mutations_to_throw와 number_of_mutations_to_delay 같은 다른 MergeTree 수준 설정과 함께 활성화해 구체화되지 않은 뮤테이션의 무한한 성장을 제한할 것을 제안합니다.
on-the-fly 뮤테이션은 서브쿼리와 비결정적 함수에 대한 지원이 제한적입니다. 합리적인 크기의 결과를 가진 스칼라 서브쿼리만 지원됩니다(mutations_max_literal_size_to_replace 설정으로 제어). 상수 비결정적 함수만 지원됩니다(예: now() 함수).
이 동작들은 다음 설정으로 제어됩니다:
| 설정 | 설명 | 기본값 |
|---|---|---|
| mutations_execute_nondeterministic_on_initiator | true이면 비결정적 함수가 초기자 복제본에서 실행되고 UPDATE와 DELETE 쿼리에서 리터럴로 대체됩니다. | false |
| mutations_execute_subqueries_on_initiator | true이면 스칼라 서브쿼리가 초기자 복제본에서 실행되고 UPDATE와 DELETE 쿼리에서 리터럴로 대체됩니다. | false |
| mutations_max_literal_size_to_replace | UPDATE와 DELETE 쿼리에서 대체할 직렬화된 리터럴의 최대 크기(바이트). | 16384 (16 KiB) |
경량 업데이트
경량 업데이트는 전통적인 뮤테이션처럼 전체 컬럼을 다시 쓰는 대신 "패치 파트" — 업데이트된 컬럼과 행만 포함하는 특수 데이터 파트 — 를 사용합니다.
UPDATE posts SET AnswerCount = AnswerCount + 1 WHERE Id = 404346
이 접근 방식은 표준 UPDATE 문법을 사용하며 병합을 기다리지 않고 즉시 패치 파트를 만듭니다. 업데이트된 값은 패치 적용을 통해 SELECT 쿼리에서 즉시 보이지만, 물리적으로는 이후 병합 중에만 구체화됩니다. 이것은 경량 업데이트를 예측 가능한 지연으로 작은 비율의 행(테이블의 ~10%까지)을 업데이트하는 데 이상적으로 만듭니다. 벤치마크는 뮤테이션보다 최대 23배 빠를 수 있음을 보여 줍니다.
트레이드오프는 SELECT 쿼리가 패치를 적용할 때 오버헤드를 부담하고, 패치 파트가 파트 한도에 포함된다는 점입니다. ~10% 임계값을 넘으면 패치-온-읽기 오버헤드가 비례적으로 커져, 더 큰 업데이트에는 동기 뮤테이션이 더 효율적입니다.
더 읽어보기: Lightweight UPDATE.
on-the-fly 뮤테이션
on-the-fly 뮤테이션은 이후 SELECT 쿼리가 백그라운드 처리를 기다리지 않고 변경된 값을 자동으로 반환하도록 행을 업데이트하는 메커니즘을 제공합니다. 이것은 일반 뮤테이션의 원자성 한계를 효과적으로 해결합니다.
SET apply_mutations_on_fly = 1;
SELECT ViewCount FROM posts WHERE Id = 404346
┌─ViewCount─┐
│ 26762 │
└───────────┘
-- Increment the count
ALTER TABLE posts UPDATE ViewCount = ViewCount + 1 WHERE Id = 404346
-- The updated value is immediately visible
SELECT ViewCount FROM posts WHERE Id = 404346
┌─ViewCount─┐
│ 26763 │
└───────────┘
뮤테이션과 이후 SELECT 쿼리 모두 apply_mutations_on_fly = 1 설정이 활성화되어 있어야 합니다. 뮤테이션 조건은 ClickHouse Keeper에 저장되어 모든 것을 메모리에 유지하며, 쿼리 중에 on-the-fly로 적용됩니다.
데이터를 업데이트하는 데 여전히 뮤테이션이 사용됨을 주의하세요 — 단지 즉시 구체화되지 않을 뿐입니다. 뮤테이션은 여전히 백그라운드에서 비동기 프로세스로 적용되며 일반 뮤테이션과 같은 무거운 오버헤드를 부담합니다. 이 연산에 사용할 수 있는 표현식도 제한적입니다(세부 사항 참고).
on-the-fly 뮤테이션은 소수의 연산에만 사용해야 합니다 — 많아야 수십 개 정도. Keeper는 조건을 메모리에 저장하므로 과도한 사용은 클러스터 안정성에 영향을 줍니다. 무거운 Keeper 부하는 관련 없는 테이블에 영향을 주는 세션 타임아웃을 일으킬 수 있습니다.
더 읽어보기: On-the-fly mutations.
비교 요약
다음 표는 벤치마크에 기반한 쿼리 성능 오버헤드를 요약합니다. 뮤테이션은 뮤테이션이 완료되고 데이터가 물리적으로 다시 쓰여지면 쿼리가 최고 속도로 실행되므로 기준선 역할을 합니다.
| 방법 | 쿼리 느려짐 | 메모리 오버헤드 | 참고 |
|---|---|---|---|
| 뮤테이션 (Mutations) | 기준선 | 기준선 | 완료 후 최고 속도; 데이터가 물리적으로 다시 쓰여짐 |
| on-the-fly 뮤테이션 | 가변 | 가변 | 즉시 가시성; 업데이트가 많이 쌓이면 성능 저하 |
| 경량 업데이트 (Lightweight updates) | 7–18% (평균 ~12%) | +20–210% | 쿼리에 가장 효율적; 테이블의 ≤10% 업데이트에 최적 |
| ReplacingMergeTree + FINAL | 21–550% (평균 ~280%) | 기준의 20–200× | 모든 행 버전을 읽어야 함; 가장 무거운 쿼리 오버헤드 |
| CoalescingMergeTree + FINAL | ReplacingMergeTree와 유사 | ReplacingMergeTree와 유사 | 컬럼 수준 병합이 비슷한 오버헤드 추가 |
| CollapsingMergeTree | 집계에 의존 | 집계에 의존 | 오버헤드는 쿼리 복잡도에 의존 |
더 많은 자료
ClickHouse에서 업데이트가 시간에 따라 어떻게 발전해 왔는지, 벤치마킹 분석과 함께 깊이 있게 보려면 다음을 참고하세요:
- Updates in ClickHouse Part 1: Purpose-Built Engines
- Updates in ClickHouse Part 2: SQL-Style Updates
- Updates in ClickHouse Part 3: Benchmarks