
【ClickHouse vs DuckDB】 로컬 Agent 분석 기반을 실측으로 선택하기 — 결정적 요인은 속도가 아니었다
요약
로컬 AI 에이전트의 데이터 분석 환경 구축을 위해 ClickHouse와 DuckDB를 비교 검증한 기록입니다. 비용 절감을 위해 클라우드 웨어하우스 대신 로컬 엔진을 활용하는 전략을 제안하며, 성능보다 락 모델(Lock Model)이 선택의 핵심 요인임을 밝힙니다.
핵심 포인트
- 클라우드 DW 비용 절감을 위해 분석 작업을 로컬로 분리하는 아키텍처 제안
- AI 에이전트의 도메인 지식 축적을 위한 풀 스캔 기반 데이터 분석 필요성
- ClickHouse와 DuckDB 비교 결과, 속도보다 락 모델이 결정적 차이로 작용
- Iceberg를 활용해 Snowflake 데이터를 로컬로 가져와 분석하는 구성 검토
서론
이 기사는 로컬 AI 에이전트용 데이터 분석 환경으로서 ClickHouse를 사용할 수 있을지를 검증한 기록을 정리한 것입니다. 비교 대상으로 DuckDB도 동일한 조건에서 측정했습니다.
결론부터 말씀드리면, 이 용도에서는 DuckDB를 선택한다는 판단을 내렸습니다. 결정적인 요인은 속도가 아니라, 「락 모델 (Lock Model)」이라는 미묘한 차이입니다.
검증 과정에서는 측정 방법 자체를 틀렸거나, 세운 가설이 실측 결과에 의해 부정되기도 했습니다. 그런 부분들도 숨기지 않고 작성하겠습니다.
대상 독자
・Snowflake 등 클라우드 데이터 웨어하우스 (Cloud Data Warehouse)의 쿼리 비용으로 고민하고 계신 분
・AI 에이전트에게 데이터 분석을 수행하게 할 기반을 검토하고 계신 분
・ClickHouse와 DuckDB의 차이를 실측 기반으로 알고 싶은 분
・열 지향 데이터베이스 (Columnar Database)의 구조에 관심이 있는 분
검증 배경
하고 싶었던 것
제가 실현하고자 했던 것은 크게 다음 두 가지입니다.
첫 번째는 상세 수준에서의 데이터 품질 풀 체크 (Full Check)입니다.
샘플링한 수천 행을 훑어보고 "대충 괜찮아 보이네"라고 판단하는 것이 아니라, 모든 행을 대상으로 결측치 · 이상치 · 분포의 편향 · 컬럼 간의 모순을 찾아내고 싶다는 목적입니다. 당연히 풀 스캔 (Full Scan)이 전제가 됩니다.
두 번째는 에이전트 (Agent)에게 부여할 지식(Knowledge)을 단일 테이블로부터 사전에 축적해 두는 것입니다.
이쪽이 더 중요했습니다. 예를 들어 채용 정보 데이터를 다루는 경우, "이 직종은 이 지역에서 시세가 이 정도다", "이 시기에 이 카테고리의 채용이 늘어난다"와 같은 경향은 완전한 도메인 지식 (Domain Knowledge)입니다. LLM은 일반적인 지식은 가지고 있지만, 이러한 지역적 특성이나 자사 데이터 고유의 경향은 당연히 알지 못합니다.
그래서 LLM이 미리 다각적인 관점에서 분석을 수행하게 하고, 그 결과를 지식으로서 축적해 두고 싶다고 생각했습니다. 에이전트가 실제 태스크에 임할 때, 이 지식이 있느냐 없느냐에 따라 답변의 질이 크게 달라집니다.
왜 로컬에서 분석하는가
문제는 비용입니다.
위와 같은 「다각적인 분석」은 하나의 질문에 대해 수십 번 쿼리를 다시 던지는 탐색적인 작업이 됩니다. 지역별, 직종별, 시기별, 그리고 이들의 조합 등 관점을 바꿔가며 반복적으로 집계하게 됩니다.
이를 Snowflake 상에서 실행하면, 그 횟수만큼 웨어하우스 (Warehouse)가 기동되어 크레딧을 소비합니다. 에이전트는 인간과 달리 시도 횟수를 예측할 수 없기 때문에 비용을 가늠하기 어렵습니다.
그래서 생각한 것이 분석 실행만 로컬로 분리하는 구성입니다.
Snowflake와 같은 클라우드 레이크하우스 (Cloud Lakehouse)를 사용하는 것 자체는 전제로 합니다. 데이터 본체는 Snowflake에 둔 채로, 거기서 필요한 범위만 Iceberg를 통해 로컬로 가져와서, 풀 스캔이나 다각적인 집계와 같은 「횟수를 던지는 작업」만 로컬 엔진이 담당하게 한다는 아이디어입니다.
한번 데이터를 내려받으면 데이터 취득 1회의 비용으로 끝나며, 이후에는 몇 번의 쿼리를 던져도 웨어하우스의 크레딧을 소비하지 않습니다.
검증을 통해 답을 내고 싶었던 것
이 구성을 전제로 판단하고 싶었던 것은 다음 두 가지입니다.
- 로컬 측 엔진으로서 ClickHouse와 DuckDB 중 어느 쪽이 적합한가
- 애초에 분석을 로컬로 분리한다는 방침 자체가 타당한가
환경과 전제
| 항목 | 내용 |
|---|---|
| 하드웨어 | Mac (Apple Silicon, 10코어) |
| ... |
검증에 사용한 데이터
이벤트 로그를 모사한 테이블을 준비하여, 양쪽 엔진에 동일한 데이터를 넣고 비교했습니다. 컬럼은 일시 · 사용자 ID · 이벤트 유형 · 국가 · 매출의 5개이며, 행 수는 검증 내용에 따라 500만 행에서 1억 행까지 조절했습니다. ClickHouse 측의 정렬 키 (Sort Key)는 이벤트 유형 · 사용자 ID · 일시 순으로 설정했습니다.
값은 균등하지 않고, 실제 데이터에 가까운 편향을 갖도록 했습니다.
| 항목 | 부여한 편향 |
|---|---|
| 이벤트 유형 | view 60% / click 25% / signup 8% / purchase 7% 의 퍼널 (Funnel) 구조 |
| ... |
균등 분포 데이터라면 어떤 관점으로 집계해도 동일한 건수가 반환되므로, 집계 결과가 올바른지 판단할 수 없습니다. 분포 설계는 측정의 타당성과 직결되므로, 이 부분은 소홀히 하지 않는 것이 좋다고 생각합니다.
데이터 생성에 사용한 SQL
CREATE TABLE IF NOT EXISTS playground.events
(
event_time DateTime,
...
행 수를 변경하여 측정할 경우에는 numbers()의 인자를 변경합니다. 1억 행 측정도 이 방법으로 생성했습니다.
MergeTree — ClickHouse의 핵심을 이해하기
ClickHouse의 성능은 거의 MergeTree라는 스토리지 엔진(Storage Engine)에서 비롯됩니다. MySQL의 InnoDB에 해당한다고 볼 수 있습니다. 이 부분을 이해해 두면 후반부의 측정 결과가 납득될 것입니다.
테이블은 「파트(Part)」의 집합체
system.parts를 보면 테이블이 여러 개의 파편으로 구성되어 있음을 알 수 있습니다.
all_1_1_0 1,111,953행 6.18 MiB level 0
all_2_2_0 1,111,953행 6.97 MiB level 0
all_3_3_0 1,067,332행 7.09 MiB level 0
메커니즘은 다음과 같습니다.
- INSERT가 발생할 때마다 쓰기 불가능(Immutable)한 파트가 새로 생성됩니다 (기존 데이터에는 전혀 손대지 않습니다).
- 백그라운드에서 작은 파트를 큰 파트로 병합(Merge)해 나갑니다 (level이 올라갑니다).
이 "병합해 나가는 트리 구조"가 MergeTree라는 이름의 유래입니다. 쓰기가 항상 순차적(Sequential)으로 이루어지기 때문에 매우 빠르며, 실측 결과 1억 행을 19.7초(약 500만 행/초) 만에 투입할 수 있었습니다.
효과 1: 압축이 매우 잘 됨
파트 내부는 ORDER BY 키로 정렬된 열 지향(Columnar) 방식으로 저장됩니다. 같은 값이 나열되므로 압축 효율이 높습니다.
| 컬럼 | 타입 | 압축 후 | 압축 전 | 압축률 |
|---|---|---|---|---|
| event_type | LowCardinality(String) | 3.65 KiB | 754 KiB | 약 200배 |
| ... |
주목할 점은 event_time이 전혀 줄어들지 않았다는 점입니다. 이는 ORDER BY (event_type, user_id, event_time)의 세 번째 키이기 때문에, 실질적으로 정렬이 깨져 있어 델타 압축(Delta Compression, 이전 값과의 차분을 저장하는 방식)이 작동하지 않기 때문입니다.
타임스탬프(Timestamp) 중심의 쿼리를 던진다면 키 순서를 재검토할 필요가 있다는 뜻입니다. 혹은 키 순서를 바꾸지 않고 컬럼 정의에서 CODEC(DoubleDelta, ZSTD)와 같이 압축 방식을 명시적으로 지정하는 방법도 있습니다. 시계열 데이터의 경우 이 방법이 더 간편할 수 있습니다.
효과 2: 희소 기본 키 인덱스 (Sparse Primary Key Index)
500만 행에 대해 인덱스는 617 마크(Mark) / 3.34 KiB뿐이었습니다. 8,192행마다 1개의 마크만 가지는 방식입니다.
B-tree처럼 모든 행을 색인하지 않고, 항상 메모리에 올라갈 수 있는 크기로 유지하도록 설계되어 있습니다.
효과 3: 키로 필터링하면 읽는 양이 급감함
동일한 500만 행 테이블에 대해 필터링 조건을 바꾸어 실제로 읽은 양을 비교했습니다.
| 필터링 조건 | 실제로 읽은 행 수 | 읽은 바이트 수 |
|---|---|---|
WHERE event_type='purchase' (키의 선두) | 81,920행 | 80 KiB |
WHERE country='JP' (키에 없음) | 5,000,000행 | 4.77 MiB |
61배의 차이입니다. ORDER BY 설계가 곧 성능 설계가 됩니다.
그리고 이것은 후술할 "ClickHouse가 Iceberg를 직접 읽을 때 느린 이유"와도 직결됩니다. Parquet를 직접 읽는 경우, 이 희소 기본 키 인덱스에 의한 스킵(Skip)은 작동하지 않습니다.
참고로 ReplacingMergeTree(병합 시 중복 제거)나 SummingMergeTree(병합 시 자동 집계)와 같은 파생 엔진들도 있지만, 모두 이 동일한 메커니즘 위에서 동작합니다.
검증 1: 애초에 "빠른" 것인가
데이터가 너무 작으면 측정이 되지 않음
처음에는 85만 행으로 측정하여 0.055초라는 수치를 얻었습니다. 하지만 이는 ClickHouse의 성능을 전혀 나타내지 못했습니다.
| 대상 | 시간 |
|---|---|
빈 쿼리 SELECT 1 | 0.001s |
| 85만 행의 GROUP BY | 0.009s |
85만 행에서는 시간의 대부분이 연결(Connection)과 파싱(Parsing)의 오버헤드입니다. 측정된 것은 ClickHouse의 성능이 아니라 통신의 오버헤드였습니다.
1억 행에서의 실측
데이터량을 1억 행으로 늘려 다시 측정했습니다.
| 쿼리 | 시간 | 처리량 (Throughput) |
|---|---|---|
count() 전체 | 0.010s | 메타데이터만 읽고 행을 읽지 않음 |
GROUP BY event_type (1개 컬럼) | 0.116s | 약 8.6억 행/초 |
GROUP BY country, event_type + 조건 집계 | 0.379s | 약 2.6억 행/초 |
GROUP BY user_id (500만 그룹) | 2.505s | 약 4,000만 행/초 |
uniqExact(user_id) | 0.540s | |
| 일간 트렌드 365일분 | 0.130s | |
WHERE event_type='purchase' | 0.045s | 기본 키(Primary Key) 앞부분에서 스킵 |
여기서 중요한 통찰은 "1억 행이니까 몇 초"라는 사고방식이 통하지 않는다는 점입니다. 처리 시간은 그룹 수와 읽는 컬럼 수에 의해 결정됩니다.
그룹 수가 적을 때는 초당 수억 행의 속도로 처리되지만, 500만 그룹의 해시 테이블 (Hash Table)을 구축하는 단계에 이르면 속도가 20배 이상 느려집니다.
타 엔진과의 비교
1,000만 행에 대해 각 쿼리를 3회씩 실행한 최속값으로 비교했습니다.
| engine | 로드 (Load) | 1개 컬럼 GROUP BY | 2개 컬럼 GROUP BY | 500만 그룹 |
|---|---|---|---|---|
| ClickHouse | 0.62s | 0.015s | 0.042s | 0.096s |
| ... |
알아낸 점은 다음 세 가지입니다.
- 행 지향 RDBMS (SQLite)에 대해서는 60~130배 빠릅니다. 분석 쿼리에서 열 지향 (Columnar) 방식을 선택하는 이유는 바로 여기에 있습니다.
- pandas에 대해 약 10배 빠릅니다.
- DuckDB에 대해서는 거의 대등하며, 조건에 따라서는 DuckDB가 더 빠릅니다.
즉, 노트북 상에서 1개의 프로세스가 수천만 행을 분석하는 한, ClickHouse는 속도 면에서 우위를 점하지 못했습니다.
측정에 사용한 코드의 골격
4개의 엔진에 동일한 내용의 집계를 던지고, 3회 실행한 최속값을 취했습니다. 로드 시간은 별도로 측정했습니다.
import time
import sqlite3
import clickhouse_connect
...
pandas는 groupby().agg()를, SQLite는 동일한 SQL을 sqlite3를 통해 실행하여 비교했습니다.
검증 2: 동시 실행이라면 이길 수 있을 것이라는 가설
여기서 저는 다음과 같은 가설을 세웠습니다.
"ClickHouse는 서버형이므로, 동시 실행 수가 늘어나면 반드시 이길 것이다."
이는 실측을 통해 깔끔하게 부정되었습니다.
풀 스캔 집계 (1,000만 행)
| engine | 클라이언트 수 | QPS | mean | p95 |
|---|---|---|---|---|
| ClickHouse | 1 | 22.8 | 44ms | 51ms |
| ... |
양쪽 모두 클라이언트를 16배로 늘려도 QPS는 1.3배밖에 늘어나지 않았습니다.
이유는 단순합니다. 쿼리 1개가 이미 10개의 코어를 다 사용하고 있기 때문입니다. 동시 실행을 해도 대기열(Wait Queue)만 늘어날 뿐이었습니다. CPU 바운드 (CPU-bound)인 풀 스캔에서는 동시 실행 수가 성능을 높여주지 않습니다.
필터링 쿼리 (실제 대시보드에 가까운 형태)
1개의 쿼리가 가벼운 경우라면 어떨지도 시도해 보았습니다.
| engine | 클라이언트 수 | QPS | mean |
|---|---|---|---|
| ClickHouse | 1 | 222.8 | 4.5ms |
| ... |
이 경우는 1개의 쿼리가 코어를 모두 점유하지 않기 때문에 병렬화할 여지가 생겼고, 양쪽 모두 4~5배로 스케일링(Scaling)되었습니다. 하지만 여기서도 DuckDB가 3배 더 빠르다는 결과가 나왔습니다.
차이의 주된 원인은 프로토콜이었다
왜 이렇게까지 차이가 나는지 조사하기 위해, 빈 쿼리의 왕복 시간 (Round-trip time)을 측정했습니다.
| 빈 쿼리 왕복 |
|---|---|
| ClickHouse HTTP | 0.95 ms |
| DuckDB 프로세스 내 호출 (In-process call) | 0.054 ms |
ClickHouse는 쿼리 1개마다 HTTP 연결, 파싱(Parsing), 직렬화(Serialization) 과정을 거칩니다. 반면 DuckDB는 동일 프로세스 내의 함수 호출이며, Arrow 버퍼를 직접 다룹니다.
단일 머신에서는 인프로세스 (In-process) 엔진을 구조적으로 이길 수 없습니다. 이는 구현의 우열 문제가 아니라 아키텍처의 차이입니다.
검증 3: 진짜 차이는 락 모델 (Lock Model)에 있었다
여기서부터가 이번의 가장 큰 발견입니다. 여러 개의 Agent 프로세스를 상정하여, 4개의 프로세스가 동시에 동일한 데이터에 접속하는 상황을 4가지 패턴으로 테스트했습니다. 결과는 다음과 같습니다.
| # | 시나리오 | DuckDB | ClickHouse |
|---|---|---|---|
| A | 4개 프로세스가 read_only로 동시에 읽기 (쓰기 프로세스 없음) | 4/4 성공 | 성공 |
| B | 쓰기 프로세스가 1명 있는 상태에서, 다른 프로세스가 읽기 전용으로 접속 | 실패 Could not set lock on file | 성공 |
| B' | 쓰기 프로세스가 1명 있는 상태에서, 다른 프로세스가 쓰기 모드로 접속 | 실패 | 성공 |
| C | 4개 프로세스가 각자 중간 결과 테이블을 작성 | 1/4만 성공 | 4/4 성공 |
시나리오 B가 충격적이었습니다. 쓰기 프로세스가 단 한 명이라도 있으면, 읽기만 하는 프로세스조차 접속할 수 없습니다.
시나리오 B를 재현하는 최소 코드
쓰기 모드로 접속한 상태를 유지하는 프로세스를 하나 띄워두고, 그 상태에서 다른 프로세스로부터 읽기 전용으로 접속합니다. 데이터베이스 파일에는 미리 분석 대상 테이블 t가 들어있다는 전제입니다.
import subprocess
import sys
import textwrap
...
실행하면, 읽기 전용임에도 불구하고 다음과 같은 에러와 함께 접속에 실패합니다.
duckdb.duckdb.IOException: IO Error: Could not set lock on file "lake/events.duckdb": ...
returncode: 1
동일한 조작을 ClickHouse에 대해서 수행해도, 당연하게도 실패하지 않습니다.
여기서 도출되는 설계 원칙은 다음과 같습니다.
- DuckDB는 "1개 파일당 쓰기 프로세스는 1개뿐"이라는 제약에서 벗어날 수 없다
- 단, 쓰기 프로세스가 0이라면 여러 프로세스에서 동시에 읽을 수 있다 (시나리오 A)
- 따라서 Agent가 정적인 스냅샷을 읽기만 하는 구성이라면, DuckDB로도 전혀 문제없다
- Agent가 중간 결과를 저장하고 싶다면, 공유 DB에 쓰게 하지 말고 Agent마다 개별적인 DuckDB 파일을 갖게 한다
두 가지를 나누는 본질은 여기에 있습니다.
- DuckDB는 라이브러리(Library)이다. 프로세스에 임베딩(Embedding)되므로 오버헤드가 거의 없다. 하지만 1개 프로세스가 1개 파일을 점유하는 모델에서 벗어날 수 없다
- ClickHouse는 서버(Server)이다. 1ms 정도의 통신 비용을 항상 지불하는 대신, 여러 프로세스가 동시에 읽고 쓸 수 있다
제품 선정의 축은 성능이 아니라 액세스 패턴(Access Pattern)이었다는 것이 이번의 가장 큰 배움입니다.
검증 4: Iceberg 연동
본래의 목적인 "Snowflake의 데이터를 로컬로 가져오는" 경로를 검증합니다.
ClickHouse 25.6의 Iceberg 대응 현황
실기기에서 확인한 결과입니다.
| 항목 | 상황 |
|---|---|
| 읽기 | 정식 지원 (iceberg / icebergS3 / icebergAzure / icebergHDFS / icebergLocal) |
| ... |
쓰기 불가라는 점은 설계상 중요합니다. ClickHouse는 Snowflake 레이크하우스(Lakehouse)에 대해 읽기 전용의 별도 엔진 역할밖에 할 수 없습니다.
Iceberg 직접 읽기 vs MergeTree
동일한 500만 행을 Iceberg 경유와 MergeTree 경유로 비교하여 읽어보았습니다. 네트워크가 없는 로컬 디스크라는 최상의 조건입니다.
| 쿼리 | Iceberg 경유 | native MergeTree | 차이 |
|---|---|---|---|
count() | 0.002s | 0.001s | 동일 (메타데이터에서 취득) |
| GROUP BY 1개 열 | 0.058s | 0.014s | 4.1배 느림 |
| GROUP BY 2개 열 | 0.100s | 0.050s | 2.0배 느림 |
WHERE event_type='purchase' | 0.021s | 0.004s | 5.3배 느림 |
sum(revenue)만 수행 | 0.007s | 0.004s | 1.8배 느림 |
필터링에서 5배의 차이가 발생하는 이유는, 앞서 언급한 MergeTree의 희소(Sparse) 기본 키 인덱스에 의한 스킵(Skip)이 작동하지 않고, Iceberg의 파티션/통계 프루닝(Pruning)까지만 사용할 수 있기 때문입니다.
그리고 이것은 최상의 조건에서의 수치이므로, 실제 S3 읽기에서는 오브젝트 스토리지(Object Storage)의 레이턴시(Latency)가 더해져 격차는 더욱 벌어집니다.
DuckDB에서의 Iceberg 읽기
DuckDB 측은 iceberg 확장을 설치하면 iceberg_scan()을 통해 테이블 디렉토리를 직접 지정하여 읽을 수 있습니다. 카탈로그를 구축하지 않은 검증 환경에서는 unsafe_enable_version_guessing 설정을 활성화하여 최신 스냅샷을 추측하도록 했으나, 운영 환경에서 Snowflake의 Iceberg REST 카탈로그에 접속할 경우에는 이 설정이 필요하지 않습니다.
결정적이었던 측정
동일한 Iceberg 테이블 500만 행에 대해, 동일한 집계를 4가지 경로로 실행했습니다. 이것이 최종적인 판단 근거가 되었습니다.
| engine | 읽기 방식 | 시간 |
|---|---|---|
| DuckDB | Iceberg 직접 읽기 | 21.2 ms |
| ... | ... | ... |
DuckDB는 Iceberg를 직접 읽어도 데이터 로드(Ingestion) 후와 거의 동일한 속도를 보입니다 (21ms vs 17ms). 즉, 데이터 로드 과정 자체가 불필요해집니다.
반면 ClickHouse는 직접 읽기가 느리며, 속도를 내기 위해서는 MergeTree로의 데이터 로드 파이프라인이 필수적입니다. 로컬 Agent 분석을 위해 매번 ETL을 구축하는 것은 명백히 효율이 떨어집니다.
이 측정에 사용한 코드
import time
from pathlib import Path
import clickhouse_connect
...
고찰: 선택의 판단 기준
검증을 통해 판단을 가르는 기준은 "쓰기 작업(Writer)이 존재하는가"로 수렴된다는 것을 정리할 수 있었습니다.
패턴 1: 읽기 전용 병렬 분석
데이터는 사전에 준비되어 있으며, 에이전트(Agent)는 서로 다른 관점으로 SELECT 쿼리를 던지기만 함.
→ DuckDB로 충분하며, 오히려 최적임 (시나리오 A에서 입증됨)
패턴 2: 쓰기 작업이 있음 / 데이터가 계속 업데이트됨
중간 집계 결과를 테이블에 저장하거나, 별도의 프로세스가 이를 읽거나, 수집 프로세스가 계속해서 추가(Append)함.
→ DuckDB는 즉시 한계에 부딪힘 (시나리오 B, C). 서버형 모델이 필요함
비즈니스 맥락에서의 보충
당초 저는 "패턴 2는 비즈니스 현장에서 드물지 않을까"라고 생각했지만, 이는 반대였습니다.
실무에서 쓰기 작업의 주체는 Agent가 아니라 파이프라인(Pipeline)입니다.
- 야간 ETL/ELT가 테이블을 업데이트하는 동안에도 BI 대시보드는 열려 있음
- 로그 및 이벤트가 지속적으로 유입됨
- dbt가 중간 테이블을 다시 만들고, 다른 작업(Job)이 이를 읽음
즉, 패턴 2는 비즈니스의 기본 상태입니다. 다만 이는 이미 "서버형 DWH의 영역"으로서 해결되어 있기 때문에 평소에는 의식되지 않을 뿐이었습니다.
그리고 이번 로컬 Agent 분석은 패턴 1에 해당합니다. 이것이 결론의 근거입니다.
고찰: 그렇다면 ClickHouse는 언제 사용하는가
"ClickHouse를 사용하려면 Snowflake를 중단해야 하는가"라는 의문도 있었지만, 답은 No이며, 병용하는 것이 표준 모델입니다.
특화된 쿼리 형태가 다름
| Snowflake | ClickHouse |
|---|---|
| 예상 | 대규모 쿼리를, 가끔 |
| ... | ... |
ClickHouse가 실제로 효과적인 3가지 유형
- 프로덕트 임베디드 고객용 분석 — 모든 사용자가 자신의 대시보드를 열며, 초당 수백 개의 쿼리와 서브 세컨드(Sub-second) 응답이 필요함
- 옵저버빌리티 (Observability) — 로그/메트릭/트레이스. 초당 수십만 행이 유입되는 동시에 검색됨
- 실시간 대시보드 — 몇 초 전의 데이터가 보이지 않으면 의미가 없는 용도
공통점은 "쿼리 횟수가 많고, 레이턴시에 엄격하며, 데이터가 계속 흐르는" 형태라는 점입니다.
일반적인 구성은 Snowflake를 중단하지 않고 서빙 레이어(Serving Layer)로 추가하는 형태가 됩니다.
소스 → Snowflake (DWH: 변환·이력·거버넌스)
↓ 일부 데이터만
ClickHouse (서빙: 고빈도·저레이턴시)
...
최종 구성
이번 검증을 바탕으로 한 권장 구성은 다음과 같습니다.
Snowflake (Iceberg tables, external volume on S3)
↓ 필요한 범위만 S3에서 직접 가져옴 (Snowflake 컴퓨팅 불필요 = 크레딧 소비 없음)
로컬: Iceberg 또는 Parquet 파일
...
DuckDB를 선택한 이유는 4가지입니다.
- Iceberg 직접 읽기가 빠름 — ClickHouse의 4배, 게다가 데이터 수집(Ingestion) 공정이 불필요
- Agent 구성과 궁합이 좋음 — read_only라면 여러 프로세스의 동시 접속이 성립함 (실증 완료)
- 운영이 가벼움 —
import duckdb
를 하는 것뿐. 서버도 Docker도 포트도 불필요 - 가벼운 쿼리를 다수 던지는 경우, 프로세스 내 호출(In-process call)인 만큼 왕복 속도가 빠름 (필터링 쿼리의 QPS에서 약 3배 차이)
참고로 4번째 점에 대해서는, 무거운 집계 쿼리 단일 건으로는 오히려 막상막하였으며, 1개 컬럼의 GROUP BY에서는 ClickHouse가 더 빠른 경우도 있었습니다. 어디까지나 "가벼운 쿼리를 반복해서 던지는" 패턴에서의 차이입니다.
로컬 분석이 "유망한" 이유
서두에 작성한 비용 측면의 메리트는 검증을 마친 지금도 변함없이 성립한다고 생각합니다. 시도 횟수를 예측할 수 없는 Agent 워크로드(Workload)에서는, 1회 취득 비용만으로 이후의 쿼리가 크레딧을 소비하지 않게 되는 차이는 매우 큽니다.
여기에 더해, 검증 중에 깨달은 점이 하나 더 있습니다. Iceberg 형식으로 S3에 놓여 있다면, Snowflake의 컴퓨팅(Compute)을 전혀 가동하지 않고 데이터를 가져올 수 있습니다. 이것이 "Iceberg 형식으로 만드는 의미"의 본질입니다. 로컬 엔진의 성능이 아니라, 과금 경로를 회피하는 것에야말로 의의가 있습니다.
유망하지 않게 되는 4가지 조건
그렇기는 하지만, 아래에 해당한다면 솔직하게 DWH(Data Warehouse)에서 분석하는 편이 좋습니다.
| 조건 | 내용 |
|---|---|
| 데이터 양 | 수백 GB ~ TB급을 통째로 로컬에서 처리하는 것은 무리. 날짜나 사업부로 파티션(Partition)을 나누고, 필요한 범위만 내려받는 설계가 전제되어야 함 |
| ... |
특히 거버넌스(Governance)는 기술로 해결할 수 없는 부분이므로, 검토 초기에 확인해 두는 것을 추천합니다.
요약
이번에는 ClickHouse와 DuckDB를 실측으로 비교하여, 로컬 Agent 분석 기반으로서 DuckDB를 선택하기까지의 과정을 정리했습니다.
직접 해보며 가장 의외였던 점은, 선정의 결정타가 속도가 아니었다는 것입니다. 양측의 속도 차이는 워크로드에 따라 뒤바뀔 정도였으며, 마지막에 결정적인 영향을 미친 것은 "쓰기 작업자(Writer)가 있으면 DuckDB는 읽기조차 할 수 없다"라는 락 모델(Lock model)의 차이였습니다.
또 하나 꼽자면, 측정 방식에 따라 결론이 달라진다는 점입니다. 데이터가 너무 작으면 측정하고 있는 것은 통신 오버헤드이며, "서버형이기 때문에 동시 실행에서 승리한다"라는 가설도 실제로 측정해 보니 성립하지 않았습니다.
ClickHouse가 나빴던 것이 아니라, 단지 이번 용도에 맞지 않았을 뿐입니다. 제품 임베디드 형태의 고객용 분석이나 옵저버빌리티(Observability)와 같은 용도가 나온다면, 다시 한번 후보에 넣고 싶습니다.
마찬가지로 Agent에게 데이터를 전달하는 기반을 검토하고 계신 분들은, 우선 "쓰기 작업자가 있는지 여부"를 확인해 보시기 바랍니다. 거기서 선택지를 상당히 좁힐 수 있을 것입니다.
제 X(구 Twitter)에서는 LLM에 국한되지 않고, AI를 활용한 업무 개선 정보를 발신하고 있으니 관심 있는 분들은 꼭 팔로우 부탁드립니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기