Ken
이 문서는 ClickHouse의 MergeTree 기반 엔진에서 사용되는 FINAL 키워드의 필요성, 동작 방식, 성능 특성, 실무 활용 전략, 최신 개선 현황, 그리고 대체 및 우회 방안까지 폭넓게 다룹니다. FINAL 키워드는 ClickHouse의 eventual consistency 아키텍처와 밀접히 연관되어 있으며, 쿼리 정확성과 성능 최적화 사이의 균형을 어떻게 잡을지에 대한 고민을 필요로 합니다.
1. FINAL 키워드의 탄생 배경과 필요성
ClickHouse의 MergeTree 계열(예: ReplacingMergeTree, CollapsingMergeTree) 엔진은 데이터 삽입 시마다 작은 정렬된 파트(parts)를 생성하고, 백그라운드에서 이 파트들을 병합(merge)하여 더 큰 파트로 통합합니다. 이 과정은 비동기적으로 이루어지므로, 중복된 행이나 오래된 버전의 행이 백그라운드 머지가 완료되기 전까지 테이블에 남아 있을 수 있습니다. 예를 들어 ReplacingMergeTree는 동일 키의 중복 행이 즉시 제거되지 않고, 머지 작업이 끝나야만 중복이 사라집니다. 따라서 쿼리 시점에 따라 중복 행이 결과에 포함되는 현상이 발생할 수 있습니다.
이러한 eventual consistency의 한계를 쿼리 시점에서 해소하기 위해 도입된 것이 FINAL 키워드입니다. SELECT 쿼리에서 테이블명 뒤에 FINAL을 붙이면, 쿼리 실행 중에 즉석에서 병합 및 중복 제거 로직을 적용하여 중복 없는 최신 상태의 결과를 반환합니다. 이는 저장된 데이터 구조를 물리적으로 변경하지 않고, 쿼리 결과의 일관성을 보장하는 역할을 합니다.
특히 ReplacingMergeTree, CollapsingMergeTree처럼 백그라운드 머지로 중복 제거를 수행하는 엔진에서는, 정확한 결과가 필요한 상황(예: 최신 버전의 행만 필요할 때)에 반드시 FINAL이 필요합니다. 공식 문서와 커뮤니티에서도 “ReplacingMergeTree에서 중복 없는 정확한 결과를 위해 FINAL이 종종 필요하다”고 명시하고 있습니다. SummingMergeTree 등에서도 머지가 완료되지 않으면 합산값이 부정확할 수 있는데, 이때도 FINAL이 일관된 결과를 제공합니다[1].
한편, OPTIMIZE ... FINAL 명령과 SELECT ... FINAL은 다릅니다. OPTIMIZE FINAL은 실제로 모든 파트를 하나로 병합해 디스크 상의 중복 데이터를 영구적으로 제거하지만, 비용이 매우 크고 테이블 락 등 부작용이 있습니다. 반면 SELECT ... FINAL은 쿼리 시점에만 일시적으로 병합 로직을 적용해 결과만 중복 없이 보여줍니다. 실무에서는 OPTIMIZE FINAL은 최소화하고, 필요한 쿼리에만 FINAL을 붙여 실시간 중복 제거 결과를 얻는 것이 권장됩니다.
2. FINAL 키워드 사용 예시
- ReplacingMergeTree에서 최신 행 조회
- 차원 테이블 조인 시 FINAL 사용
- 뷰/머티리얼라이즈드 뷰 활용
- 세션/프로필 단위 FINAL 자동 적용
예를 들어, hits_rmt 테이블에서 특정 FUniqID에 해당하는 이벤트의 최신 레코드만 조회하려면 다음과 같이 FINAL을 사용합니다.
SELECT * FROM hits_rmt FINAL WHERE FUniqID = '7411978401145070562';
이 쿼리는 약 1.1억 행 중 중복을 제거해 13개의 최신 행만 반환합니다(중복이 있으면 39개 반환). 단, 쿼리 시간이 크게 늘어날 수 있습니다.
차원 테이블(user_dim 등)이 ReplacingMergeTree로 관리된다면, fact 테이블과 조인할 때도 FINAL을 붙여 최신 정보만 조인합니다.
SELECT e.event_id, d.user_name, d.age, e.event_time
FROM events AS e
JOIN user_dim FINAL AS d
ON e.user_id = d.user_id
WHERE e.event_date = '2025-09-01';
이렇게 하면 user_dim에 오래된 기록이 남아 있어도, 이벤트가 여러 행과 중복 조인되는 문제를 방지할 수 있습니다.
항상 FINAL 결과를 제공하는 뷰를 만들어 SELECT * FROM hits_latest ... 형태로 활용할 수 있습니다.
CREATE VIEW hits_latest AS SELECT * FROM hits_rmt FINAL;
ClickHouse 23.2부터는 SET final = 1;로 모든 쿼리에 FINAL이 자동 적용되도록 설정할 수 있습니다. 쿼리 작성이 간결해지고, 조인·서브쿼리 등에도 일관성 있게 FINAL이 적용됩니다.
3. FINAL 키워드의 성능 오버헤드
FINAL은 정확한 결과를 보장하는 대신, 상당한 성능 오버헤드를 유발합니다. 그 이유는 쿼리 시점에 각 파트의 데이터를 모두 읽어와 병합 및 필터링을 수행해야 하므로, CPU, 메모리, I/O 부하가 크게 증가하기 때문입니다.
- 성능 비교 사례
- 인덱스 활용 저하
- 최신 버전의 성능 최적화
Altinity의 테스트에 따르면, 동일 집계 쿼리에서 FINAL을 사용하면 쿼리 시간이 0.9초에서 11초로 약 12배 느려진 사례가 있습니다. 또 다른 사례에서는 1.5초 걸리던 쿼리가 FINAL 적용 시 100배 가까이 느려지기도 했습니다. 테이블 크기와 중복 상태에 따라 성능 저하는 수 배에서 수십 배까지 다양하게 나타납니다.
FINAL은 파트 전체를 읽어야 하므로, PREWHERE 등 인덱스 기반 최적화의 효과가 크게 줄어듭니다. 예를 들어, 정렬키가 아닌 컬럼으로 WHERE 조건을 줄 경우, PREWHERE 최적화가 비활성화되어 전체 스캔이 발생합니다. 이로 인해 0.4초 걸리던 쿼리가 FINAL 적용 시 9초까지 늘어나는 사례도 있습니다.
ClickHouse 22.6 이후 FINAL 병합 과정은 멀티스레드로 처리되어 과거보다 빨라졌고, 24.1에서는 수직 분할(vertical algorithm) 방식으로 CPU 캐시 효율이 향상되었습니다. do_not_merge_across_partitions_select_final=1 옵션을 켜면 파티션별 병합을 분리해 쿼리 시간을 7배 이상 단축하는 효과도 있습니다. 25.6부터는 Additional Skip Index도 FINAL에서 사용할 수 있어 필터링 성능이 개선되었습니다[2].
하지만 이 모든 최적화를 적용해도, FINAL 쿼리는 일반 쿼리 대비 항상 더 많은 비용이 듭니다. 대용량 테이블이나 실시간성이 중요한 환경에서는 FINAL 사용을 최소화하는 것이 좋습니다.
4. FINAL 키워드 우회 및 대체 전략
성능 오버헤드로 인해 실무에서는 FINAL 사용을 최소화하거나, 대체하는 다양한 전략이 활용됩니다.
(a) TTL 및 자동 머지 정책
테이블 설정에서 min_age_to_force_merge_seconds 등으로 일정 기간이 지난 파티션에 대해 강제 머지를 수행하면, 백그라운드에서 주기적으로 완전 병합이 이루어져 FINAL 없이도 중복 없는 데이터를 조회할 수 있습니다. TTL(Delete) 기능으로 만료 데이터 삭제 시 파트 병합이 트리거되어 병합이 가속될 수도 있습니다.
(b) 파티션 키 설계 최적화
중복 데이터가 파티션을 넘지 않도록 파티션 키를 설계하면 FINAL 비용이 크게 줄어듭니다. 예를 들어 일별 파티션을 만들고, 업데이트가 같은 일자 안에서만 발생하도록 설계하면, 파티션 단위로 OPTIMIZE ... PARTITION ... FINAL을 주기적으로 실행해 FINAL 없이도 완결된 조회가 가능합니다.
(c) 쿼리 레벨 중복 제거(버전 컬럼 활용)
ReplacingMergeTree에서 버전(version) 컬럼(예: updated_time)을 도입하면, 서브쿼리나 argMax 함수, LIMIT N BY 절 등을 활용해 쿼리 차원에서 최신 행만 선별할 수 있습니다.
이 방법들은 FINAL 없이도 각 키별 최신 행만 빠르게 조회할 수 있으며, 실제로 FINAL 쿼리보다 수배~수십배 빠른 성능을 보입니다[3].
(d) 머티리얼라이즈드 뷰(Materialized View) 활용
데이터 삽입 시점에 중복을 제거해 별도 테이블에 저장하는 Materialized View를 활용하면, 조회 시 FINAL이 필요 없어집니다. AggregatingMergeTree 엔진과 argMax 집계를 조합하거나, LIVE VIEW로 FINAL 결과를 캐시해두는 패턴도 있습니다. 단, MV 기반 방식은 과거 데이터 변경에 대한 대응이 어렵다는 한계가 있습니다.
(e) 데이터 설계 단계의 중복 회피
가장 근본적인 방법은 데이터 파이프라인에서 중복이 발생하지 않도록 설계하는 것입니다. 최근에는 ClickHouse에 Unique Key 제약 도입이 논의되고 있으며, 고유 키가 보장되는 테이블은 애초에 중복 데이터가 없으므로 FINAL이 필요 없어집니다[4][5].
5. FINAL 키워드의 최신 개선 동향 및 로드맵(2024~2025년)
(1) 성능 최적화
- 20.5: FINAL 쿼리의 멀티스레드 처리 도입
- 22.6: 멀티스레드 병합 성능 강화
- 22.8: 불필요한 데이터 읽기 최소화
- 23.5: 메모리 사용량 감소
- 23.9: 파티션 내 파트 1개일 때 불필요한 PK 컬럼 읽기 생략
- 23.12: 교차 영역만 FINAL 적용(불필요한 병합 최소화)
- 24.1: 동일 파트 내 level>0 행 비교 생략, vertical algorithm 도입(캐시 효율 향상)
- 25.6: Additional Skip Indexes 지원으로 필터링 성능 개선
이러한 최적화로 FINAL의 오버헤드는 과거 대비 크게 줄었으나, 여전히 일반 MergeTree 쿼리만큼 빠르지는 않습니다[2].
(2) 쿼리 편의성 및 기능 개선
23.2부터 세션/프로필 단위 final=1 옵션 지원, JOIN/서브쿼리에도 FINAL 자동 적용 등 쿼리 편의성이 향상되었습니다.
(3) 기능적 대체 방안
Unique Key 제약 도입(RFC 진행 중), Materialized View 자동 갱신 기능, 물리적 Update/Delete 성능 향상 등 FINAL의 필요성을 줄이기 위한 기능이 활발히 논의되고 있습니다.
(4) 남은 과제
PREWHERE 최적화(정렬키 외 컬럼 필터 시 성능 저하), Distributed 환경에서의 FINAL 적용, CollapsingMergeTree의 FINAL 정확성 등 세부 개선거리도 남아 있습니다.
6. 결론 및 실무 권장사항
FINAL 키워드는 MergeTree 아키텍처의 eventual consistency 문제를 해결하기 위한 핵심 도구이지만, 성능 오버헤드와 편의성 이슈가 상존합니다. 다행히 최근 버전에서 성능과 사용성이 크게 개선되고 있고, 앞으로 Unique Key 등 근본적 해결책이 도입될 전망입니다.
실무에서는 다음과 같은 전략이 권장됩니다.
- FINAL 사용을 최소화하고, 파티션 설계, 버전 컬럼, argMax, LIMIT BY, Materialized View 등 다양한 우회 전략을 병용할 것
- FINAL이 꼭 필요한 경우 최신 ClickHouse 버전의 최적화 기능 및 설정(do_not_merge_across_partitions_select_final 등)을 적극 활용할 것
- 데이터 파이프라인 및 테이블 설계 단계부터 중복 발생을 최대한 억제할 것
이러한 접근을 통해, ClickHouse의 장점을 최대한 살리면서도 FINAL의 부작용을 최소화할 수 있습니다.
참고: 본 연구는 ClickHouse 공식 문서, Altinity 블로그, Datazip, StackOverflow, 최신 커밋/릴리즈 노트 등 다양한 실무 사례와 2024~2025년 최신 동향을 종합적으로 분석하여 작성되었습니다[7][8][9][10][11].