본문으로 바로가기
본문으로 바로가기

ClickHouse의 업데이트

업데이트 처리 방식은 기반 설계 철학과 목표 사용 사례가 다르기 때문에, 분석용 데이터베이스와 트랜잭션 데이터베이스가 서로 다르게 접근합니다. ClickHouse는 읽기 중심 분석과 높은 처리량의 append-only 작업에 최적화된 컬럼 지향 데이터베이스입니다. 실제 환경에서는 삭제 및 업데이트를 append 작업으로 전환할 수 있도록 테이블을 재구성하는 경우가 많습니다. 이러한 작업은 비동기적으로 처리되거나 읽기 시점에 처리되며, 이를 통해 ClickHouse의 강점인 높은 처리량의 데이터 수집 성능을 활용할 수 있습니다. ClickHouse는 강력한 업데이트 및 삭제 작업도 지원합니다.

이 가이드에서는 ClickHouse에서 사용할 수 있는 업데이트 방법을 개괄적으로 설명하고, 워크로드에 적합한 업데이트 전략을 선택할 수 있도록 안내합니다.

업데이트 전략 선택

ClickHouse에서 데이터를 업데이트하는 기본적인 접근 방식은 두 가지입니다:

  1. 특화된 테이블 엔진을 사용하여 삽입을 통해 업데이트를 처리하는 방법
  2. UPDATE ... SET 또는 ALTER TABLE ... UPDATE 문과 같은 선언적 업데이트를 사용하는 방법

위의 두 범주 각각에는 데이터를 업데이트하는 여러 가지 방법이 있습니다. 각 방법은 장점과 성능 특성이 다르므로, 데이터 모델과 업데이트하려는 데이터 양에 따라 적절한 방법을 선택해야 합니다.

특화된 테이블 엔진을 사용할 때

대량의 업데이트가 있거나 행 단위 변경이 자주 발생하거나, 업데이트 및 삭제 이벤트의 연속 스트림을 처리해야 하는 경우에는 특화된 테이블 엔진을 사용하는 것이 더 적합합니다.

일반적으로 가장 많이 사용하는 엔진은 다음과 같습니다:

EngineSyntaxWhen to use
ReplacingMergeTreeENGINE = ReplacingMergeTree대량의 데이터를 업데이트해야 할 때 사용합니다. 이 테이블 엔진은 머지 과정에서 데이터 중복 제거에 최적화되어 있습니다.
CoalescingMergeTreeENGINE = CoalescingMergeTree데이터가 조각(fragment) 형태로 도착하고, 전체 행 교체가 아니라 컬럼 단위 병합(coalescing)이 필요할 때 사용합니다.
CollapsingMergeTreeENGINE = CollapsingMergeTree(Sign)개별 행을 자주 업데이트해야 하거나, 시간이 지나면서 변경되는 객체의 최신 상태를 유지해야 하는 시나리오에서 사용합니다. 예를 들어, 사용자 활동이나 게시글 통계를 추적하는 경우에 적합합니다.

MergeTree 계열 테이블 엔진은 백그라운드에서 데이터 파트(parts)를 머지하므로 결과적 일관성(eventual consistency) 을 제공합니다. 따라서 테이블을 쿼리하는 중간 단계에서 올바른 중복 제거를 보장하려면 FINAL 키워드를 사용해야 합니다. 다른 엔진 유형도 있지만, 여기서 소개한 엔진들이 가장 일반적으로 사용됩니다.

선언적 업데이트를 사용해야 하는 경우

선언적 UPDATE 문은 중복 제거 로직을 관리하는 복잡성 없이 단순한 업데이트 작업을 수행할 때 더 직관적일 수 있지만, 일반적으로 특화된 테이블 엔진보다 더 적은 수의 행을, 더 낮은 빈도로 업데이트하는 경우에 더 적합합니다.

