- Authors
- Name
- 1. 들어가며
- 2. DuckDB 아키텍처 이해
- 3. 벡터화 실행 엔진
- 4. 설치와 환경 구성
- 5. SQL 분석 쿼리 실전
- 6. Python 통합
- 7. 데이터 레이크 통합 (Parquet, S3, Delta Lake)
- 8. 분석 도구 비교
- 9. 확장 기능(Extensions) 활용
- 10. MotherDuck: 클라우드 DuckDB
- 11. 성능 최적화 팁
- 12. 트러블슈팅
- 13. 프로덕션 운영 패턴
- 14. 실패 사례와 대응
- 15. DuckDB를 쓰면 안 되는 경우 정리
- 16. 참고자료

1. 들어가며
데이터 분석 환경이 급격히 변하고 있다. 과거에는 분석 쿼리 하나를 실행하기 위해 Spark 클러스터를 띄우거나, BigQuery에 데이터를 올리거나, Redshift 인스턴스를 프로비저닝해야 했다. 하지만 노트북 한 대의 RAM이 32GB를 넘고, SSD 속도가 3GB/s에 달하는 시대에 "분석을 위해 반드시 분산 시스템이 필요한가?"라는 질문이 제기되기 시작했다.
DuckDB는 이 질문에 대한 가장 설득력 있는 답이다. 네덜란드 CWI(Centrum Wiskunde and Informatica)에서 Mark Raasveldt와 Hannes Muehleisen이 시작한 이 프로젝트는, SQLite의 임베디드 철학을 OLAP 세계에 적용한 인프로세스(in-process) 분석 데이터베이스다. 별도의 서버 프로세스 없이, pip install duckdb 한 줄로 수억 행의 Parquet 파일을 서브초 만에 집계할 수 있다.
DuckDB를 선택해야 하는 시나리오는 다음과 같다.
- 로컬 머신에서 수 GB~수십 GB 규모의 분석 쿼리를 빠르게 돌리고 싶은 경우
- Parquet, CSV, JSON 파일을 ETL 파이프라인 없이 바로 SQL로 쿼리하고 싶은 경우
- Pandas DataFrame의 성능 한계에 부딪혀 대안을 찾는 경우
- S3 데이터 레이크의 파일을 서버 없이 직접 분석하고 싶은 경우
- CI/CD 파이프라인에서 데이터 품질 검증용 분석 쿼리가 필요한 경우
반면 DuckDB가 적합하지 않은 경우도 명확하다. 동시 다발적인 쓰기가 필요한 OLTP 워크로드, 수백 명이 동시에 접속하는 서비스 백엔드, 페타바이트 이상의 분산 처리가 필요한 경우에는 PostgreSQL, ClickHouse, Spark 같은 도구가 더 적합하다.
2. DuckDB 아키텍처 이해
DuckDB의 놀라운 성능은 세 가지 핵심 설계 원칙의 시너지에서 나온다. 컬럼 지향 스토리지, 벡터화 실행 엔진, 그리고 모젤 기반 병렬 처리(Morsel-Driven Parallelism)다.
인프로세스(In-Process) 아키텍처
DuckDB는 SQLite처럼 별도의 서버 데몬 없이 애플리케이션 프로세스 내부에서 직접 실행된다. 네트워크 통신 오버헤드가 전혀 없으며, 런타임 의존성도 없다. Python, R, Java, Node.js 등 다양한 언어에서 라이브러리 형태로 로드된다.
컬럼 지향 스토리지
전통적인 행 지향(row-oriented) 데이터베이스는 하나의 행에 속한 모든 컬럼 값을 연속된 블록에 저장한다. 분석 쿼리가 전체 컬럼 중 일부만 필요로 할 때 나머지 데이터도 함께 읽어야 하므로 I/O 낭비가 크다.
DuckDB는 컬럼 단위로 데이터를 저장하여, 쿼리에 필요한 컬럼만 선택적으로 스캔한다. 동일 타입의 값이 연속 배치되므로 압축률도 극적으로 높아진다.
행 지향 (SQLite/PostgreSQL):
┌──────┬─────────┬─────────┬──────────┐
│ id │ name │ country │ revenue │ -- 행 1의 모든 컬럼
├──────┼─────────┼─────────┼──────────┤
│ id │ name │ country │ revenue │ -- 행 2의 모든 컬럼
└──────┴─────────┴─────────┴──────────┘
↑ SELECT country, SUM(revenue) 시에도
id, name 컬럼 데이터를 불필요하게 읽음
컬럼 지향 (DuckDB):
┌────────────────────────┐
│ id: 1, 2, 3, ... │ -- 컬럼 블록 1 (스킵)
├────────────────────────┤
│ name: A, B, C, ... │ -- 컬럼 블록 2 (스킵)
├────────────────────────┤
│ country: KR, US, ... │ -- 컬럼 블록 3 (스캔!)
├────────────────────────┤
│ revenue: 100, 200, ... │ -- 컬럼 블록 4 (스캔!)
└────────────────────────┘
↑ 필요한 컬럼만 디스크에서 읽음
CPU 캐시 효율 극대화
3. 벡터화 실행 엔진
DuckDB의 성능을 결정짓는 핵심 메커니즘은 벡터화 실행 엔진(Vectorized Execution Engine)이다. 전통적인 Volcano(iterator) 모델과의 차이를 이해하면 DuckDB가 왜 빠른지 명확히 알 수 있다.
Volcano 모델 vs 벡터화 모델
전통적인 Volcano 모델에서는 쿼리 트리의 각 연산자가 한 번에 한 행씩 상위 연산자에게 전달한다. 행 하나를 처리할 때마다 가상 함수 호출과 분기 예측 실패가 발생하여, 실제 연산보다 함수 호출 오버헤드가 더 커지는 문제가 있다.
벡터화 모델은 한 번에 1,024~2,048개의 값(벡터)을 묶어서 처리한다. 함수 호출 오버헤드가 벡터 크기만큼 분산(amortize)되고, CPU의 SIMD 명령어와 캐시 라인을 최대한 활용할 수 있다.
┌─────────────────────────────────────────────────────┐
│ DuckDB 벡터화 실행 엔진 │
├─────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ SQL Parser │ SQL 텍스트 → AST │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Binder │ 테이블/컬럼 이름 해석 │
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Query Optimizer │ 조인 순서, 필터 푸시다운 │
│ │ (Cost-based) │ 서브쿼리 플래트닝 │
│ └──────┬──────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Physical Plan (Pipeline-based) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────────┐ │ │
│ │ │Scan │→ │Filter│→ │HashAggr │ │ │
│ │ │(Vec) │ │(Vec) │ │ (Vec) │ │ │
│ │ └──────┘ └──────┘ └──────────┘ │ │
│ │ ↑ 벡터(1024행) 단위 데이터 전달 │ │
│ └─────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ Morsel-Driven Parallelism │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │Thread 1│ │Thread 2│ │Thread N│ │ │
│ │ │Morsel A│ │Morsel B│ │Morsel C│ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ 각 스레드가 독립적 데이터 청크 처리 │ │
│ └─────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Result │ 벡터 → DataFrame / Arrow Table │
│ └─────────────┘ │
└─────────────────────────────────────────────────────┘
모젤 기반 병렬 처리(Morsel-Driven Parallelism)
DuckDB는 테이블 데이터를 "모젤(Morsel)"이라는 청크(약 10만 행 단위)로 분할한다. 각 워커 스레드가 독립적으로 하나의 모젤을 가져가 처리하고, 처리가 끝나면 다음 모젤을 가져간다. 이 work-stealing 방식은 스레드 간 부하를 자연스럽게 균형화하며, 기존 파티션 기반 병렬 처리보다 훨씬 유연하다.
4. 설치와 환경 구성
DuckDB의 가장 큰 장점 중 하나는 설치의 간편함이다. 외부 의존성이 전혀 없으며, 플랫폼별로 바이너리가 제공된다.
CLI 설치
# macOS (Homebrew)
brew install duckdb
# Linux (apt)
sudo apt-get install duckdb
# 또는 직접 바이너리 다운로드
wget https://github.com/duckdb/duckdb/releases/latest/download/duckdb_cli-linux-amd64.zip
unzip duckdb_cli-linux-amd64.zip
chmod +x duckdb
sudo mv duckdb /usr/local/bin/
# 버전 확인
duckdb --version
Python 패키지 설치
# pip
pip install duckdb
# conda
conda install -c conda-forge python-duckdb
# 특정 버전 설치 (예: 1.2.x)
pip install duckdb==1.2.1
기본 사용법 확인
-- DuckDB CLI 실행
-- 인메모리 모드 (데이터는 세션 종료 시 삭제)
duckdb
-- 파일 기반 영구 저장 모드
duckdb my_analytics.db
-- 기본 쿼리 테스트
SELECT version();
-- v1.2.1
-- 시스템 정보 확인
SELECT * FROM duckdb_settings()
WHERE name IN ('threads', 'memory_limit', 'temp_directory');
5. SQL 분석 쿼리 실전
DuckDB는 PostgreSQL 호환 SQL을 지원하며, 분석에 특화된 다양한 SQL 확장 기능을 제공한다.
파일 직접 쿼리
DuckDB의 가장 강력한 기능 중 하나는 CREATE TABLE 없이 파일을 바로 쿼리할 수 있다는 것이다.
-- CSV 파일 직접 쿼리
SELECT
product_category,
COUNT(*) AS order_count,
ROUND(AVG(amount), 2) AS avg_amount,
SUM(amount) AS total_revenue
FROM read_csv('sales_2025.csv', auto_detect=true)
WHERE order_date >= '2025-01-01'
GROUP BY product_category
ORDER BY total_revenue DESC
LIMIT 10;
-- 여러 CSV 파일을 글로브 패턴으로 한번에 쿼리
SELECT
filename,
COUNT(*) AS row_count
FROM read_csv('logs/access_*.csv', auto_detect=true, filename=true)
GROUP BY filename
ORDER BY row_count DESC;
-- JSON 파일 쿼리
SELECT
json_extract_string(data, '$.user.name') AS user_name,
json_extract(data, '$.events') AS events
FROM read_json('user_activity.json', auto_detect=true);
-- Parquet 파일 쿼리 (가장 빠른 포맷)
SELECT
region,
YEAR(event_timestamp) AS event_year,
COUNT(*) AS event_count
FROM read_parquet('events/*.parquet')
GROUP BY region, event_year
ORDER BY event_count DESC;
Window 함수 활용
-- 매출 데이터에서 이동 평균과 순위 계산
WITH daily_sales AS (
SELECT
sale_date,
product_id,
SUM(amount) AS daily_amount
FROM read_parquet('sales_data.parquet')
GROUP BY sale_date, product_id
)
SELECT
sale_date,
product_id,
daily_amount,
-- 7일 이동 평균
ROUND(AVG(daily_amount) OVER (
PARTITION BY product_id
ORDER BY sale_date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
), 2) AS moving_avg_7d,
-- 제품별 매출 순위 (일별)
RANK() OVER (
PARTITION BY sale_date
ORDER BY daily_amount DESC
) AS daily_rank,
-- 전일 대비 증감률
ROUND(
(daily_amount - LAG(daily_amount) OVER (
PARTITION BY product_id ORDER BY sale_date
)) * 100.0 / NULLIF(LAG(daily_amount) OVER (
PARTITION BY product_id ORDER BY sale_date
), 0), 2
) AS pct_change
FROM daily_sales
ORDER BY sale_date DESC, daily_rank
LIMIT 20;
PIVOT과 UNPIVOT
-- 월별 카테고리별 매출 피벗 테이블
PIVOT (
SELECT
MONTHNAME(order_date) AS month,
category,
SUM(revenue) AS total
FROM read_parquet('orders.parquet')
WHERE YEAR(order_date) = 2025
GROUP BY month, category
)
ON category
USING SUM(total)
GROUP BY month
ORDER BY month;
6. Python 통합
DuckDB의 Python 통합은 단순한 DB 드라이버를 넘어, Pandas와 Polars DataFrame을 직접 SQL로 쿼리하는 독창적인 기능을 제공한다.
Pandas DataFrame 직접 쿼리
import duckdb
import pandas as pd
# Pandas DataFrame 생성
sales_df = pd.DataFrame({
'product': ['A', 'B', 'C', 'A', 'B', 'C'] * 1000,
'region': ['Seoul', 'Busan', 'Seoul', 'Busan', 'Seoul', 'Busan'] * 1000,
'amount': [100, 200, 150, 300, 250, 180] * 1000,
'date': pd.date_range('2025-01-01', periods=6000, freq='h')
})
# DataFrame을 바로 SQL로 쿼리 (변수명이 테이블명)
result = duckdb.sql("""
SELECT
product,
region,
COUNT(*) AS order_count,
ROUND(AVG(amount), 2) AS avg_amount,
SUM(amount) AS total_revenue,
MIN(date) AS first_order,
MAX(date) AS last_order
FROM sales_df
GROUP BY product, region
ORDER BY total_revenue DESC
""").df() # 결과를 다시 Pandas DataFrame으로 반환
print(result)
# 대용량 Parquet 파일과 DataFrame 조인
products_df = pd.DataFrame({
'product': ['A', 'B', 'C'],
'category': ['Electronics', 'Clothing', 'Food'],
'margin_pct': [0.15, 0.40, 0.25]
})
enriched = duckdb.sql("""
SELECT
s.product,
p.category,
SUM(s.amount) AS revenue,
ROUND(SUM(s.amount) * p.margin_pct, 2) AS estimated_profit
FROM sales_df s
JOIN products_df p ON s.product = p.product
GROUP BY s.product, p.category, p.margin_pct
ORDER BY estimated_profit DESC
""").df()
print(enriched)
영구 데이터베이스와 Relation API
import duckdb
# 영구 데이터베이스 연결
con = duckdb.connect('analytics.db')
# 테이블 생성 및 Parquet에서 데이터 로드
con.execute("""
CREATE TABLE IF NOT EXISTS events AS
SELECT * FROM read_parquet('raw_events/*.parquet');
""")
# Relation API (메서드 체이닝)
result = (
con.table('events')
.filter("event_type = 'purchase'")
.aggregate("product_id, COUNT(*) AS cnt, SUM(amount) AS total")
.order("total DESC")
.limit(10)
.df()
)
# 결과를 Parquet로 내보내기
con.execute("""
COPY (
SELECT product_id, COUNT(*) AS cnt, SUM(amount) AS total
FROM events
WHERE event_type = 'purchase'
GROUP BY product_id
ORDER BY total DESC
) TO 'top_products.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
""")
con.close()
Apache Arrow 연동
import duckdb
import pyarrow as pa
import pyarrow.parquet as pq
# Arrow Table을 직접 쿼리
arrow_table = pq.read_table('large_dataset.parquet')
result = duckdb.sql("""
SELECT
category,
COUNT(*) AS cnt,
APPROX_QUANTILE(value, 0.95) AS p95
FROM arrow_table
GROUP BY category
""").arrow() # 결과를 Arrow Table로 반환 (제로카피)
# Arrow의 제로카피 전달로 메모리 효율 극대화
print(f"Result rows: {result.num_rows}")
7. 데이터 레이크 통합 (Parquet, S3, Delta Lake)
DuckDB가 데이터 엔지니어링 세계에서 폭발적으로 성장한 핵심 이유 중 하나는, 복잡한 인프라 없이 데이터 레이크를 직접 쿼리할 수 있다는 점이다.
S3 Parquet 파일 직접 쿼리
-- httpfs 확장 설치 및 로드
INSTALL httpfs;
LOAD httpfs;
-- AWS S3 인증 설정 (Secrets 방식 - 권장)
CREATE SECRET my_s3_secret (
TYPE S3,
KEY_ID 'AKIAIOSFODNN7EXAMPLE',
SECRET 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
REGION 'ap-northeast-2'
);
-- S3의 Parquet 파일 직접 쿼리
SELECT
event_date,
user_segment,
COUNT(DISTINCT user_id) AS dau,
COUNT(*) AS total_events,
ROUND(AVG(session_duration_sec), 1) AS avg_session_sec
FROM read_parquet('s3://my-data-lake/events/year=2025/month=12/*.parquet')
GROUP BY event_date, user_segment
ORDER BY event_date DESC, dau DESC;
-- 하이브 파티셔닝된 데이터 쿼리 (파티션 프루닝 자동 적용)
SELECT *
FROM read_parquet(
's3://my-data-lake/events/**/*.parquet',
hive_partitioning=true
)
WHERE year = 2025 AND month = 12
AND event_type = 'purchase';
-- 쿼리 결과를 S3에 Parquet로 다시 저장
COPY (
SELECT
date_trunc('hour', event_timestamp) AS hour,
COUNT(*) AS event_count,
COUNT(DISTINCT user_id) AS unique_users
FROM read_parquet('s3://my-data-lake/raw_events/**/*.parquet',
hive_partitioning=true)
WHERE year = 2025
GROUP BY hour
)
TO 's3://my-data-lake/aggregated/hourly_summary.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD);
Delta Lake 읽기
-- Delta Lake 확장 설치
INSTALL delta;
LOAD delta;
-- Delta Lake 테이블 읽기
SELECT *
FROM delta_scan('s3://my-data-lake/delta_tables/user_events/')
WHERE event_date >= '2025-12-01'
LIMIT 100;
GCS(Google Cloud Storage) 연동
-- GCS 인증 설정
CREATE SECRET gcs_secret (
TYPE GCS,
KEY_ID 'my-gcs-key-id',
SECRET 'my-gcs-secret'
);
-- GCS Parquet 파일 쿼리
SELECT COUNT(*), SUM(revenue)
FROM read_parquet('gs://my-bucket/analytics/sales_*.parquet');
8. 분석 도구 비교
DuckDB의 위치를 정확히 이해하려면 경쟁 도구들과의 비교가 필수적이다.
| 비교 항목 | DuckDB | SQLite | ClickHouse | BigQuery |
|---|---|---|---|---|
| 아키텍처 | 인프로세스, 컬럼 지향 | 인프로세스, 행 지향 | 클라이언트-서버, 컬럼 지향 | 서버리스, 컬럼 지향 |
| 최적 워크로드 | 로컬/임베디드 OLAP | 로컬/임베디드 OLTP | 분산 실시간 OLAP | 대규모 클라우드 분석 |
| 설치 난이도 | pip install 한 줄 | OS 기본 탑재 | 서버 설치 필요 | 설치 불필요 (SaaS) |
| 동시 사용자 | 단일~소수 | 단일~소수 | 수백~수천 | 수천+ |
| 최대 데이터 규모 | 수십 GB~수백 GB (싱글 노드) | 수 GB | TB~PB (클러스터) | PB+ |
| 쓰기 동시성 | 단일 Writer | 단일 Writer (WAL 모드 시 Reader 병행) | 다중 Writer | 다중 Writer |
| Parquet 네이티브 지원 | 내장 | 미지원 | MATERIALIZED VIEW 통해 가능 | 네이티브 |
| S3 직접 쿼리 | httpfs 확장 | 미지원 | s3 테이블 함수 | 네이티브 (External Table) |
| 비용 모델 | 무료 (오픈소스, MIT) | 무료 (퍼블릭 도메인) | 무료 (오픈소스) / 클라우드 유료 | 쿼리 과금 ($5/TB 스캔) |
| Python 통합 | DataFrame 직접 SQL 쿼리 | DB-API 기본 | clickhouse-driver | google-cloud-bigquery |
| 실시간 수집 | 부적합 | 부적합 | 최적화 (MergeTree) | Streaming Insert |
| 트랜잭션 | ACID (싱글 Writer) | ACID | 제한적 ACID | 완전 ACID |
사용 시나리오별 추천
- 노트북에서 Parquet/CSV 분석 → DuckDB
- 모바일 앱의 로컬 데이터 저장 → SQLite
- 수십억 행 실시간 대시보드 → ClickHouse
- 팀 전체가 공유하는 데이터 웨어하우스 → BigQuery
- 개발/테스트 환경에서 빠른 프로토타이핑 → DuckDB
- CI/CD 파이프라인의 데이터 품질 검증 → DuckDB
9. 확장 기능(Extensions) 활용
DuckDB는 핵심 기능을 가볍게 유지하면서, 확장(Extension)을 통해 필요한 기능을 선택적으로 추가하는 설계를 따른다.
주요 확장 목록
-- 설치된 확장 확인
SELECT * FROM duckdb_extensions() WHERE installed = true;
-- 핵심 확장 설치
INSTALL httpfs; -- S3/HTTP 원격 파일 접근
INSTALL json; -- JSON 파싱 (자동 로드)
INSTALL parquet; -- Parquet 지원 (자동 로드)
INSTALL spatial; -- 지리공간 데이터 처리 (ST_* 함수)
INSTALL icu; -- 국제화 정렬/비교
INSTALL fts; -- Full-Text Search (전문 검색)
INSTALL delta; -- Delta Lake 테이블 읽기
INSTALL excel; -- Excel 파일 읽기/쓰기
INSTALL sqlite; -- SQLite 데이터베이스 직접 읽기
INSTALL postgres; -- PostgreSQL 직접 쿼리 (FDW)
INSTALL mysql; -- MySQL 직접 쿼리
-- 확장 로드
LOAD httpfs;
LOAD spatial;
Spatial 확장 실전 예제
INSTALL spatial;
LOAD spatial;
-- 서울 지역 매장 반경 1km 이내 주문 분석
SELECT
store_name,
COUNT(*) AS nearby_orders,
SUM(order_amount) AS nearby_revenue
FROM orders o
JOIN stores s ON o.store_id = s.id
WHERE ST_DWithin(
ST_Point(o.longitude, o.latitude),
ST_Point(s.longitude, s.latitude),
0.01 -- 약 1km (WGS84 근사)
)
GROUP BY store_name
ORDER BY nearby_revenue DESC;
PostgreSQL 직접 쿼리 (postgres 확장)
INSTALL postgres;
LOAD postgres;
-- PostgreSQL에 직접 연결하여 쿼리
ATTACH 'dbname=mydb user=analyst host=pg-server.internal' AS pg (TYPE POSTGRES);
-- PostgreSQL 테이블과 로컬 Parquet 파일 조인
SELECT
p.customer_id,
p.customer_name,
l.total_purchases,
l.last_purchase_date
FROM pg.public.customers p
JOIN (
SELECT
customer_id,
COUNT(*) AS total_purchases,
MAX(purchase_date) AS last_purchase_date
FROM read_parquet('s3://data-lake/purchases/**/*.parquet')
GROUP BY customer_id
) l ON p.customer_id = l.customer_id
WHERE l.total_purchases > 100
ORDER BY l.total_purchases DESC;
10. MotherDuck: 클라우드 DuckDB
MotherDuck는 DuckDB의 공동 창시자가 설립한 회사에서 운영하는 서버리스 클라우드 분석 서비스다. DuckDB의 강점을 유지하면서 클라우드의 확장성과 협업 기능을 추가한다.
핵심 특징
- 하이브리드 쿼리 실행: 쿼리의 일부는 로컬(클라이언트)에서, 일부는 클라우드에서 실행하여 네트워크 전송을 최소화
- 서버리스 인프라: Duckling이라는 인스턴스 크기(pulse, standard, jumbo, mega, giga)를 워크로드에 맞게 선택
- 데이터 공유: 팀 멤버 간 데이터베이스를 URL로 공유
- 영구 스토리지: 데이터를 클라우드에 안전하게 저장하고 자동 최적화
- MCP 서버 연동: Warp, JetBrains IDE 등 다양한 개발 도구와 통합
MotherDuck 연결
import duckdb
# MotherDuck 토큰으로 연결
con = duckdb.connect('md:my_database?motherduck_token=eyJhb...')
# 로컬 데이터와 클라우드 데이터를 하이브리드 쿼리
con.execute("""
SELECT
cloud_table.user_id,
local_file.purchase_amount
FROM my_database.users AS cloud_table
JOIN read_parquet('local_data/purchases.parquet') AS local_file
ON cloud_table.user_id = local_file.user_id
WHERE cloud_table.signup_date >= '2025-01-01'
""")
result = con.fetchdf()
print(result)
11. 성능 최적화 팁
DuckDB는 기본 설정에서도 우수한 성능을 제공하지만, 워크로드 특성에 맞게 튜닝하면 추가적인 성능 향상을 얻을 수 있다.
메모리와 스레드 설정
-- 현재 설정 확인
SELECT name, value, description
FROM duckdb_settings()
WHERE name IN ('memory_limit', 'threads', 'temp_directory');
-- 메모리 한도 설정 (기본: 시스템 RAM의 약 80%)
SET memory_limit = '16GB';
-- 스레드 수 조정 (기본: CPU 코어 수)
-- 다른 작업과 병행 시 줄여서 사용
SET threads = 4;
-- 디스크 스필 디렉토리 설정 (메모리 초과 시 사용)
SET temp_directory = '/fast-ssd/duckdb_temp';
쿼리 최적화 원칙
-- 1. EXPLAIN으로 쿼리 플랜 확인
EXPLAIN
SELECT region, SUM(amount) AS total
FROM read_parquet('large_sales.parquet')
WHERE sale_date >= '2025-01-01'
GROUP BY region;
-- 2. EXPLAIN ANALYZE로 실제 실행 프로파일링
EXPLAIN ANALYZE
SELECT region, SUM(amount) AS total
FROM read_parquet('large_sales.parquet')
WHERE sale_date >= '2025-01-01'
GROUP BY region;
-- 3. Parquet 필터 푸시다운 활용
-- WHERE 조건이 Parquet 파일의 Row Group 통계에 반영되어
-- 불필요한 데이터를 읽지 않음
SELECT COUNT(*)
FROM read_parquet('partitioned_data/**/*.parquet',
hive_partitioning=true)
WHERE year = 2025 AND month = 12;
-- → year != 2025인 파일은 아예 열지 않음 (파티션 프루닝)
-- 4. 적절한 데이터 타입 사용
-- VARCHAR 대신 ENUM으로 카디널리티 낮은 컬럼 최적화
CREATE TYPE status_enum AS ENUM ('active', 'inactive', 'pending');
-- 5. 커넥션 재사용
-- 빈번한 connect/disconnect는 캐시 손실을 초래
-- 하나의 연결을 유지하며 쿼리 실행
Parquet 파일 최적화
-- 최적화된 Parquet 파일 생성
-- ZSTD 압축 + 적절한 Row Group 크기
COPY (
SELECT * FROM large_table
ORDER BY partition_key, timestamp -- 정렬하여 압축률 극대화
) TO 'optimized_output.parquet'
(FORMAT PARQUET,
COMPRESSION ZSTD,
ROW_GROUP_SIZE 122880); -- 약 120K 행/그룹
-- 파티셔닝된 Parquet 내보내기
COPY (SELECT * FROM events)
TO 'output_dir' (FORMAT PARQUET,
PARTITION_BY (year, month),
COMPRESSION ZSTD);
12. 트러블슈팅
흔한 오류와 해결법
메모리 부족 오류(Out of Memory)는 가장 빈번하게 발생하는 문제다.
-- 증상: "Out of Memory Error: could not allocate block of size ..."
-- 해결 1: 메모리 한도 늘리기
SET memory_limit = '32GB';
-- 해결 2: 디스크 스필 활성화
SET temp_directory = '/tmp/duckdb_spill';
-- 해결 3: 쿼리를 청크 단위로 분할
-- 전체 테이블 집계 대신 파티션별로 처리
CREATE TABLE monthly_agg AS
SELECT * FROM (
SELECT month, SUM(amount) AS total
FROM read_parquet('data/year=2025/month=01/*.parquet')
GROUP BY month
UNION ALL
SELECT month, SUM(amount) AS total
FROM read_parquet('data/year=2025/month=02/*.parquet')
GROUP BY month
-- ... 반복
);
S3 접속 오류도 자주 발생한다.
-- 증상: "HTTP Error: Unable to connect to URL"
-- 해결 1: 인증 정보 확인
SELECT * FROM duckdb_secrets();
-- 해결 2: 리전 명시
CREATE OR REPLACE SECRET (
TYPE S3,
REGION 'ap-northeast-2',
KEY_ID 'AKIA...',
SECRET '...'
);
-- 해결 3: 엔드포인트 커스텀 (MinIO, R2 등)
CREATE SECRET (
TYPE S3,
KEY_ID 'minioadmin',
SECRET 'minioadmin',
ENDPOINT 'localhost:9000',
USE_SSL false,
URL_STYLE 'path'
);
파일 잠금 충돌은 멀티프로세스 환경에서 발생한다.
-- 증상: "Could not set lock on file ... database is locked"
-- 원인: 다른 프로세스가 동일 DB 파일을 열고 있음
-- DuckDB는 단일 Writer만 허용
-- 해결 1: access_mode를 read_only로 설정 (읽기만 필요한 경우)
-- Python에서:
-- con = duckdb.connect('my.db', read_only=True)
-- 해결 2: 인메모리 모드에서 Parquet 직접 쿼리
-- DB 파일 잠금 자체를 회피
-- con = duckdb.connect() -- 인메모리
-- con.sql("SELECT * FROM read_parquet('data.parquet')")
13. 프로덕션 운영 패턴
DuckDB를 프로덕션에서 안전하게 활용하기 위한 패턴들이다.
패턴 1: ETL 파이프라인의 변환 계층
import duckdb
from datetime import datetime
def daily_aggregation_job():
"""DuckDB를 ETL의 T(Transform) 단계로 활용"""
con = duckdb.connect()
today = datetime.now().strftime('%Y-%m-%d')
# S3에서 원본 데이터 읽기 → 변환 → S3에 결과 저장
con.execute(f"""
INSTALL httpfs; LOAD httpfs;
CREATE SECRET (TYPE S3, REGION 'ap-northeast-2');
COPY (
SELECT
date_trunc('hour', event_ts) AS hour,
event_type,
COUNT(*) AS cnt,
COUNT(DISTINCT user_id) AS unique_users,
APPROX_COUNT_DISTINCT(session_id) AS approx_sessions,
PERCENTILE_CONT(0.50) WITHIN GROUP
(ORDER BY duration_ms) AS p50_duration,
PERCENTILE_CONT(0.99) WITHIN GROUP
(ORDER BY duration_ms) AS p99_duration
FROM read_parquet(
's3://raw-data/events/dt={today}/**/*.parquet'
)
GROUP BY hour, event_type
)
TO 's3://processed-data/hourly_agg/dt={today}/result.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD);
""")
con.close()
print(f"Daily aggregation completed for {today}")
패턴 2: CI/CD 데이터 품질 검증
import duckdb
import sys
def validate_data_quality(parquet_path: str) -> bool:
"""CI/CD 파이프라인에서 데이터 품질을 자동 검증"""
con = duckdb.connect()
checks_passed = True
# 검증 1: 행 수 최소 기준
row_count = con.execute(f"""
SELECT COUNT(*) FROM read_parquet('{parquet_path}')
""").fetchone()[0]
if row_count < 1000:
print(f"FAIL: Row count {row_count} below minimum 1000")
checks_passed = False
# 검증 2: NULL 비율 체크
null_check = con.execute(f"""
SELECT
ROUND(COUNT(*) FILTER (WHERE user_id IS NULL)
* 100.0 / COUNT(*), 2) AS null_pct
FROM read_parquet('{parquet_path}')
""").fetchone()[0]
if null_check > 5.0:
print(f"FAIL: user_id NULL ratio {null_check}% exceeds 5%")
checks_passed = False
# 검증 3: 값 범위 체크
range_check = con.execute(f"""
SELECT
MIN(amount) AS min_amt,
MAX(amount) AS max_amt
FROM read_parquet('{parquet_path}')
""").fetchone()
if range_check[0] < 0:
print(f"FAIL: Negative amount detected: {range_check[0]}")
checks_passed = False
con.close()
return checks_passed
if __name__ == '__main__':
path = sys.argv[1]
if not validate_data_quality(path):
sys.exit(1)
print("All data quality checks passed")
패턴 3: 마이크로서비스 내 임베디드 분석
DuckDB를 API 서버 내부에 임베디드하여, 외부 분석 서비스 호출 없이 빠른 집계 응답을 제공할 수 있다.
from fastapi import FastAPI
import duckdb
app = FastAPI()
# 애플리케이션 시작 시 한번만 연결 (커넥션 재사용)
analytics_con = duckdb.connect('analytics_cache.db', read_only=True)
@app.get("/api/dashboard/summary")
async def dashboard_summary(date_from: str, date_to: str):
result = analytics_con.execute("""
SELECT
product_category,
COUNT(*) AS orders,
ROUND(SUM(revenue), 2) AS total_revenue,
ROUND(AVG(revenue), 2) AS avg_order_value
FROM sales
WHERE order_date BETWEEN ? AND ?
GROUP BY product_category
ORDER BY total_revenue DESC
""", [date_from, date_to]).df()
return result.to_dict(orient='records')
14. 실패 사례와 대응
DuckDB를 실무에 도입하면서 흔히 겪는 실패 사례와 그 대응 방법을 정리한다.
실패 1: OLTP 워크로드에 DuckDB 적용
증상: 사용자 인증, 주문 처리 등 행 단위 읽기/쓰기가 빈번한 서비스 백엔드에 DuckDB를 적용했더니, 단일 행 조회가 비정상적으로 느리고 동시 쓰기 시 잠금 충돌이 빈번하게 발생했다.
원인: DuckDB는 컬럼 지향 OLAP 엔진이다. 단일 행 포인트 쿼리에는 컬럼 스토리지의 오버헤드가 오히려 독이 된다. 또한 단일 Writer 제약 때문에 동시 쓰기는 근본적으로 불가하다.
대응: OLTP 워크로드는 PostgreSQL이나 MySQL을 사용하고, DuckDB는 분석 전용 레이어로 분리한다. OLTP DB의 데이터를 주기적으로 Parquet로 내보내고, DuckDB로 분석하는 이원화 구조를 채택한다.
실패 2: 메모리보다 큰 데이터셋을 무방비로 처리
증상: 32GB RAM 서버에서 100GB Parquet 파일을 GROUP BY 집계하자 프로세스가 OOM으로 kill되었다.
원인: DuckDB는 디스크 스필(spill to disk) 기능을 제공하지만, 기본 temp_directory가 설정되어 있지 않거나 임시 디스크 공간이 부족하면 메모리만으로 처리하려고 시도한다.
대응: temp_directory를 충분한 공간이 있는 SSD 경로로 반드시 설정한다. 또한 memory_limit를 시스템 RAM보다 낮게(약 70~80%) 설정하여 OS와 다른 프로세스의 메모리를 보호한다.
실패 3: 동시 접속 서비스에 단일 DB 파일 공유
증상: 여러 마이크로서비스가 동일한 DuckDB 파일에 동시에 접근하자 "database is locked" 오류가 빈발했다.
원인: DuckDB는 하나의 프로세스만 Writer로 접근할 수 있다. 여러 프로세스가 동시에 쓰기를 시도하면 잠금 충돌이 발생한다.
대응: 쓰기 작업은 단일 프로세스로 제한하고, 읽기 전용 접근은 read_only 모드를 사용한다. 또는 각 서비스가 인메모리 모드에서 Parquet 파일을 직접 쿼리하는 구조로 전환한다. 다중 사용자 동시 접속이 필수라면 MotherDuck이나 ClickHouse로 전환을 검토한다.
실패 4: 실시간 스트리밍 데이터 수집에 사용
증상: Kafka에서 초당 수만 건의 이벤트를 DuckDB에 실시간 INSERT하자 쓰기 성능이 급격히 저하되었다.
원인: DuckDB는 분석 쿼리(읽기)에 최적화되어 있으며, 행 단위 실시간 INSERT에는 적합하지 않다. 컬럼 스토리지는 배치 쓰기에서 효율적이지만 단건 INSERT에는 오버헤드가 크다.
대응: 실시간 수집은 Kafka + ClickHouse 또는 Kafka + S3(Parquet)로 처리하고, DuckDB는 적재 완료된 Parquet 파일을 분석하는 용도로만 사용한다.
15. DuckDB를 쓰면 안 되는 경우 정리
DuckDB가 부적합한 상황을 명확히 인식하는 것이 올바른 도구 선택의 첫걸음이다.
- 고동시성 OLTP: 수백 명 이상의 동시 사용자가 행 단위로 읽고 쓰는 서비스 → PostgreSQL, MySQL
- 실시간 스트리밍 수집: 초당 수만~수십만 건의 실시간 INSERT → ClickHouse, Apache Kafka + Flink
- 페타바이트급 분산 처리: 단일 노드로 감당할 수 없는 규모 → Spark, Trino, BigQuery
- 다중 Writer 필수: 여러 프로세스가 동시에 데이터를 갱신해야 하는 경우 → PostgreSQL
- ACID 트랜잭션이 핵심인 결제/금융 시스템: → PostgreSQL, CockroachDB
- 서브밀리초 포인트 쿼리가 필요한 캐시 레이어: → Redis, Memcached
16. 참고자료
- DuckDB 공식 문서 - 설치, API, SQL 레퍼런스 등 모든 공식 가이드
- DuckDB In-Depth: How It Works and What Makes It Fast (endjin) - 벡터화 실행 엔진과 아키텍처 심층 분석
- MotherDuck: Architecture and Capabilities - MotherDuck 하이브리드 쿼리 아키텍처
- DuckDB S3 Parquet Import 공식 가이드 - S3 연동 설정 및 사용법
- DuckDB Extensions 공식 문서 - 확장 설치 및 관리 가이드
- ClickHouse vs DuckDB 2026 비교 (Tasrie IT) - OLAP 데이터베이스 벤치마크 비교
- DuckDB Python API 공식 문서 - Python 클라이언트 상세 가이드
- DuckDB Performance Guide - 성능 최적화 공식 가이드
- 15+ Companies Using DuckDB in Production (MotherDuck) - 프로덕션 사례 모음
- DuckDB: An Architectural Deep Dive (ThinhDA) - 인프로세스 OLAP 엔진 아키텍처 분석