LLM WikiAccess-protected knowledge portal
← 스터디 홈
6편 · 약 24분

DuckDB와 로컬 OLAP: 단일 프로세스 분석 엔진의 활용

분산 엔진 없이 분석할 수 없을까

Trino나 Spark는 클러스터를 구성하고, 노드 간 네트워크 직렬화를 거치며, 코디네이터·워커 모두에 JVM을 올려야 한다. 수십 GB짜리 Parquet 파일을 로컬에서 탐색하거나, 개발 환경에서 dbt 모델을 빠르게 테스트하거나, 온콜 엔지니어가 노트북에서 즉시 데이터를 분석해야 하는 상황에는 이런 부담이 장벽이 된다.

DuckDB는 이 틈새를 공략한다. 단일 프로세스로 실행되는 인-프로세스 OLAP 데이터베이스로, 서버 없이 Python·Go·Java·R 등 애플리케이션에 라이브러리로 임베드된다. "분석을 위한 SQLite"라고 부르기도 한다.


DuckDB의 위치

SQLite 인-프로세스 행 저장 (OLTP) 단일 파일 DB ≤ 수 GB
DuckDB ★ 인-프로세스 컬럼 저장 (OLAP) 벡터화 실행 단일 노드 수백 GB
Trino / Spark 분산 클러스터 컬럼 저장 (OLAP) 다중 노드 TB~PB 규모
DuckDB는 "서버 없는 분산 엔진"이 아니라, 단일 노드에서 분산 엔진 수준의 분석 성능을 내는 엔진이다.
DuckDB가 분석 엔진 스펙트럼에서 차지하는 위치

아키텍처: 인-프로세스 임베드

DuckDB는 공유 라이브러리(.so / .dll)로 배포된다. 별도 서버 프로세스가 없고, 애플리케이션과 같은 메모리 공간에서 실행된다. TCP 소켓 직렬화가 없어 로컬 파일 접근이 매우 빠르다.

애플리케이션 프로세스 (단일 프로세스) Python / Go 애플리케이션 코드 API 호출 DuckDB 라이브러리 SQL Parser Optimizer 벡터화 실행 엔진 (DataChunk) 스토리지 엔진 (행 그룹 + 컬럼 세그먼트) 외부 파일 Parquet CSV / JSON Arrow IPC S3 / GCS Iceberg .duckdb 파일 직접 읽기/쓰기 결과
DuckDB 인-프로세스 실행 모델

핵심: 애플리케이션 코드가 DuckDB를 라이브러리로 호출하고, DuckDB는 파일(Parquet, CSV, 네이티브 .duckdb)을 직접 읽어 쿼리를 처리한 뒤 결과를 반환한다. 네트워크 왕복이 없다.


컬럼형 스토리지: 행 그룹과 컬럼 세그먼트

DuckDB의 네이티브 스토리지 포맷은 Parquet과 유사한 컬럼형 레이아웃을 사용한다.

  • 행 그룹(Row Group): 기본 120,000행 단위의 수평 파티션. 행 그룹이 쿼리 필터링과 병렬 처리의 최소 단위다.
  • 컬럼 세그먼트(Column Segment): 행 그룹 내 각 컬럼의 데이터. 타입별 압축 알고리즘을 독립적으로 적용한다.
  • 존 맵(Zone Map): 각 컬럼 세그먼트의 최솟값·최댓값을 메타데이터로 저장. WHERE ts > '2024-01-01' 같은 필터로 읽지 않아도 되는 행 그룹을 건너뛴다(predicate pushdown).

압축 알고리즘

DuckDB는 컬럼의 데이터 분포를 분석해 가장 적합한 알고리즘을 자동으로 선택한다.

알고리즘적합한 데이터 패턴
딕셔너리 인코딩저카디널리티 (상태 코드, 범주)
RLE (Run-Length Encoding)반복 값이 많은 정렬된 컬럼
비트 패킹정수 범위가 좁을 때
Frame of Reference값이 특정 범위에 몰릴 때
FSST공통 부분 문자열이 많은 문자열 컬럼
Chimp / Patas부동소수점 시계열

목표는 최대 압축 비율이 아니라 빠른 압축 해제다. 분석 쿼리는 쓰기 속도보다 읽기 속도가 중요하기 때문이다.


벡터화 실행 엔진

DuckDB는 Volcano 모델(한 번에 한 행)이 아닌 벡터화 실행을 채택했다. 데이터를 DataChunk라는 2,048행 배치 단위로 처리한다.

  • CPU SIMD(AVX2/AVX-512) 명령어로 여러 행을 동시에 계산한다.
  • 각 연산자(Filter, Project, Aggregate, Join)가 배치 단위로 호출되어 함수 호출 오버헤드가 줄어든다.
  • 배치 크기(2,048)가 L1/L2 캐시에 맞게 설계되어 캐시 미스를 줄인다.

모셀 기반 병렬 처리(Morsel-Driven Parallelism)

멀티코어 CPU를 활용하기 위해 테이블 스캔을 행 그룹 단위 "모셀"로 쪼개어 워커 스레드에 동적으로 분배한다. 스레드 수는 기본적으로 코어 수와 같고, PRAGMA threads = N으로 조정할 수 있다.

-- 스레드 수 확인 및 조정
SELECT current_setting('threads');
SET threads = 8;

다양한 파일 포맷과 소스 쿼리

DuckDB의 실용적인 강점 중 하나는 외부 파일을 별도 로드 없이 직접 쿼리할 수 있다는 것이다.

-- Parquet 파일 직접 쿼리 (로컬 또는 S3)
SELECT year, sum(amount)
FROM read_parquet('s3://my-bucket/sales/year=2024/**/*.parquet')
GROUP BY year;

-- CSV 자동 스키마 추론
SELECT * FROM read_csv_auto('data/*.csv') LIMIT 5;

-- JSON 파일
SELECT json_extract(payload, '$.user_id') AS uid
FROM read_json('events.jsonl');

-- 여러 Parquet 파일 glob
SELECT count(*) FROM 'logs/2024/**/*.parquet';

S3, GCS, Azure Blob에서 직접 읽으려면 httpfs 확장을 설치한다.

INSTALL httpfs;
LOAD httpfs;
SET s3_region = 'ap-northeast-2';

Apache Iceberg 테이블 쿼리

DuckDB 1.x부터 Iceberg REST Catalog를 통해 Iceberg 테이블을 직접 쿼리할 수 있다. Spark나 Trino 없이 로컬 또는 CI 환경에서 Iceberg 데이터를 탐색할 때 유용하다.

INSTALL iceberg;
LOAD iceberg;

-- Iceberg REST Catalog 연결
CREATE SECRET iceberg_catalog (
    TYPE ICEBERG,
    TOKEN 'my-token',
    ENDPOINT 'https://catalog.example.com'
);

SELECT * FROM iceberg_scan('s3://warehouse/my_table/');

Python 통합

Python에서 DuckDB를 쓰면 Pandas DataFrame과의 연동이 자연스럽다. DuckDB는 Pandas의 Arrow 변환 없이 DataFrame을 직접 쿼리한다.

import duckdb
import pandas as pd

# 메모리 내 DB (기본)
con = duckdb.connect()

# 파일 기반 영속 DB
con = duckdb.connect("analytics.duckdb")

df = pd.read_csv("orders.csv")

# Pandas DataFrame을 DuckDB로 직접 쿼리
result = con.execute("""
    SELECT customer_id, sum(amount) AS total
    FROM df
    WHERE status = 'completed'
    GROUP BY customer_id
    ORDER BY total DESC
    LIMIT 10
""").df()  # 결과를 다시 DataFrame으로

duckdb.connect(":memory:") 또는 인수 없이 duckdb.connect()는 메모리 내 임시 DB다.


dbt + DuckDB: 로컬 개발 패턴

dbt-duckdb 어댑터를 사용하면 Snowflake, BigQuery 없이 로컬에서 dbt 모델을 전체 실행할 수 있다. CI 환경에서 분산 웨어하우스 비용 없이 통합 테스트를 돌릴 때 특히 유용하다.

# profiles.yml
my_project:
  target: dev
  outputs:
    dev:
      type: duckdb
      path: target/dev.duckdb
dbt run --profiles-dir .
dbt test --profiles-dir .

DuckDB vs Trino vs Spark: 언제 무엇을 쓸까

DuckDB 선택 • 데이터 크기 < 수백 GB (단일 노드) • 로컬 EDA / 빠른 탐색 • dbt 개발·테스트 환경 • 임베드 분석 (앱 내 OLAP) • 클러스터 불필요한 파이프라인
Trino 선택 • 다수 소스를 단일 SQL로 페더레이션 • 다중 사용자 대화형 쿼리 • 수 TB 이상 대화형 분석 • Iceberg / Delta / Hudi 중앙 카탈로그
Spark 선택 • 대규모 ETL·변환 배치 파이프라인 • ML 피처 엔지니어링 • Structured Streaming • 수십 TB 이상 배치 처리
엔진 선택 기준

DuckDB는 Trino·Spark의 대체재가 아니라 스택의 다른 계층을 담당한다. 동일 조직에서 DuckDB(로컬·CI)→Trino(대화형)→Spark(배치)를 각각 써도 자연스럽다.


주의점과 한계

  1. 단일 쓰기 프로세스: 같은 .duckdb 파일에 여러 프로세스가 동시 쓰기를 하면 오류가 발생한다. 읽기는 여러 프로세스에서 가능하다.
  2. 동시 쿼리 다중 사용자 제한: 클러스터가 없으므로 수십 명이 동시에 쿼리하는 BI 도구 백엔드로는 적합하지 않다. MotherDuck(DuckDB 클라우드 서비스)이 멀티 세션을 지원한다.
  3. 메모리 한계: 기본적으로 사용 가능한 메모리를 최대한 활용하다가 부족하면 임시 파일로 스필한다. 처리 데이터가 노드 디스크를 초과하면 한계에 부딪힌다.
  4. OLTP 부적합: 높은 TPS의 포인트 업데이트나 단건 삽입에는 InnoDB/PostgreSQL이 더 적합하다.

정리: DuckDB가 쿼리 페더레이션 생태계에서 차지하는 자리

이 시리즈에서 다룬 Trino가 "클러스터 전체 데이터소스를 SQL로 연결"하는 엔진이라면, DuckDB는 "단일 머신에서 파일과 스트림을 직접 분석"하는 엔진이다. 두 도구는 서로 배타적이지 않다. Trino가 Iceberg로 관리하는 데이터를 DuckDB로 로컬에서 샘플링해 탐색하고, 개발한 로직을 Trino나 Spark로 스케일아웃하는 흐름이 현실적인 데이터 플랫폼 패턴이다.


References

  • DuckDB 공식 문서, https://duckdb.org/docs/
  • DuckDB 경량 압축 공식 블로그, https://duckdb.org/2022/10/28/lightweight-compression
  • DuckDB 스토리지 포맷 공식 문서, https://duckdb.org/docs/current/internals/storage
  • Calmops: DuckDB Internals — Vectorized Execution, Columnar Storage, https://calmops.com/database/duckdb/duckdb-internals/
  • endjin: DuckDB in Depth — How It Works and What Makes It Fast, https://endjin.com/blog/duckdb-in-depth-how-it-works-what-makes-it-fast
  • Alibaba Cloud: DuckDB Internals Part 2 — Table Storage Format, https://www.alibabacloud.com/blog/duckdb-internals---part-2-table-storage-format_602657
  • Definite: DuckDB vs Trino — The Full Comparison, https://www.definite.app/blog/duckdb-vs-trino-comparison
  • MotherDuck: What Is DuckDB, https://motherduck.com/learn/what-is-duckdb/