MethodSyntaxWhen to use
Lightweight updatesUPDATE [table] SET ... WHERE대부분의 시나리오에서, 특히 애플리케이션이나 워크플로의 일부로 빈번하고 소규모의 UPDATE(테이블의 약 10%까지)를 실행할 때 사용합니다. 예를 들어, 사용자가 자신의 이벤트 기록을 삭제하려고 하는데 이벤트가 많은 사용자가 포함된 멀티테넌트 테이블 전반에 분산되어 있는 경우입니다. 이 접근 방식은 전체 컬럼을 다시 쓰지 않고도 즉시 표시되도록 패치 파트를 생성합니다. SELECT 쿼리에 오버헤드를 추가하지만 지연 시간은 예측 가능합니다.
Update mutationALTER TABLE [table] UPDATE더 큰 규모의 데이터를 관리할 때, 특히 업데이트가 테이블 파티셔닝과 일치하는 경우 사용합니다. 예를 들어, 월 단위로 파티셔닝된 테이블에서 특정 월에 속한 모든 행의 컬럼을 업데이트해야 하는 경우입니다.

특화된 테이블 엔진을 사용한 업데이트

ReplacingMergeTree

ReplacingMergeTree는 백그라운드 머지 중에 동일한 정렬 키를 가진 행을 중복 제거하여 최신 버전만 유지합니다.

CREATE TABLE posts
(
    Id UInt32,
    Title String,
    ViewCount UInt32,
    Version UInt32
)
ENGINE = ReplacingMergeTree(Version)
ORDER BY Id

이 엔진은 안정적인 키로 업데이트를 식별할 수 있는 개별 행에 대한 고빈도 업데이트에 이상적입니다. 벤치마크에 따르면 단일 행 업데이트의 경우 mutation보다 최대 4,700배까지 빠를 수 있습니다.

행을 업데이트하려면 동일한 정렬 키 값과 더 높은 버전 번호를 가진 새 버전을 삽입하면 됩니다. 이전 버전은 백그라운드 머지 중에 제거됩니다. 중복 제거는 결국 일어나는 방식으로 처리되므로(즉, 머지 중에만 발생하므로), 올바르게 중복 제거된 결과를 얻으려면 FINAL 수정자 또는 이에 상응하는 쿼리 로직을 사용해야 합니다. FINAL 수정자는 데이터에 따라 21~550% 범위의 쿼리 오버헤드를 추가합니다.

ReplacingMergeTree는 정렬 키 값을 업데이트할 수 없습니다. 또한 논리적 삭제를 위한 Deleted 컬럼도 지원합니다.

참조하다: ReplacingMergeTree guide | ReplacingMergeTree reference

CoalescingMergeTree

CoalescingMergeTree는 머지 과정에서 각 컬럼에 대해 가장 최근의 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는 전체 행을 교체하는 대신 각 컬럼별로 가장 최근의 널이 아닌 값을 유지합니다. 의도대로 동작하도록 하려면 키가 아닌 컬럼은 Nullable이어야 합니다. ReplacingMergeTree와 마찬가지로 올바르게 병합(coalesce)된 결과를 얻으려면 FINAL을 사용해야 합니다.

이 엔진은 ClickHouse 25.6부터 사용할 수 있습니다.

자세한 내용: CoalescingMergeTree

CollapsingMergeTree

업데이트는 비용이 많이 들지만 INSERT 작업을 활용해 업데이트를 구현할 수 있다는 아이디어에서 출발하여, CollapsingMergeTreeSign 컬럼을 사용해 머지 과정에서 ClickHouse가 행을 어떻게 처리할지 지정합니다. Sign 컬럼에 -1이 INSERT되면, 해당 행은 일치하는 +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 계열 엔진을 사용하는 테이블에서 동작합니다.

MethodSyntaxBest forTrade-offs
MutationsALTER TABLE ... UPDATE드문 대량 업데이트, 특히 업데이트가 테이블 파티셔닝과 일치할 때 적합함I/O 부하가 큼; 컬럼을 다시 기록해야 함
Lightweight updatesUPDATE ... 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

실시간 뮤테이션

ALTER TABLE ... UPDATE를 통한 뮤테이션은 백그라운드 프로세스를 통해 적용되므로, 변경된 값이 쿼리에 반영되기까지 대기해야 할 수 있습니다. ClickHouse는 "실시간 뮤테이션"을 통해 이러한 동작을 변경할 수 있는 방법을 제공합니다. 실시간 뮤테이션이 활성화되면 업데이트된 행은 즉시 업데이트된 것으로 마크되며, 이후 실행되는 SELECT 쿼리에서는 변경된 값이 자동으로 반환됩니다.

실시간 뮤테이션은 쿼리 수준 설정인 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;

-- 예시를 위해 뮤테이션의 백그라운드 구체화를 비활성화합니다
-- 실시간 뮤테이션이 활성화되지 않았을 때의 기본 동작을 보여줍니다
SYSTEM STOP MERGES test_on_fly_mutations;
SET mutations_sync = 0;

-- 새 테이블에 몇 개의 행을 삽입합니다
INSERT INTO test_on_fly_mutations VALUES (1, 'a'), (2, 'b'), (3, 'c');

-- 행 값을 업데이트합니다
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 쿼리로 업데이트 결과를 확인해 보겠습니다:

-- 실시간 뮤테이션을 명시적으로 비활성화합니다
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 │
└────┴───┘

이제 실시간 뮤테이션을 활성화했을 때 어떤 일이 발생하는지 살펴보겠습니다:

-- 실시간 뮤테이션을 활성화합니다
SET apply_mutations_on_fly = 1;

SELECT id, v FROM test_on_fly_mutations ORDER BY id;

이제 SELECT 쿼리는 뮤테이션이 적용될 때까지 대기하지 않아도 즉시 올바른 결과를 반환합니다:

┌─id─┬─v─┐
│  3 │ c │
└────┴───┘
성능 영향

실시간 뮤테이션이 활성화되면 뮤테이션은 즉시 구체화되지 않고 SELECT 쿼리 시에만 적용됩니다. 다만 뮤테이션은 여전히 백그라운드에서 비동기적으로 구체화되며, 이는 리소스 소모가 큰 작업이라는 점에 유의하십시오.

일정 시간 동안 등록되는 뮤테이션 수가 백그라운드에서 처리되는 뮤테이션 수를 계속 초과하면, 적용해야 하는 미구체화 뮤테이션의 대기열은 계속 늘어납니다. 그 결과 결국 SELECT 쿼리 성능이 저하됩니다.

미구체화 뮤테이션이 무한정 증가하는 것을 제한하려면 apply_mutations_on_fly 설정을 number_of_mutations_to_throw, number_of_mutations_to_delay와 같은 다른 MergeTree 수준 설정과 함께 활성화하는 것이 좋습니다.

서브쿼리 및 비결정적 함수 지원

실시간 뮤테이션은 서브쿼리 및 비결정적 함수에 대해 제한적으로 지원됩니다. 합리적인 크기의 결과를 반환하는 스칼라 서브쿼리만 지원되며(크기는 설정 mutations_max_literal_size_to_replace로 제어됨), 상수 비결정적 함수만 지원됩니다(예: now() 함수).

이 동작은 다음 설정으로 제어됩니다:

설정설명기본값
mutations_execute_nondeterministic_on_initiatortrue이면 비결정적 함수가 initiator 레플리카에서 실행되고, UPDATEDELETE 쿼리에서 리터럴로 대체됩니다.false
mutations_execute_subqueries_on_initiatortrue이면 스칼라 서브쿼리가 initiator 레플리카에서 실행되고, UPDATEDELETE 쿼리에서 리터럴로 대체됩니다.false
mutations_max_literal_size_to_replaceUPDATEDELETE 쿼리에서 대체할 직렬화된 리터럴의 최대 크기(바이트 단위)입니다.16384 (16 KiB)

경량 업데이트

경량 업데이트는 전체 컬럼을 다시 기록하는 기존 뮤테이션과 달리, 업데이트된 컬럼과 행만을 포함하는 특수한 데이터 파트인 "패치 파트(patch parts)"를 사용합니다.

UPDATE posts SET AnswerCount = AnswerCount + 1 WHERE Id = 404346

이 방식은 표준 UPDATE 구문을 사용하며 머지 작업을 기다리지 않고 즉시 패치 파트(파트)를 생성합니다. 갱신된 값은 패치 적용을 통해 SELECT 쿼리에서 즉시 확인할 수 있지만, 스토리지에 물리적으로 반영되는 것은 이후 머지 과정에서만 이루어집니다. 이러한 특성 덕분에 경량 업데이트는 예측 가능한 지연 시간으로 테이블의 일부 행(전체의 약 10%까지)만 갱신할 때 이상적입니다. 벤치마크에 따르면 뮤테이션보다 최대 23배까지 빠를 수 있습니다.

대신 SELECT 쿼리는 패치를 적용할 때 오버헤드가 발생하고, 패치 파트는 파트 개수 제한에 포함됩니다. 약 10% 임계값을 넘어서면 읽기 시 패치 적용 오버헤드가 비례하여 증가하므로, 더 큰 규모의 갱신에는 동기식 뮤테이션이 더 효율적입니다.

자세한 내용: Lightweight UPDATE

실시간 뮤테이션

실시간 뮤테이션은 행을 업데이트하여, 이후 실행되는 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에 저장되며, Keeper는 이를 메모리에 유지한 채 쿼리 시점에 실시간(on-the-fly)으로 적용합니다.

데이터 업데이트에는 여전히 뮤테이션이 사용되며, 단지 즉시 구체화(materialize)되지 않을 뿐입니다. 뮤테이션은 여전히 비동기 백그라운드 프로세스로 적용되며, 일반적인 뮤테이션과 동일한 큰 오버헤드를 발생시킵니다. 이 연산에서 사용할 수 있는 표현식도 제한됩니다(자세한 내용 참조).

참고

실시간 뮤테이션은 소수의 작업(많아도 수십 개 정도)에만 사용해야 합니다. Keeper는 조건을 메모리에 저장하므로, 과도하게 사용하면 클러스터 안정성에 영향을 줍니다. Keeper 부하가 심해지면 관련 없는 테이블에도 영향을 미치는 세션 타임아웃이 발생할 수 있습니다.

참조하다: 실시간 뮤테이션

비교 요약

다음 표는 벤치마크를 기반으로 쿼리 성능 오버헤드를 요약한 것입니다. 뮤테이션은 뮤테이션이 완료되고 데이터가 물리적으로 다시 기록된 뒤에는 쿼리가 최대 속도로 실행되므로 기준선 역할을 합니다.

MethodQuery slowdownMemory overheadNotes
Mutations기준선기준선완료 후 최대 속도; 데이터가 물리적으로 다시 기록됨
On-the-fly mutations가변적가변적즉시 반영됨; 많은 업데이트가 누적되면 성능 저하
Lightweight updates7–18% (평균 약 12%)+20–210%쿼리에 가장 효율적임; 테이블의 10% 이하를 업데이트할 때 가장 적합
ReplacingMergeTree + FINAL21–550% (평균 약 280%)기준선의 20–200배모든 행 버전을 읽어야 함; 쿼리 오버헤드가 가장 큼
CoalescingMergeTree + FINALReplacingMergeTree와 유사ReplacingMergeTree와 유사컬럼 수준 코얼레싱으로 인해 비슷한 수준의 오버헤드가 추가됨
CollapsingMergeTree집계에 따라 다름집계에 따라 다름오버헤드는 쿼리 복잡도에 따라 달라짐

추가 자료

ClickHouse에서 업데이트 기능이 시간에 따라 어떻게 발전해 왔는지와 벤치마크 분석을 포함한 심층적인 내용을 살펴보려면 다음 문서를 참고하십시오: