Skip to content
Published on

DuckDB Internals Deep Dive — 임베디드 OLAP, 벡터화 실행, Morsel-Driven 병렬, Storage Format 완전 정복 (2025)

Authors

TL;DR

  • DuckDB는 CWI (Centrum Wiskunde & Informatica, 네덜란드)의 Mark Raasveldt와 Hannes Mühleisen이 2018년 시작. "분석용 SQLite"가 목표.
  • 설계 철학: 단일 파일 파일 형식, 단일 프로세스, 외부 의존성 없음. pip install duckdb만으로 풀 OLAP 엔진.
  • 저장: 컬럼나. 각 블록에 compression (RLE, bitpacking, dictionary, FSST for strings).
  • 실행 모델: Vector 기반 — 1024 행을 한 chunk로 처리. SIMD 친화적.
  • 병렬 처리: Morsel-Driven Parallelism — HyPer 논문의 아이디어. 각 morsel(~100K 행)을 독립적으로 처리.
  • 쿼리 최적화: Cost-based join ordering, filter pushdown, projection pruning, 일반 rule-based 최적화.
  • Python 통합: Pandas DataFrame / Polars / Arrow를 zero-copy로 접근. duckdb.sql("SELECT * FROM my_df")가 바로 동작.
  • Parquet/Arrow/CSV/JSON 네이티브 — 복사 없이 scan.
  • WASM: 브라우저에서도. Observable notebook, Malloy, Evidence가 활용.
  • MotherDuck: DuckDB 기반 클라우드 서비스. "hybrid execution"으로 로컬과 클라우드 혼합.

1. DuckDB가 해결한 문제

1.1 데이터 사이언스의 두 극단

2020년 전후, Python 데이터 과학자의 선택지:

Pandas (로컬, 단일 머신):

  • 작은 데이터(< 10 GB)에 편리.
  • 큰 데이터에서 메모리 부족, 성능 문제.
  • 단일 스레드가 대부분.

Spark / Dask (분산):

  • 큰 데이터(TB+)에 필요.
  • 작은 데이터엔 오버헤드 과다.
  • JVM 클러스터 운영 필요.
  • 시작만 수 초.

그 중간(10 GB ~ 1 TB)이 사각지대.

1.2 Mark Raasveldt와 Hannes Mühleisen

2018년 CWI에서 둘은 **"SQLite for OLAP"**을 구상. 다음을 원했다:

  • 임베디드 (프로세스에 내장).
  • 분석 쿼리에 빠름.
  • 단일 파일 포맷.
  • 다양한 파일 포맷(Parquet, CSV) 지원.
  • Python이 주 사용처이지만 C/C++ API도.

2019년 첫 공개. 2020-2022년 빠른 성장. 2024년 1.0 릴리스 — 프로덕션 준비.

1.3 시간이 증명한 선택

Pandas, Spark가 있는 상황에서 "또 다른 DB"가 필요할까? 2025년 답: 그렇다.

  • Pandas 2.0+: DuckDB 또는 Arrow를 백엔드로.
  • Polars: 유사 철학, Rust로.
  • Malloy, Evidence: DuckDB 기반 BI.
  • Fivetran, dlt: ETL에 DuckDB 내장.
  • Mode, Hex: 인터랙티브 분석에 DuckDB.
  • MotherDuck: DuckDB를 서버리스 클라우드로.

DuckDB가 "데이터 과학의 새 표준"이 되고 있다.


2. 설계 철학

2.1 임베디드

DuckDB는 라이브러리 — Python의 import duckdb, C++의 링크, Rust의 crate.

import duckdb

con = duckdb.connect('my.duckdb')  # 또는 ':memory:'
con.execute("CREATE TABLE users (id INT, name VARCHAR)")
con.execute("INSERT INTO users VALUES (1, 'Alice')")
result = con.execute("SELECT * FROM users").fetchall()

서버 프로세스 없음. SQLite와 같은 모델.

2.2 단일 파일

my_data.duckdb  ← 하나의 파일에 모든 테이블, 인덱스

백업, 배포, 공유 간편. 수 GB ~ 수백 GB까지.

2.3 외부 의존성 없음

C++ 코드 하나로 빌드 가능. 부속 라이브러리 없음. 작은 바이너리 (~30 MB).

2.4 "No Locks, No Compromise"

동시성을 지원하되 간단히:

  • Single-writer: 한 번에 하나의 writer.
  • Multiple readers: 읽기는 writer와 병행.
  • MVCC 기반.

복잡한 분산 consensus 없이 간단한 구현이면서도 analytical 워크로드에 충분.

2.5 호환성

  • ANSI SQL 준수 (PostgreSQL 호환).
  • Parquet, Arrow, CSV, JSON 직접 읽기.
  • Python, R, Java, Node.js, Go, Rust, WASM 바인딩.

3. 저장 — 컬럼나

3.1 기본 구조

각 테이블이 여러 row group으로 구성. 각 row group은 여러 컬럼 chunk.

Table "events":
  Row Group 1 (122,880 rows):
    Column "timestamp": [bytes...]
    Column "user_id": [bytes...]
    Column "event_type": [bytes...]
    Column "value": [bytes...]
  Row Group 2:
    ...

각 컬럼 chunk가 독립적으로 압축.

3.2 Compression

DuckDB의 compression 코덱:

Uncompressed: 원본 그대로. RLE (Run-Length Encoding): 반복 값. AAABBC3A 2B 1C. Bit-packing: 정수 값 범위에 맞는 최소 비트. 0-7이면 3 비트씩. Dictionary: 고유 값 사전 + 인덱스. 문자열 컬럼에 효과적. FSST (Fast Static Symbol Table): 문자열 압축. DuckDB 팀 논문. Chimp / Alp: 실수(float) 압축. 시계열 데이터용.

각 chunk마다 가장 효율적인 compression 자동 선택.

3.3 Row Group 크기

기본 row group은 122,880 행. 이 숫자는 2^17 - 2^13 — 특정 최적화가 가능하게 선택됨.

  • 너무 작으면: 메타데이터 오버헤드.
  • 너무 크면: filter pushdown 덜 효과적.
  • 120K 정도가 균형.

3.4 Block Size

내부적으로 256 KB 블록. 디스크 I/O 단위.

3.5 Checkpoint

Write 후 주기적으로 checkpoint: 메모리의 WAL을 실제 파일에 반영. SQLite와 유사.

3.6 파일 포맷 안정성

2024년 1.0 릴리스부터 Storage Format 안정화. 이전 버전에서는 포맷 변경이 잦았지만 이제 forward/backward 호환.


4. Vector 기반 실행

4.1 Volcano vs Vectorized

전통 DB (PostgreSQL):

for each row:
    apply_filter(row)
    transform(row)
    aggregate(row)

Volcano model — 한 행씩 처리. 함수 호출 오버헤드 누적. 분기 예측 실패.

Columnar + Vectorized (DuckDB, ClickHouse):

for each batch of 1024 rows:
    filter_batch()
    transform_batch()
    aggregate_batch()

Vector model — 1024 행을 한 번에. SIMD 친화적, 캐시 효율.

4.2 DuckDB의 Vector 크기

1024 행 per chunk가 기본. 이유:

  • L1/L2 캐시에 맞음: 한 컬럼 1024 × 8 바이트 = 8 KB. L1 = 32 KB, L2 = 256 KB. 여러 컬럼 동시에 처리해도 캐시에 머무름.
  • SIMD 단위: AVX-512는 8개 i64를 한 번에. 1024 / 8 = 128 반복. 효율적.
  • 분기 예측: 반복 문 내부는 예측하기 쉬움.

4.3 Vector 구조

struct Vector {
    Type type;           // INT, VARCHAR, DOUBLE, etc.
    VectorType vtype;    // FLAT, CONSTANT, DICTIONARY, SEQUENCE
    ValidityMask nulls;  // 각 행의 null 여부
    void* data;          // 실제 값
    idx_t count;         // 현재 행 수
};

Vector type:

  • FLAT: 연속 배열.
  • CONSTANT: 모든 행이 같은 값. 값 하나만 저장 (압축).
  • DICTIONARY: 고유 값 + 인덱스.
  • SEQUENCE: 등차수열 (예: 0, 1, 2, ..., 1023).

Vector type이 compile time에 알려지면 특수 경로 실행 → 성능 ↑.

4.4 Chunks

여러 vector가 모이면 DataChunk:

struct DataChunk {
    vector<Vector> data;
    idx_t count;  // 현재 행 수 (최대 1024)
};

"1024 행의 넓은 테이블". 쿼리 실행 단위.

4.5 Operator Pipeline

SELECT user_id, SUM(value)
FROM events
WHERE event_type = 'click'
GROUP BY user_id

Operator tree:

HashAggregate (GROUP BY user_id, SUM(value))
Filter (event_type = 'click')
TableScan (events)

각 operator가 DataChunk를 input으로, DataChunk를 output으로:

class Operator {
    virtual unique_ptr<DataChunk> GetChunk() = 0;
};

Pipeline 기반 실행. Pull model (operator가 위 operator에 "다음 chunk 줘"라고 요청).


5. Morsel-Driven Parallelism

5.1 전통 병렬 모델의 문제

전통적 접근: exchange operator로 데이터를 worker 사이에 보냄.

  FilterExchangeAggregate
            (무거운)

문제:

  • Shuffle 비용 (data 복사).
  • Load imbalance: 일부 worker가 느림.
  • 복잡한 구현.

5.2 HyPer Morsel 아이디어

2014년 Viktor Leis et al.의 "Morsel-Driven Parallelism" 논문 (HyPer DB).

핵심:

  1. 입력을 morsel로 분할 — 각 ~100K 행.
  2. Worker pool이 있음 (thread 수).
  3. 각 worker가 morsel queue에서 가져와 처리.
  4. Work stealing으로 load balance 자동.

Operator는 worker가 여러 개인 것을 모름. 각자 자기 morsel을 처리한 후 output morsel을 push.

5.3 DuckDB의 구현

DuckDB는 pipeline 기반 parallelism:

Pipeline 1: TableScanFilterPartialHashAggregate
Pipeline 2: FinalHashAggregateOutput

각 pipeline이 병렬 실행. 한 pipeline이 완료되면 다음 pipeline 시작.

Pipeline 내부에서 morsel 단위로 병렬:

Input morsels:  [M1] [M2] [M3] [M4] [M5] ...

Worker 1: pull M1 → process → output
Worker 2: pull M2 → process → output
Worker 3: pull M3 → process → output
Worker 4: pull M4 → process → output
...

Thread-safe queue로 morsel 분배. Lock 거의 없음.

5.4 Parallel Hash Aggregate

Aggregation의 병렬화:

Phase 1: 각 worker가 partial hash table 빌드 (local morsel 기반)
Phase 2: Partial hash tables를 merge
Phase 3: Final hash table에서 output

Phase 1이 완전 독립 → 선형 확장. Phase 2는 partition-based merge — 각 partition을 독립 worker에 배정.

5.5 결과

DuckDB는 코어 수에 거의 선형으로 확장. 4 cores → 4배, 16 cores → 16배 (실제 워크로드에 가까운 경우).


6. Query Optimizer

6.1 Rule-Based Optimizer

기본 최적화:

  • Filter pushdown: Filter를 scan 쪽으로 밀어서 일찍 적용.
  • Projection pruning: 불필요한 컬럼 읽지 않음.
  • Constant folding: 1 + 2 같은 상수 계산.
  • CSE (Common Subexpression Elimination).
  • Predicate simplification: x AND FALSEFALSE.
  • Deliminator: correlated subquery 제거.

6.2 Cost-Based Join Ordering

Join 순서는 성능에 큰 영향:

A (1M) JOIN B (100) JOIN C (10)
  • A JOIN B first: 1M × 100 = 100M intermediate.
  • B JOIN C first: 100 × 10 = 1K intermediate, then 1K JOIN A.

두 번째가 훨씬 빠름.

DuckDB는 dynamic programming 기반 join ordering. 테이블 카디널리티 + 조건 selectivity로 cost 추정.

6.3 Statistics

기본 통계:

  • Row count.
  • Distinct count (approximate with HyperLogLog).
  • Min/max per column.
  • Null fraction.

ANALYZE 없이도 scan 중 자동 수집.

6.4 Zonemap / Pruning

Row group마다 min/max 저장. 쿼리 예:

SELECT * FROM events WHERE timestamp > '2024-01-01'

각 row group의 max timestamp를 체크 → 조건에 맞지 않으면 skip. 실제 데이터 읽지 않음.

Parquet 같은 포맷의 predicate pushdown과 유사.

6.5 Late Materialization

Column store의 장점 활용:

SELECT name, email FROM users WHERE age > 30
  • Early materialization: 모든 컬럼을 읽은 후 filter → 낭비.
  • Late materialization: age만 먼저 읽어 filter → matching row index만 저장 → name, email을 그 index만 → 효율.

DuckDB는 기본 late materialization.


7. Python 통합

7.1 Zero-Copy DataFrame

DuckDB의 killer feature:

import duckdb
import pandas as pd

df = pd.DataFrame({
    'name': ['Alice', 'Bob'],
    'age': [30, 25]
})

# DataFrame을 SQL로 직접 쿼리
result = duckdb.sql("SELECT * FROM df WHERE age > 28").df()
#                                    ^^ Python 변수를 테이블로
print(result)

어떻게? DuckDB가 Python interpreter의 변수를 inspect해서 DataFrame을 zero-copy로 접근. Pandas의 내부 Arrow 또는 NumPy 버퍼를 직접 읽는다.

7.2 Polars와 Arrow도

import polars as pl
df = pl.DataFrame({'a': [1,2,3]})
duckdb.sql("SELECT * FROM df").pl()  # Polars 결과
import pyarrow as pa
tbl = pa.table({'a': [1,2,3]})
duckdb.sql("SELECT * FROM tbl").arrow()

모든 프레임 라이브러리와 상호 운용.

7.3 Relational API

SQL 대신 Python API로:

rel = duckdb.sql("SELECT * FROM df")
    .filter("age > 28")
    .project("name, age * 2 as double_age")
    .aggregate("SUM(double_age)")

Lazy evaluation — .df(), .pl(), .fetchall() 호출 전엔 실행 안 됨. Optimizer가 전체 계획을 보고 최적화.

7.4 Jupyter Notebook

# cell 1
import duckdb
con = duckdb.connect(':memory:')

# cell 2
%load_ext sql  # ipython-sql 확장
%sql duckdb:///my.duckdb

# cell 3
%%sql
SELECT * FROM my_table LIMIT 10

SQL을 직접 Jupyter에 쓸 수 있다. 분석 워크플로우가 매끄럽다.

7.5 Pandas 2.0+ 통합

Pandas 2.0은 Arrow 백엔드 지원:

df = pd.read_parquet('data.parquet', dtype_backend='pyarrow')

이 df는 Arrow 메모리 구조. DuckDB가 zero-copy로 접근.

앞으로 "Pandas → DuckDB → 결과 → Pandas" 파이프라인이 표준이 될 가능성.


8. Parquet / Arrow 통합

8.1 Parquet 직접 읽기

SELECT * FROM 'data.parquet' WHERE year = 2024

파일을 테이블처럼. DuckDB가:

  1. Parquet footer 파싱.
  2. Row group metadata 확인.
  3. Predicate pushdown — 해당 row group만 읽기.
  4. Projection pushdown — 필요 컬럼만.
  5. 병렬 scan.

8.2 여러 파일 동시

SELECT * FROM 'logs/*.parquet'
SELECT * FROM read_parquet(['f1.parquet', 'f2.parquet'])

Hive partitioning 지원:

data/year=2024/month=01/day=01/part.parquet

WHERE year = 2024 → 자동으로 해당 디렉토리만 스캔.

8.3 S3 / HTTP

SELECT * FROM 's3://bucket/data.parquet'
SELECT * FROM 'https://example.com/data.csv'

httpfs extension으로 원격 파일 직접. Range request로 필요한 바이트만 다운로드.

8.4 CSV / JSON

SELECT * FROM 'data.csv'  -- 자동 스키마 감지
SELECT * FROM read_json('data.json', auto_detect=true)

Smart CSV parser — Unicode BOM, 다양한 구분자, quoted fields 모두 처리.

JSON도 nested 지원:

SELECT item->>'name' AS name, (item->>'price')::FLOAT AS price
FROM 'items.json'

8.5 Arrow 결과

result = con.execute("SELECT * FROM t").fetch_arrow_table()

Zero-copy로 Arrow 테이블 반환. DuckDB 내부 vector가 이미 Arrow 호환.


9. WASM과 브라우저

9.1 DuckDB-WASM

DuckDB가 WebAssembly로 컴파일 가능. 브라우저에서 직접 분석 DB:

<script type="module">
import * as duckdb from '@duckdb/duckdb-wasm';

const JSDELIVR_BUNDLES = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(JSDELIVR_BUNDLES);

const worker = new Worker(bundle.mainWorker);
const logger = new duckdb.ConsoleLogger();
const db = new duckdb.AsyncDuckDB(logger, worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);

const conn = await db.connect();
const result = await conn.query(`
    SELECT * FROM 'https://example.com/data.parquet' LIMIT 10
`);
</script>

서버 없이 클라이언트 측 분석. Observable, Malloy, Evidence 같은 도구가 활용.

9.2 사용 사례

  • Observable notebook: 브라우저에서 DuckDB로 JSON/CSV/Parquet 분석.
  • Evidence: 정적 대시보드. 빌드 타임에 DuckDB로 집계, HTML 배포.
  • Static BI: 데이터를 Parquet으로 배포, 브라우저에서 SQL.

"서버 없는 BI"가 가능해졌다.

9.3 성능

Native와 비교하면 20-50% 느리지만 여전히 실용적. 100 MB Parquet을 수 초에 분석.


10. Python 워크플로우 예

10.1 대체 시나리오

Before (Pandas):

import pandas as pd

df = pd.read_parquet('large.parquet')
grouped = df[df['event_type'] == 'click'].groupby('user_id')['value'].sum()
top10 = grouped.nlargest(10)
  • 전체 파일 메모리 로드.
  • 단일 스레드.
  • 큰 데이터에 느림.

After (DuckDB):

import duckdb

result = duckdb.sql("""
    SELECT user_id, SUM(value) AS total
    FROM 'large.parquet'
    WHERE event_type = 'click'
    GROUP BY user_id
    ORDER BY total DESC
    LIMIT 10
""").df()
  • 파일 스트리밍 (전체 로드 X).
  • 병렬 실행.
  • 10배 이상 빠름.
  • 같은 Pandas DataFrame 결과.

10.2 ETL 파이프라인

con = duckdb.connect('warehouse.duckdb')

# Extract: API/file에서
con.execute("""
    CREATE OR REPLACE TABLE raw_events AS
    SELECT * FROM read_parquet('s3://bucket/events/*.parquet')
""")

# Transform
con.execute("""
    CREATE OR REPLACE TABLE clean_events AS
    SELECT
        user_id,
        event_type,
        CAST(timestamp AS TIMESTAMP) AS ts,
        value
    FROM raw_events
    WHERE value > 0
""")

# Load
con.execute("COPY clean_events TO 'output.parquet' (FORMAT PARQUET)")

로컬에서 몇 GB → 수십 GB ETL이 가능. Spark 필요 없음.

10.3 Jupyter 분석

%load_ext sql
%sql duckdb:///analysis.duckdb

%%sql
SELECT
    date_trunc('month', purchase_date) AS month,
    category,
    SUM(amount) AS revenue
FROM sales
GROUP BY 1, 2
ORDER BY 1, 2

Notebook이 SQL-first. Pandas보다 SQL이 표현력이 좋은 경우 — 특히 window function, CTE, join.

10.4 dbt와 통합

dbt-duckdb adapter:

# profiles.yml
my_project:
  target: dev
  outputs:
    dev:
      type: duckdb
      path: /tmp/my.duckdb

dbt의 모든 feature (models, tests, snapshots)를 로컬 DuckDB에서. 개발/테스트가 빠름. 프로덕션은 Snowflake 등 그대로.


11. Polars와의 관계

11.1 두 프로젝트

Polars: Ritchie Vink의 Rust 기반 DataFrame 라이브러리. DuckDB: C++ 기반 SQL 엔진.

같은 문제(빠른 로컬 OLAP)를 다른 인터페이스로.

11.2 Polars의 엔진

사실 Polars의 초기 query engine은 DuckDB 팀과 함께 설계했다. Expressions, lazy evaluation, 최적화 패턴이 비슷.

2024년부터 Polars가 자체 엔진(new streaming engine)으로 전환 중. 하지만 많은 아이디어가 DuckDB에서 비롯.

11.3 비교

항목DuckDBPolars
언어C++Rust
인터페이스SQLDataFrame API
파일 포맷자체 .duckdbParquet 또는 자체
Python 통합Zero-copyNative
분석 기능풍부 (SQL 표준)Polars API
생태계dbt, BI 도구ML 파이프라인

둘 다 훌륭. SQL이 편하면 DuckDB, DataFrame API가 편하면 Polars.

11.4 함께 사용

import polars as pl
import duckdb

# Polars DataFrame
df = pl.read_parquet('data.parquet')

# DuckDB로 SQL 쿼리
result = duckdb.sql("SELECT name, SUM(value) FROM df GROUP BY name").pl()
#                                                                    ^^
#                                                         Polars 결과로 반환

상호 운용성이 우수. 같은 Arrow 메모리 기반.


12. 확장 (Extensions)

12.1 Extension System

DuckDB는 확장 가능한 아키텍처:

INSTALL httpfs;
LOAD httpfs;

-- 이제 S3 접근 가능
SELECT * FROM 's3://bucket/file.parquet';

Extension은 동적 라이브러리. 런타임에 로드.

12.2 주요 Extensions

  • httpfs: HTTP/S3 접근.
  • postgres_scanner: Postgres 테이블 직접 쿼리.
  • mysql_scanner: MySQL도.
  • sqlite_scanner: SQLite 테이블 접근.
  • iceberg: Apache Iceberg 테이블.
  • delta: Delta Lake 테이블.
  • fts: Full-text search.
  • json: 고급 JSON 함수.
  • spatial: GIS 함수.
  • vss (Vector Similarity Search): 임베딩 검색 (HNSW).
  • icu: 국제화.

수십 개. 생태계 풍부.

12.3 써드파티

커뮤니티가 만든 extension도 로드 가능:

INSTALL 'my_extension' FROM 'https://example.com/';
LOAD 'my_extension';

Signing 지원. 안전한 로딩.

12.4 Iceberg / Delta

현대 데이터 레이크 포맷 지원:

SELECT * FROM iceberg_scan('s3://bucket/iceberg-table');

다른 엔진(Spark, Trino)이 쓴 테이블을 DuckDB로 읽기. 엔진 간 상호 운용성.


13. MotherDuck

13.1 회사

2022년 DuckDB Labs의 일부 창립자가 MotherDuck 회사 설립. DuckDB 기반 관리형 서비스.

13.2 Hybrid Execution

핵심 아이디어: 로컬과 클라우드의 하이브리드.

로컬 (노트북):
  - 사용자 상호작용
  - 작은 데이터 캐시
  - 최근 쿼리

클라우드 (MotherDuck):
  - 큰 데이터셋 저장
  - 공유 데이터
  - 무거운 계산

쿼리가 "로컬 데이터 + 클라우드 데이터"를 join 가능. Optimizer가 어디서 실행할지 결정.

13.3 Use Case

작은 팀:

  • MotherDuck에 공용 데이터셋.
  • 각자 노트북에서 DuckDB로 분석.
  • 같은 쿼리가 어느 쪽이든 작동.

13.4 Free Tier

MotherDuck는 무료 티어 제공. 작은 팀에 매우 매력적인 가격.

Snowflake, BigQuery의 저비용 대안으로 자리잡는 중.


14. 성능 벤치마크

14.1 TPC-H

표준 OLAP 벤치마크. DuckDB는 TPC-H를 적극 활용.

Single-machine 성능 비교 (1TB 스케일, 근사):

DuckDB:      1x (baseline)
PostgreSQL:  20-50x slower
SQLite:      100x+ slower
ClickHouse:  0.5-0.8x (약간 빠름)

DuckDB는 단일 머신 성능에 최적화. 분산은 MotherDuck 또는 partition 조합.

14.2 Pandas 비교

단순 GroupBy 예 (10 GB Parquet):

Pandas:  45 (메모리 부족 위험)
Polars:  12DuckDB:  8

DuckDB가 일반적으로 가장 빠름. 단, 작은 데이터(100 MB 미만)는 Pandas가 오버헤드 적어 비슷.

14.3 Spark 비교

입력: 10 GB CSV

Spark (local mode): 120 (JVM startup + plan + execute)
Spark (cluster):    40 (cluster에 비용 있음)
DuckDB:             8

소규모 데이터에서 DuckDB가 압도적 빠름. Spark의 startup 오버헤드가 작은 워크로드에 과함.

1 TB급에서는 Spark 클러스터가 유리해질 수 있지만 DuckDB도 놀라운 성능.


15. 튜닝

15.1 메모리

SET memory_limit='8GB';

기본: 시스템의 80%. 작은 머신에선 명시적으로 제한.

큰 데이터에서 메모리 초과 시 disk spill — temp file을 사용.

15.2 스레드

SET threads=8;

기본: CPU 코어 수. 통제가 필요하면 명시.

15.3 Temp directory

SET temp_directory='/fast/ssd/tmp';

Disk spill용. 빠른 SSD에 두면 성능 ↑.

15.4 Profiling

EXPLAIN ANALYZE SELECT * FROM t WHERE x > 10;

실제 실행 시간, row count, operator별 비용 표시.

PRAGMA enable_profiling;

각 쿼리의 자세한 프로파일.

15.5 튜닝 팁

  • 필요한 컬럼만 SELECT (projection pruning 활용).
  • WHERE 절을 가능한 일찍 (predicate pushdown).
  • 큰 join에 ORDER BY + LIMIT: top-k만 필요하면 정렬 일부만.
  • 인덱스보다는 파티셔닝: row group skipping이 대부분 경우 충분.
  • COPY for bulk load: INSERT보다 100배 빠름.

16. 학습 리소스

공식:

영상:

  • Hannes Mühleisen, Mark Raasveldt의 발표들 (CIDR, SIGMOD).
  • "DuckDB under the hood" 시리즈.

:

  • Mark Raasveldt & Hannes Mühleisen의 논문들.
  • CWI Database Architecture 그룹 논문들.

예제:

비디오:

  • "CMU Database Group" 강의 중 Vectorized Execution.

17. 요약 — 한 장 정리

┌─────────────────────────────────────────────────────┐
DuckDB Cheat Sheet├─────────────────────────────────────────────────────┤
│ 철학:Embedded OLAP database                             │
"SQLite for analytics"│   단일 파일, 단일 프로세스                             │
No server, no lock                                 │
│                                                       │
│ 저장:│   컬럼나                                               │
Row groups (122,880 rows)Compression: RLE, bitpack, dict, FSSTCheckpoint + WAL│                                                       │
│ 실행:Vector-based (1024 rows per chunk)SIMD friendly                                      │
Late materialization                               │
Pipeline operators                                 │
│                                                       │
│ 병렬:Morsel-Driven Parallelism~100K rows per morsel                              │
Work stealing                                      │
│   거의 선형 확장                                      │
│                                                       │
Optimizer:Rule-based (pushdown, pruning, CSE)Cost-based join ordering                           │
HyperLogLog distinct count                         │
Row group skipping (min/max)│                                                       │
Python:│   pip install duckdb                                 │
Zero-copy DataFrame 접근 (Pandas, Polars, Arrow)Relational API (lazy)Jupyter ipython-sql 통합                           │
│                                                       │
│ 파일 통합:Parquet (predicate pushdown)Arrow (zero-copy)CSV (smart parser)JSON (nested 지원)S3 / HTTP (httpfs extension)│                                                       │
│ 브라우저:DuckDB-WASMObservable, Malloy, Evidence│                                                       │
Extensions:│   httpfs, postgres_scanner, iceberg, delta          │
│   fts, spatial, vss (vector search)│                                                       │
MotherDuck:│   관리형 서비스                                        │
Hybrid execution (local + cloud)│   무료 티어                                           │
│                                                       │
│ vs 경쟁자:Pandas: 10배 빠름, 큰 데이터 OKPolars: 유사 철학, DataFrame APISQLite: OLAP에 훨씬 빠름                            │
ClickHouse: DuckDB가 임베디드                      │
Spark: DuckDB가 단일 머신에서 빠름                  │
│                                                       │
│ 튜닝:│   memory_limit, threads                              │
EXPLAIN ANALYZEProjection + predicate pushdown                    │
COPY for bulk load                                 │
└─────────────────────────────────────────────────────┘

18. 퀴즈

Q1. DuckDB가 "SQLite for analytics"로 불리는 이유는?

A. 동일한 배포/사용 모델, 다른 워크로드 최적화. SQLite가 OLTP(트랜잭션, 작은 읽기/쓰기, 많은 동시 사용자)에 임베디드 DB를 제공한 반면, DuckDB는 OLAP(큰 스캔, 집계, 복잡 쿼리)에 같은 모델을 적용. 공통점: (1) 라이브러리 형태 — 서버 없음, pip install로 끝, (2) 단일 파일 포맷 — 백업/이동 간편, (3) 외부 의존성 없음 — 작은 바이너리, (4) ACID + MVCC. 차이: SQLite는 로우 기반 저장 (B-tree), DuckDB는 컬럼나 저장 + 벡터화 실행. 같은 "임베디드 DB" 철학을 다른 워크로드에 적용한 것이 DuckDB의 genius. 2020년대 데이터 과학자가 원했던 "작은 서버 없이 SQL 분석"의 답.

Q2. Morsel-Driven Parallelism이 전통 exchange 기반 병렬과 다른 점은?

A. 데이터 이동 최소화와 work stealing. 전통적 병렬 DB(Spark, PostgreSQL parallel query)는 exchange operator로 데이터를 worker 사이에 shuffle — 네트워크 비용 크고, shuffle 후 load imbalance가 남음(일부 worker가 느림). Morsel-Driven: 입력을 작은 morsel(~100K rows)로 나누고 worker pool이 queue에서 가져옴. 각 worker가 자기 속도로 pull → 빠른 worker가 더 많은 morsel 처리 → 자동 load balance. Shuffle 없고, 각 morsel은 독립적. 2014 Leis et al.의 HyPer 논문이 제안, DuckDB/Umbra/Vectorwise가 채택. 장점: (1) 구현 단순(lock-free queue), (2) 선형 확장, (3) long-tail 제거. 단일 머신에서 수십 코어 활용에 최적.

Q3. DuckDB의 vector가 1024 행인 이유는?

A. L1/L2 캐시 최적화 + SIMD 효율. 너무 작으면(예: 16 rows) 함수 호출/분기 예측 오버헤드가 지배 → Volcano 모델의 단점 재현. 너무 크면(예: 100K rows) 한 컬럼이 L1/L2 캐시를 초과 → 스필. 1024 rows × 8 bytes = 8 KB per column — L1(32 KB)에 여러 컬럼이 동시 상주 가능. AVX-512 기준 8 × int64를 한 번에 처리 → 1024 / 8 = 128 iterations — 분기 예측이 쉬움. 또한 row group size 122,880 = 2^17 - 2^13이라는 특정 숫자도 compression + vector 크기 조합에서 최적으로 설계. "마법의 숫자"가 아니라 하드웨어 특성에 기반. ClickHouse는 65,536, DuckDB는 1024 — 각자 다른 trade-off. "행 기반 Volcano → column-at-a-time → vector-at-a-time"의 진화에서 현재 sweet spot.

Q4. DuckDB가 Python DataFrame을 zero-copy로 접근하는 방법은?

A. Arrow 메모리 표현의 활용. Pandas 2.0+, Polars, PyArrow는 모두 Arrow columnar memory layout을 내부 또는 외부 표현으로 사용. DuckDB의 내부 vector도 Arrow 호환. duckdb.sql("SELECT * FROM df") 호출 시 DuckDB가: (1) Python interpreter의 locals에서 df 변수 검색(replacement scan), (2) DataFrame 타입 감지, (3) 내부 Arrow 버퍼 포인터 획득, (4) DuckDB vector가 그 같은 메모리를 가리키게 함. 데이터 복사 없음 → 거대한 DataFrame도 지연 없음. Pandas 1.x(NumPy 기반)는 일부 복사 필요하지만 2.0+의 Arrow 백엔드는 진짜 zero-copy. 역방향도 동일 — .df(), .pl(), .arrow() 결과도 zero-copy. 결과: Python과 SQL이 투명하게 통합 — 이것이 Pandas 대체 가속의 핵심 기술적 근거.

Q5. DuckDB의 row group size(122,880)가 왜 특별한가?

A. Compression + vector 크기 + row group skipping의 균형점. 더 작게 하면(예: 10K) row group 메타데이터 오버헤드가 커지고, 압축 효율 떨어짐. 더 크게 하면(예: 1M) predicate pushdown으로 skip할 수 있는 단위가 커서 selective 쿼리에서 필요 이상의 데이터를 읽음. 122,880 = 120 × 1024 — 120개 vector가 한 row group. 이 크기에서 (1) compression dictionaries가 충분히 크고, (2) min/max statistics가 의미 있는 skipping 가능, (3) metadata 오버헤드가 총 크기의 1% 미만. Parquet의 기본 row group(1M rows)은 bulk analytics 최적, DuckDB 122,880은 point-ish queries에도 대응. 같은 이유로 Parquet 읽을 때 DuckDB가 120K 단위로 cut해서 처리. 설계 결정의 디테일이 실제 쿼리 성능에 미치는 영향의 좋은 예.

Q6. DuckDB-WASM이 가능하게 한 새로운 사용 사례는?

A. 서버 없는 BI와 정적 대시보드. 전통 BI는 서버 + DB + 인증 인프라 필요 — 작은 팀/개인에게 과함. DuckDB-WASM: (1) Parquet/CSV를 static file로 호스팅 (GitHub Pages, S3, CloudFront), (2) 브라우저의 DuckDB-WASM이 range request로 필요한 바이트만 다운로드, (3) SQL이 브라우저에서 실행, (4) 결과를 JS로 시각화. Evidence.dev가 이 패턴: Markdown + SQL을 빌드 타임에 HTML/JSON으로 static 생성, 방문자가 브라우저에서 쿼리 가능. Observable notebook: 인터랙티브 분석, 모든 것이 클라이언트. Mosaic (UW IDL): 수백만 행 인터랙티브 시각화. "작은 서비스 + 정적 파일 + DuckDB-WASM"이 간단한 분석 앱의 새 아키텍처. 인프라 비용 거의 0, 확장성 무한(CDN), 프라이버시 우수(데이터가 서버에 갈 필요 없음). Snowflake의 대척점.

Q7. DuckDB와 Polars는 경쟁 관계인가 보완 관계인가?

A. 둘 다 — 같은 문제를 다른 각도에서 해결하는 보완적 도구. 공통점: (1) 로컬/임베디드 OLAP, (2) 컬럼나 + 벡터화, (3) Arrow 메모리 호환, (4) Pandas 대체 목표. 차이: DuckDB는 SQL 엔진 (C++), Polars는 DataFrame API (Rust). 사용자 선호에 따라 — SQL이 편하면 DuckDB, method chaining이 편하면 Polars. 흥미로운 역사: Polars의 초기 query engine 아이디어는 DuckDB 팀과 공유됐다 (expressions, lazy eval, 최적화). 2024년 Polars가 자체 streaming engine으로 전환 중이지만 뿌리는 같음. 실무 패턴: 함께 사용. Polars로 DataFrame 조작 → duckdb.sql()로 SQL 쿼리 → Polars DataFrame으로 돌려받기. 같은 Arrow 메모리로 zero-copy. "둘 중 하나"가 아니라 "둘 다"가 정답. Pandas(순차, 단일 스레드)를 대체하는 공동 전선.


이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:

  • "ClickHouse Internals Deep Dive" — 서버 기반 컬럼나 OLAP.
  • "SQLite Internals Deep Dive" — 임베디드 OLTP의 교과서.
  • "Apache Arrow & Columnar Memory" — 공통 메모리 포맷.
  • "Columnar Storage (Parquet/ORC/Arrow/Dremel)" — 파일 포맷 심층.