ReplacingMergeTree의 정합성 지연과 FINAL 키워드
🌓

ReplacingMergeTree의 정합성 지연과 FINAL 키워드

ClickHouse 분류
Core Architecture
Type
Lab
작성자

Ken

ReplacingMergeTree는 Eventual Consistency를 준수하지만, 그 시간을 어느 정도 기간으로 확정할 수 있는지를 확인하기 위해 테스트 하였습니다. 결론부터 말씀드리면, ReplacingMergeTree의 중복 제거 지연 시간은 "측정 불가"입니다. 이론적으로 무한대까지 가능하며, 정확한 결과가 필요하면 반드시 FINAL 키워드 또는 argMax 패턴을 사용해야 합니다.

  • 들어가며
  • 배경: ReplacingMergeTree의 동작 원리
  • 실험 1: FINAL 유무에 따른 결과 차이
  • 실험 환경
  • 테이블 생성
  • 데이터 삽입 (3회 별도 INSERT)
  • 결과 비교
  • 실험 2: 대량 데이터에서의 정합성 불일치
  • 시나리오
  • 결과: 행 수 차이
  • 결과: 집계 값 차이
  • 핵심 질문: Merge는 언제 발생하는가?
  • 시간 경과 관찰
  • part_log 확인
  • 왜 "최대 지연 시간"을 측정할 수 없는가?
  • 이유 1: Merge는 확정적(Deterministic)이 아니다
  • 이유 2: Merge 우선순위는 파트 크기에 의존
  • 이유 3: 시스템 부하가 Merge를 지연
  • 이유 4: 연속 삽입 시 악화
  • 결론
  • 해결 방안
  • 방안 1: FINAL 키워드 (가장 안전)
  • 방안 2: argMax/argMin 함수
  • 방안 3: 주기적 OPTIMIZE FINAL
  • 방안 4: min_age_to_force_merge_seconds 설정
  • 패턴 선택 가이드
  • 모니터링 권장 쿼리
  • 결론
  • ReplacingMergeTree의 핵심 특성
  • 실무 핵심 권장사항

들어가며

ClickHouse의 ReplacingMergeTree는 동일한 정렬 키를 가진 행들 중 최신 버전만 유지하는 테이블 엔진입니다. 하지만 이 "중복 제거"는 즉시 일어나지 않습니다.

이 글에서는 실제 ClickHouse Cloud 환경에서 실험을 통해:

  • FINAL 키워드 사용 유무에 따른 데이터 정합성 차이
  • 왜 "최대 지연 시간"을 정확히 측정할 수 없는지
  • 실무에서의 해결 방안

을 다룹니다.

배경: ReplacingMergeTree의 동작 원리

ReplacingMergeTree는 동일한 ORDER BY 키를 가진 행들 중 가장 최신 버전만 유지합니다. 하지만 중요한 점은:

INSERT → 새 파트 생성 → (언젠가) Background Merge → 중복 제거

중복 제거는 INSERT 시점이 아닌 Merge 시점에 발생합니다.

실험 1: FINAL 유무에 따른 결과 차이

실험 환경

  • ClickHouse Cloud (SharedMergeTree)
  • 테스트 일시: 2025-12-14

테이블 생성

CREATE TABLE blog_test.user_events_replacing
(
    user_id UInt64,
    event_type String,
    event_value Int64,
    updated_at DateTime DEFAULT now()
)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY (user_id, event_type)

데이터 삽입 (3회 별도 INSERT)

결과 비교

FINAL 없이 조회:

SELECT user_id, event_value, updated_at, _part
FROM blog_test.user_events_replacing ORDER BY user_id, updated_at;
user_id
event_value
updated_at
_part
1
100
10:00:00
all_0_0_1
1
150
11:00:00
all_1_1_1
1
999
12:00:00
all_2_2_1
2
200
10:00:00
all_0_0_1
2
250
11:00:00
all_1_1_1
3
300
10:00:00
all_0_0_1

→ 6개 행 반환 (중복 포함)

FINAL 사용 조회:

SELECT user_id, event_value, updated_at
FROM blog_test.user_events_replacing FINAL ORDER BY user_id;
user_id
event_value
updated_at
1
999
12:00:00
2
250
11:00:00
3
300
10:00:00

→ 3개 행 반환 (정확한 최신값)

실험 2: 대량 데이터에서의 정합성 불일치

시나리오

결과: 행 수 차이

쿼리 유형
행 수
비고
WITHOUT FINAL
160,000
중복 포함
WITH FINAL
100,000
정확한 결과

→ 60% 더 많은 행이 중복으로 존재!

결과: 집계 값 차이

쿼리 유형
sum(event_value)
avg(event_value)
WITHOUT FINAL
224,776,927
1,404.86
WITH FINAL
184,866,480
1,848.66

→ 집계 결과가 약 22% 차이남!

핵심 질문: Merge는 언제 발생하는가?

시간 경과 관찰

데이터 삽입 후 파트 상태를 모니터링했습니다:

SELECT name, rows, modification_time,
       dateDiff('second', modification_time, now()) as age_seconds
FROM system.parts
WHERE database = 'blog_test' AND table = 'user_events_large' AND active = 1;
시점
파트 수
자동 Merge 발생
삽입 직후
3개
No
+2분
3개
No
+7분
3개
No
+새 INSERT
4개
No

→ 7분이 지나도 자연 merge가 발생하지 않음!

part_log 확인

SELECT event_type, part_name, rows
FROM system.part_log
WHERE database = 'blog_test' AND event_type = 'MergeParts';

결과: MergeParts 이벤트 없음

왜 "최대 지연 시간"을 측정할 수 없는가?

이유 1: Merge는 확정적(Deterministic)이 아니다

Background merge 스케줄러는 다음 설정에 의해 동작합니다:

설정
기본값
의미
merge_selecting_sleep_ms
5000ms
Merge 후보 선택 간격
max_merge_selecting_sleep_ms
60000ms
최대 대기 시간
min_age_to_force_merge_seconds
0
강제 merge (비활성화)

min_age_to_force_merge_seconds = 0은 **"시간 기반 강제 merge가 없다"**는 의미입니다.

이유 2: Merge 우선순위는 파트 크기에 의존

ClickHouse는 write amplification을 최소화하기 위해 작은 파트들을 우선 merge합니다:

max_bytes_to_merge_at_max_space_in_pool = 150GB
max_bytes_to_merge_at_min_space_in_pool = 1MB

결과적으로:

  • 대형 파트 + 소형 파트 공존 시 → 소형끼리만 merge
  • 대형 파트의 중복은 무기한 방치 가능

이유 3: 시스템 부하가 Merge를 지연

상황
Merge 지연 가능성
CPU 부하 높음
높음
디스크 I/O 포화
매우 높음
동시 INSERT 다수
높음
메모리 압박
높음

이유 4: 연속 삽입 시 악화

지속적인 INSERT가 발생하면:

  1. 새 파트가 계속 생성됨
  2. Merge 스케줄러가 새 파트 처리에 집중
  3. 기존 중복 파트 merge가 후순위로 밀림
  4. 정합성 지연이 누적

결론

"최대 지연 시간"은 이론적으로 무한대입니다.

확정적 상한선이 없으며, 외부 요인(부하, 삽입 패턴, 파트 크기)에 전적으로 의존합니다.

해결 방안

방안 1: FINAL 키워드 (가장 안전)

SELECT * FROM user_events FINAL WHERE user_id = 123;

✅ 항상 정확한 결과

⚠️ 쿼리 오버헤드 존재

방안 2: argMax/argMin 함수

SELECT
    user_id,
    argMax(event_value, updated_at) as latest_value,
    max(updated_at) as latest_time
FROM user_events
GROUP BY user_id;

✅ FINAL 없이 정확한 결과

✅ 복잡한 집계 가능

⚠️ GROUP BY 필수

방안 3: 주기적 OPTIMIZE FINAL

-- 스케줄러로 실행
OPTIMIZE TABLE user_events FINAL;

실험에서 OPTIMIZE 실행 후:

상태
파트 수
행 수
실행 전
4개
170,000 (중복)
실행 후
1개
100,000 (정확)

✅ Background에서 정합성 보장

⚠️ I/O 부하, 주기적 실행 필요

방안 4: min_age_to_force_merge_seconds 설정

CREATE TABLE user_events (...)
ENGINE = ReplacingMergeTree(updated_at)
ORDER BY (user_id)
SETTINGS min_age_to_force_merge_seconds = 3600;

✅ 자동화된 정합성 보장

⚠️ 설정 시간까지는 여전히 불일치 가능

패턴 선택 가이드

상황
권장 패턴
단순 조회, 정확성 최우선
FINAL
복잡한 집계 쿼리
argMax/argMin
배치 처리 후 정합성 필요
OPTIMIZE FINAL
대시보드 (약간의 지연 허용)
일반 쿼리 + 주기적 OPTIMIZE
실시간 정확성 필수
FINAL 또는 argMax

모니터링 권장 쿼리

-- 파트 상태 모니터링
SELECT
    table,
    count() as part_count,
    sum(rows) as total_rows,
    min(modification_time) as oldest_part,
    dateDiff('hour', min(modification_time), now()) as oldest_age_hours
FROM system.parts
WHERE database = 'your_db' AND active = 1
GROUP BY table
HAVING part_count > 10
ORDER BY oldest_age_hours DESC;

파트가 많거나 오래된 파트가 있으면 OPTIMIZE FINAL을 검토하세요.

결론

ReplacingMergeTree의 핵심 특성

  1. 중복 제거는 Merge 시점에 발생 → 즉시성 보장 없음
  2. FINAL 없이 조회하면 중복 노출 가능
  3. 자연 Merge 타이밍은 예측 불가
  4. 최대 지연 시간은 이론적으로 무한대

실무 핵심 권장사항

  1. 정확성이 필요하면 반드시 FINAL 또는 argMax 사용
  2. "어느 정도 지연되어도 괜찮은지" 먼저 판단
  3. 주기적 OPTIMIZE FINAL 스케줄링 검토
  4. system.parts로 파트 상태 모니터링
ReplacingMergeTree는 "eventually consistent" 엔진입니다.

정확한 결과가 필요하면, 직접 FINAL을 명시하세요.

활용 코드

clickhouse-hols/workload/replacingmergetree at main · litkhai/clickhouse-hols

ClickHouse Hands-on Labs . Contribute to litkhai/clickhouse-hols development by creating an account on GitHub.

clickhouse-hols/workload/replacingmergetree at main · litkhai/clickhouse-hols