개요
업데이트를 처리하는 방식에서 분석용 데이터베이스와 트랜잭션 데이터베이스는 기본 설계 철학과 목표 사용 사례가 다르기 때문에 상당히 다른 동작 방식을 보입니다. 예를 들어 PostgreSQL은 행 지향이며 ACID를 준수하는 관계형 데이터베이스로, Multi-Version Concurrency Control(MVCC)과 같은 메커니즘을 통해 트랜잭션 특성을 갖는 견고한 업데이트 및 삭제 작업을 지원하여 데이터 일관성과 무결성을 보장합니다. 이를 통해 높은 동시성 환경에서도 안전하고 신뢰할 수 있는 데이터 변경이 가능합니다.
반면 ClickHouse는 읽기 위주의 분석과 높은 처리량의 append-only 작업에 최적화된 컬럼 지향 데이터베이스입니다. ClickHouse는 제자리(in-place) 업데이트와 삭제를 네이티브로 지원하지만, 높은 I/O를 피하려면 신중하게 사용해야 합니다. 또 다른 방법으로는 테이블 구조를 변경하여 삭제와 업데이트를 추가 작업으로 전환한 뒤, 이를 비동기적으로 및/또는 읽기 시점에 처리하도록 구성할 수 있으며, 이를 통해 실시간 데이터 조작보다는 높은 처리량의 데이터 수집과 효율적인 쿼리 성능에 초점을 맞출 수 있습니다.
이 가이드는 ClickHouse에서 사용 가능한 업데이트 방법을 개괄하고, 사용 사례에 적합한 업데이트 전략을 선택하는 데 도움을 줍니다.
업데이트 전략 선택
ClickHouse에서 데이터를 업데이트하는 기본적인 접근 방식은 두 가지입니다:
- 특수 테이블 엔진(specialized table engines) 을 사용하여 insert를 통해 업데이트를 처리하는 방법
UPDATE ... SET또는ALTER TABLE ... UPDATESQL 문과 같은 선언적 업데이트(declarative updates) 를 사용하는 방법
위의 두 범주 각각에도 데이터를 업데이트하는 여러 가지 방법이 있습니다. 각 방법은 고유한 장점과 성능 특성을 가지므로, 데이터 모델과 업데이트하려는 데이터 양에 따라 적절한 방식을 선택해야 합니다.
전용 테이블 엔진을 사용할 때
대량의 업데이트가 있거나 행 단위 변경이 자주 발생하거나, 업데이트 및 삭제 이벤트의 연속 스트림을 처리해야 하는 경우에는 전용 테이블 엔진을 사용하는 것이 더 적합합니다.
일반적으로 가장 많이 사용하는 엔진은 다음과 같습니다:
| Engine | Syntax | When to use |
|---|---|---|
| ReplacingMergeTree | ENGINE = ReplacingMergeTree | 대량의 데이터를 업데이트해야 할 때 사용합니다. 이 테이블 엔진은 머지 과정에서 데이터 중복 제거에 최적화되어 있습니다. |
| CoalescingMergeTree | ENGINE = CoalescingMergeTree | 데이터가 조각(fragment) 형태로 도착하고, 전체 행 교체가 아니라 컬럼 단위 병합(coalescing)이 필요할 때 사용합니다. |
| CollapsingMergeTree | ENGINE = CollapsingMergeTree(Sign) | 개별 행을 자주 업데이트해야 하거나, 시간이 지나면서 변경되는 객체의 최신 상태를 유지해야 하는 시나리오에서 사용합니다. 예를 들어, 사용자 활동이나 게시글 통계를 추적하는 경우에 적합합니다. |
MergeTree 계열 테이블 엔진은 백그라운드에서 데이터 파트(parts)를 머지하므로 결과적 일관성(eventual consistency) 을 제공합니다. 따라서 테이블을 쿼리하는 중간 단계에서 올바른 중복 제거를 보장하려면 FINAL 키워드를 사용해야 합니다.
다른 엔진 유형도 있지만, 여기서 소개한 엔진들이 가장 일반적으로 사용됩니다.
선언적 업데이트를 사용해야 하는 경우
선언적 UPDATE SQL 문은 중복 제거(deduplication) 로직을 직접 관리하는 복잡성 없이 단순한 업데이트 작업을 수행할 때 더 직관적일 수 있습니다. 그러나 일반적으로 전문화된 테이블 엔진을 사용하는 경우보다 적은 수의 행을 상대적으로 드물게 업데이트하는 데 더 적합합니다.
| Method | Syntax | When to use |
|---|---|---|
| Update mutation | ALTER TABLE [table] UPDATE | 데이터를 디스크에 즉시 업데이트해야 할 때(예: 컴플라이언스 목적) 사용합니다. SELECT 성능에 부정적인 영향을 줍니다. |
| On-the-fly updates | ALTER TABLE [table] UPDATE | 적은 양의 데이터를 업데이트할 때 사용합니다. 이후의 모든 SELECT 쿼리에서 행은 즉시 갱신된 데이터로 반환되지만, 처음에는 디스크 상에서 내부적으로만 업데이트된 것으로 표시됩니다. SET apply_mutations_on_fly = 1; 설정으로 활성화합니다. |
| Lightweight updates | UPDATE [table] SET ... WHERE | 적은 양의 데이터(테이블의 약 10%까지)를 업데이트할 때 사용합니다. 전체 컬럼을 다시 쓰지 않고도 즉시 조회 가능하도록 패치 파트(patch parts)를 생성합니다. SELECT 쿼리에 오버헤드를 추가하지만 지연 시간은 예측 가능합니다. |
특화된 테이블 엔진을 사용한 업데이트
ReplacingMergeTree
ReplacingMergeTree는 백그라운드 병합 과정에서 동일한 정렬 키를 가진 행을 중복 제거하여 최신 버전만 유지합니다.
이 엔진은 안정적인 키로 업데이트를 식별할 수 있는 개별 행에 대한 빈번한 업데이트에 이상적입니다. 벤치마크에 따르면 단일 행 업데이트의 경우 뮤테이션보다 최대 4,700배까지 빠를 수 있습니다.
행을 업데이트하려면 동일한 정렬 키 값과 더 높은 버전 번호를 가진 새 버전을 삽입하면 됩니다. 오래된 버전은 백그라운드 머지 중에 제거됩니다. 중복 제거는 머지 시점에만 수행되는 최종적(eventual) 처리이므로, 올바르게 중복 제거된 결과를 얻으려면 FINAL 수정자 또는 동등한 쿼리 로직을 사용해야 합니다. FINAL 수정자는 데이터에 따라 21~550% 범위의 쿼리 오버헤드를 추가합니다.
ReplacingMergeTree는 정렬 키 값을 업데이트할 수 없습니다. 또한 논리적 삭제를 위한 Deleted 컬럼을 지원합니다.
자세히 알아보기: ReplacingMergeTree 가이드 | ReplacingMergeTree 레퍼런스 문서
CoalescingMergeTree
CoalescingMergeTree는 머지 과정에서 각 컬럼에 대해 가장 최근의 null이 아닌 값을 유지하여 희소 레코드를 통합합니다. 이를 통해 전체 행을 대체하는 대신 컬럼 수준의 업서트(upsert)가 가능해집니다.
이 엔진은 데이터가 여러 소스에서 조각난 형태로 유입되거나, 서로 다른 컬럼이 서로 다른 시점에 채워지는 시나리오를 위해 설계되었습니다. 일반적인 사용 사례로는 분산된 하위 시스템으로부터 수집되는 IoT 텔레메트리 데이터, 사용자 프로필 정보 보강, 지연된 차원값이 있는 ETL 파이프라인 등이 있습니다.
동일한 정렬 키를 가진 행이 병합될 때 CoalescingMergeTree는 전체 행을 교체하는 대신 각 컬럼별로 가장 최근의 널이 아닌 값을 유지합니다. 의도대로 동작하도록 하려면 키가 아닌 컬럼은 Nullable이어야 합니다. ReplacingMergeTree와 마찬가지로 올바르게 병합(coalesce)된 결과를 얻으려면 FINAL을 사용해야 합니다.
이 엔진은 ClickHouse 25.6부터 사용할 수 있습니다.
자세한 내용: CoalescingMergeTree
CollapsingMergeTree
업데이트는 비용이 많이 들지만 INSERT 작업을 활용해 업데이트를 구현할 수 있다는 아이디어에서 출발하여, CollapsingMergeTree는 Sign 컬럼을 사용해 머지 과정에서 ClickHouse가 행을 어떻게 처리할지 지정합니다. Sign 컬럼에 -1이 INSERT되면, 해당 행은 일치하는 +1 행과 짝을 이룰 때 병합(삭제)됩니다. 업데이트할 행은 테이블을 생성할 때 사용한 ORDER BY 절의 정렬 키를 기준으로 식별됩니다.
ReplacingMergeTree와 달리 CollapsingMergeTree에서는 정렬 키 값을 수정할 수 있습니다. 금융 거래나 게임 상태 추적처럼 취소 시맨틱스를 갖는 가역적 연산에 적합합니다.
위에서 설명한 업데이트 방식은 애플리케이션이 취소 행을 삽입하기 위해 클라이언트 측에서 상태를 유지해야 합니다. 이는 ClickHouse 관점에서 가장 효율적인 방식이지만, 대규모 환경에서는 다루기 복잡해질 수 있습니다. 또한 올바른 결과를 얻기 위해 쿼리에서 sign 곱셈을 활용한 집계가 필요합니다.
더 알아보기: CollapsingMergeTree
선언적 업데이트
다음 방법은 MergeTree family 엔진을 사용하는 테이블에서 동작합니다.
| Method | Syntax | Best for | Trade-offs |
|---|---|---|---|
| Mutations | ALTER TABLE ... UPDATE | 데이터의 물리적 삭제를 요구하는 규제 준수; 드문 대량 업데이트 | I/O 부하가 큼; 컬럼을 다시 기록해야 함 |
| Lightweight updates | UPDATE ... SET ... WHERE | 작은 규모의 업데이트(행의 약 0.1-10%); 성능이 중요한 빈번한 업데이트 | SELECT 오버헤드 증가; 패치 파트 개수가 제한에 포함됨 |
| On-the-fly mutations | ALTER TABLE ... UPDATE with apply_mutations_on_fly=1 | 즉각적인 반영; 제한된 횟수의 작업 | Keeper 종속성; 수십 개 작업 수준까지로 확장에 한계가 있음 |
뮤테이션
뮤테이션(ALTER TABLE ... UPDATE)은 WHERE 표현식과 일치하는 행을 포함하는 모든 파트를 다시 씁니다. 이를 통해 디스크 상에서 데이터가 실제로 수정되었음을 보장합니다.
규제 준수를 위해 물리적인 데이터 삭제가 보장되어야 하거나, I/O 오버헤드를 감내할 수 있는 드문 대량 업데이트 작업에는 뮤테이션을 사용합니다.
뮤테이션은 상당한 I/O 부하를 유발하며, WHERE 조건과 일치하는 모든 파트를 다시 작성합니다. 이 과정에는 원자성이 없습니다. 파트는 뮤테이션된 파트가 준비되는 즉시 교체되며, 뮤테이션이 진행되는 동안 실행을 시작한 SELECT 쿼리는 이미 뮤테이션이 완료된 파트의 데이터와 아직 뮤테이션되지 않은 파트의 데이터를 함께 보게 됩니다. 진행 상태는 system.mutations 테이블을 통해 추적할 수 있습니다.
뮤테이션은 I/O 사용량이 많아 클러스터의 SELECT 성능에 영향을 줄 수 있으므로, 가능한 한 적게 사용하는 것이 좋습니다. 뮤테이션이 처리 속도보다 빠르게 큐에 쌓이면 쿼리 성능이 저하됩니다. system.mutations를 통해 큐를 모니터링하십시오.
자세한 내용: ALTER TABLE UPDATE
경량 업데이트
경량 업데이트는 전체 컬럼을 다시 기록하는 기존 뮤테이션과 달리, 업데이트된 컬럼과 행만을 포함하는 특수한 데이터 파트인 "패치 파트(patch parts)"를 사용합니다.
이 방식은 표준 UPDATE 구문을 사용하며 머지 작업을 기다리지 않고 즉시 패치 파트(파트)를 생성합니다. 갱신된 값은 패치 적용을 통해 SELECT 쿼리에서 즉시 확인할 수 있지만, 스토리지에 물리적으로 반영되는 것은 이후 머지 과정에서만 이루어집니다. 이러한 특성 덕분에 경량 업데이트는 예측 가능한 지연 시간으로 테이블의 일부 행(전체의 약 10%까지)만 갱신할 때 이상적입니다. 벤치마크에 따르면 뮤테이션보다 최대 23배까지 빠를 수 있습니다.
대신 SELECT 쿼리는 패치를 적용할 때 오버헤드가 발생하고, 패치 파트는 파트 개수 제한에 포함됩니다. 약 10% 임계값을 넘어서면 읽기 시 패치 적용 오버헤드가 비례하여 증가하므로, 더 큰 규모의 갱신에는 동기식 뮤테이션이 더 효율적입니다.
자세한 내용: Lightweight UPDATE
실시간 뮤테이션
실시간 뮤테이션은 행을 업데이트하여, 이후 실행되는 SELECT 쿼리가 백그라운드 처리가 완료되기를 기다리지 않고도 변경된 값을 자동으로 반환할 수 있게 하는 메커니즘을 제공합니다. 이를 통해 일반 뮤테이션이 가진 원자성 측면의 한계를 효과적으로 해소합니다.
뮤테이션과 이후의 SELECT 쿼리 모두에서 apply_mutations_on_fly = 1 설정을 활성화해야 합니다. 뮤테이션 조건은 ClickHouse Keeper에 저장되며, Keeper는 이를 메모리에 유지한 채 쿼리 시점에 실시간(on-the-fly)으로 적용합니다.
데이터 업데이트에는 여전히 뮤테이션이 사용되며, 단지 즉시 구체화(materialize)되지 않을 뿐입니다. 뮤테이션은 여전히 비동기 백그라운드 프로세스로 적용되며, 일반적인 뮤테이션과 동일한 큰 오버헤드를 발생시킵니다. 이 연산에서 사용할 수 있는 표현식도 제한됩니다(자세한 내용 참조).
On-the-fly 뮤테이션은 소수의 작업(많아도 수십 개 정도)에만 사용해야 합니다. Keeper는 조건을 메모리에 저장하므로, 과도하게 사용하면 클러스터 안정성에 영향을 줍니다. Keeper 부하가 심해지면 관련 없는 테이블에도 영향을 미치는 세션 타임아웃이 발생할 수 있습니다.
자세히 보기: On-the-fly 뮤테이션
비교 요약
다음 표는 벤치마크를 기반으로 한 쿼리 성능 오버헤드를 요약합니다. 뮤테이션은 기준선으로 사용되며, 뮤테이션이 완료되어 데이터가 물리적으로 다시 기록되면 쿼리는 최대 속도로 실행됩니다.
| Method | Query slowdown | Memory overhead | Notes |
|---|---|---|---|
| Mutations | 기준선 | 기준선 | 완료 후 최대 속도; 데이터가 물리적으로 다시 기록됨 |
| On-the-fly mutations | 가변적 | 가변적 | 즉시 반영; 많은 업데이트가 누적되면 성능 저하 |
| Lightweight updates | 7–18% (평균 약 12%) | +20–210% | 쿼리에 가장 효율적; 테이블의 최대 10% 이하 업데이트에 가장 적합 |
ReplacingMergeTree + FINAL | 21–550% (평균 약 280%) | 기준선의 20–200배 | 모든 행 버전을 읽어야 함; 쿼리 오버헤드가 가장 큼 |
CoalescingMergeTree + FINAL | ReplacingMergeTree와 유사 | ReplacingMergeTree와 유사 | 컬럼 단위 병합으로 유사한 수준의 오버헤드 발생 |
| CollapsingMergeTree | 집계에 따라 다름 | 집계에 따라 다름 | 오버헤드는 쿼리 복잡도에 따라 달라짐 |
추가 자료
ClickHouse에서 업데이트 기능이 시간에 따라 어떻게 발전해 왔는지와 벤치마크 분석을 포함한 심층적인 내용을 살펴보려면 다음 문서를 참고하십시오: