들어가며 — HN 단골손님이 된 오리
Hacker News에는 주기적으로 1면에 올라오는 단골 주제들이 있습니다. SQLite 예찬, Postgres로 충분하다는 글, 그리고 DuckDB 활용기입니다. "노트북에서 수억 행을 몇 초에 집계했다", "데이터 웨어하우스 없이 분석 파이프라인을 다 끝냈다" 같은 제목이 올라올 때마다 댓글란은 비슷한 결론으로 수렴합니다. 우리 데이터는 생각보다 작고, DuckDB는 생각보다 빠르다는 것입니다.
2026년의 맥락에서는 또 다른 이유가 더해졌습니다. AI 코딩 에이전트가 보편화되면서 "에이전트가 데이터를 직접 탐색하는" 워크플로가 흔해졌는데, 서버 설치도 인증 설정도 없이 파일 하나로 SQL 분석 환경이 만들어지는 DuckDB는 에이전트 친화적 도구의 대표 사례가 됐습니다. 클라우드 비용 절감 압박과 빅테크 피로감이라는 시대 분위기도 "내 노트북에서 끝내는 분석"의 매력을 키우고 있습니다.
이 글에서는 DuckDB가 사랑받는 구조적 이유부터 실전 쿼리 패턴, 데이터 엔지니어링에서의 위치, 그리고 한계까지 실무 관점에서 정리합니다.
DuckDB란 무엇인가 — OLAP의 SQLite
DuckDB를 한 문장으로 정의하면 "임베디드 분석 데이터베이스"입니다. SQLite가 애플리케이션 안에 라이브러리로 박혀 트랜잭션 처리를 담당하듯, DuckDB는 프로세스 안에 박혀 분석 쿼리를 담당합니다. 비교 축을 정리하면 이렇습니다.
- SQLite: 임베디드, 행 지향, OLTP(많은 작은 읽기쓰기)에 최적
- DuckDB: 임베디드, 컬럼 지향, OLAP(소수의 무거운 집계)에 최적
- Postgres와 MySQL: 클라이언트 서버, 행 지향, OLTP
- ClickHouse와 BigQuery: 클라이언트 서버(또는 분산), 컬럼 지향, OLAP
즉 DuckDB는 "서버 없는 ClickHouse"이자 "분석용 SQLite"라는 비어 있던 사분면을 정확히 채운 프로젝트입니다. 네덜란드 CWI 연구소의 데이터베이스 연구 그룹에서 출발해 2024년 1.0 버전에 도달했고, 이후 안정성과 생태계가 빠르게 성숙했습니다.
아키텍처 — 왜 빠른가
DuckDB의 속도는 세 가지 설계에서 나옵니다.
+--------------------------------------------------------------+
| 사용자 프로세스 (Python, CLI, Node, JVM, ...) |
| |
| +------------------------------------------------------+ |
| | DuckDB (라이브러리로 임베드, 서버 없음) | |
| | | |
| | SQL 파서 / 옵티마이저 | |
| | | | |
| | v | |
| | 벡터화 실행 엔진 | |
| | - 한 번에 행 1개가 아니라 벡터(약 2048개 값) 처리 | |
| | - CPU 캐시 친화적, SIMD 활용 | |
| | - 모든 코어로 자동 병렬화 | |
| | | | |
| | v | |
| | 컬럼 지향 저장소 | |
| | - 단일 파일 DB (ACID, MVCC) | |
| | - 컬럼별 압축 (딕셔너리, RLE, 비트패킹) | |
| | - Parquet/CSV/JSON 직접 스캔 (가져오기 불필요) | |
| +------------------------------------------------------+ |
| |
| 메모리를 넘는 데이터는 디스크로 스필 (out-of-core 실행) |
+--------------------------------------------------------------+
첫째, 컬럼 지향 저장입니다. 분석 쿼리는 보통 수십 개 컬럼 중 두세 개만 읽습니다. 컬럼 단위로 저장하면 필요한 컬럼만 디스크에서 읽으면 되고, 같은 타입의 값이 모여 있어 압축 효율도 높아집니다.
둘째, 벡터화 실행입니다. 행을 하나씩 처리하는 전통적 볼케이노 모델 대신, 약 2048개 값의 벡터 단위로 연산자를 통과시킵니다. 함수 호출 오버헤드가 수천 분의 일로 줄고, CPU 캐시와 SIMD 명령을 효율적으로 활용합니다.
셋째, 단일 파일과 out-of-core 처리입니다. 데이터베이스 전체가 파일 하나이고, 메모리보다 큰 데이터는 자동으로 디스크에 중간 결과를 흘리며 처리합니다. "메모리에 안 들어가면 끝"인 pandas와의 결정적 차이입니다.
설치 없이 시작하기
DuckDB의 진입 장벽은 사실상 0입니다.
CLI 설치 (macOS)
brew install duckdb
인메모리 모드로 바로 실행
duckdb
파일 DB로 열기 (없으면 생성)
duckdb analytics.duckdb
Python: pip install duckdb 한 줄이면 끝
연결 없이 바로 쿼리 (인메모리)
duckdb.sql("SELECT 42 AS answer").show()
파일 DB 사용
con = duckdb.connect("analytics.duckdb")
con.sql("CREATE TABLE t AS SELECT * FROM range(1000000)")
서버 데몬도, 포트 설정도, 사용자 계정도 없습니다. 이 마찰 없음이 "일단 DuckDB로 열어보자"라는 습관을 만들고, 그 습관이 인기의 토대가 됐습니다.
파일 직접 쿼리 — DuckDB의 킬러 기능
DuckDB는 데이터를 "가져오기(import)" 하지 않고도 파일을 테이블처럼 직접 쿼리합니다.
-- CSV를 바로 쿼리 (스키마 자동 추론)
SELECT count(*) FROM 'events-2026-06.csv';
-- Parquet 여러 파일을 글롭 패턴으로 한 번에
SELECT user_id, sum(amount) AS total
FROM 'data/sales/*.parquet'
GROUP BY user_id
ORDER BY total DESC
LIMIT 10;
-- JSON Lines도 직접
SELECT payload.event_type, count(*)
FROM read_json_auto('logs/*.jsonl')
GROUP BY 1;
-- 파일 형식 간 변환도 SQL 한 줄
COPY (SELECT * FROM 'raw.csv') TO 'clean.parquet' (FORMAT parquet);
httpfs 확장을 올리면 원격 파일도 같은 문법으로 다룹니다.
INSTALL httpfs;
LOAD httpfs;
-- HTTP URL 직접 쿼리
SELECT * FROM 'https://example.com/open-data/stats.parquet' LIMIT 5;
-- S3 버킷의 Parquet을 노트북에서 바로 집계
SET s3_region = 'ap-northeast-2';
SELECT date_trunc('day', ts) AS d, count(*)
FROM 's3://my-bucket/events/year=2026/month=06/*.parquet'
GROUP BY d ORDER BY d;
Parquet의 컬럼 통계와 행 그룹 메타데이터를 활용해 필요한 부분만 읽으므로, 수십 GB 파일에서도 필터가 잘 걸리면 수 초 안에 답이 나옵니다. "데이터 레이크를 데이터 웨어하우스에 적재하지 않고 그 자리에서 쿼리한다"는 흐름의 중심에 이 기능이 있습니다.
실전 쿼리 예제 모음
분석 SQL에서 자주 쓰는 패턴 세 가지를 DuckDB 문법으로 정리합니다.
윈도우 함수 — 이동 평균과 순위
-- 일별 매출의 7일 이동 평균과 전일 대비 증감
WITH daily AS (
SELECT date_trunc('day', ts) AS d, sum(amount) AS revenue
FROM 'sales/*.parquet'
GROUP BY d
)
SELECT
d,
revenue,
avg(revenue) OVER (
ORDER BY d ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS ma7,
revenue - lag(revenue) OVER (ORDER BY d) AS diff_prev_day,
rank() OVER (ORDER BY revenue DESC) AS best_day_rank
FROM daily
ORDER BY d;
PIVOT — 행을 열로
DuckDB는 PIVOT을 전용 구문으로 지원합니다. 컬럼 값을 일일이 나열하지 않아도 됩니다.
-- 카테고리별 월 매출을 와이드 포맷으로
PIVOT (
SELECT category, strftime(ts, '%Y-%m') AS month, amount
FROM 'sales/*.parquet'
)
ON month
USING sum(amount)
GROUP BY category;
-- 반대 방향 UNPIVOT도 지원
UNPIVOT monthly_wide
ON COLUMNS(* EXCLUDE (category))
INTO NAME month VALUE amount;
ASOF JOIN — 시계열의 가장 가까운 과거 매칭
시세 데이터와 체결 데이터처럼 타임스탬프가 정확히 일치하지 않는 시계열을 붙일 때 진가를 발휘합니다.
-- 각 거래에 대해 "거래 시점 이전의 가장 최근 환율"을 매칭
SELECT t.trade_id, t.ts, t.amount_usd, r.rate AS usdkrw_at_trade
FROM trades t
ASOF JOIN fx_rates r
ON t.ts >= r.ts;
일반 조인으로 이걸 구현하려면 상관 서브쿼리나 윈도우 함수 곡예가 필요하지만, ASOF JOIN은 의도를 그대로 문법으로 표현하면서 실행 계획도 효율적입니다.
pandas와 Polars — 경쟁이 아니라 공생
DuckDB는 데이터프레임 라이브러리를 대체하기보다 보완합니다. 핵심은 Apache Arrow 기반의 zero-copy 연동입니다.
df = pd.read_parquet("events.parquet")
pandas DataFrame을 복사 없이 SQL로 쿼리 (변수명을 그대로 인식)
result = duckdb.sql("""
SELECT user_id, count(*) AS cnt
FROM df
WHERE event_type = 'purchase'
GROUP BY user_id
HAVING cnt >= 3
""").df() # 결과를 다시 pandas로
Polars와도 동일하게 왕복 가능
pdf = duckdb.sql("SELECT * FROM 'big.parquet'").pl() # Arrow 경유, 복사 최소화
실무 패턴은 대체로 이렇습니다. 무거운 스캔, 조인, 집계는 DuckDB가 디스크에서 직접 수행하고, 마지막 정제와 시각화 직전 처리만 데이터프레임으로 받습니다. "pandas 메모리 부족" 문제의 8할은 이 분업으로 사라집니다.
확장 생태계
DuckDB는 코어를 작게 유지하고 기능을 확장으로 분리합니다. INSTALL과 LOAD 두 명령이면 됩니다.
-- 공간 분석: GIS 함수와 지오메트리 타입
INSTALL spatial;
LOAD spatial;
SELECT ST_Distance(
ST_Point(126.9780, 37.5665), -- 서울
ST_Point(139.6917, 35.6895) -- 도쿄
) AS deg_distance;
-- 전문 검색: BM25 기반 인덱스
INSTALL fts;
LOAD fts;
PRAGMA create_fts_index('docs', 'doc_id', 'body');
SELECT doc_id, fts_main_docs.match_bm25(doc_id, 'duckdb embedded') AS score
FROM docs ORDER BY score DESC LIMIT 10;
이외에도 json(기본 내장 수준), httpfs, iceberg, delta, postgres 스캐너, sqlite 스캐너 등이 자주 쓰입니다. postgres 스캐너는 운영 DB 테이블을 DuckDB에서 직접 읽게 해줘서, "운영 DB 부하 없이 분석 복제본 만들기"의 간편한 대안이 됩니다.
데이터 엔지니어링에서의 위치 — 경량 ETL과 dbt
DuckDB는 "분석가의 장난감"을 넘어 파이프라인의 정식 부품이 됐습니다. 대표 패턴 두 가지를 소개합니다.
첫째, 경량 ETL 엔진입니다. 하루 수십 GB 이하의 배치라면, Spark 클러스터 대신 컨테이너 하나에서 DuckDB로 변환을 끝내는 구성이 비용과 운영 난이도 모두에서 압도적으로 단순합니다.
컨테이너 하나로 끝나는 일일 배치 ETL의 골격
con = duckdb.connect()
con.sql("INSTALL httpfs; LOAD httpfs;")
con.sql("""
COPY (
SELECT
cast(ts AS DATE) AS event_date,
user_id,
event_type,
amount
FROM 's3://raw-bucket/events/2026-06-11/*.jsonl'
WHERE event_type IN ('purchase', 'refund')
)
TO 's3://curated-bucket/events/dt=2026-06-11/data.parquet'
(FORMAT parquet, COMPRESSION zstd)
""")
둘째, dbt-duckdb입니다. dbt의 모델 관리, 테스트, 문서화를 그대로 쓰면서 실행 엔진만 DuckDB를 씁니다. 웨어하우스 비용 없이 SQL 변환 파이프라인의 모범 사례를 도입할 수 있어, 스타트업과 사이드 프로젝트에서 채택이 빠르게 늘었습니다. CI에서 dbt 모델을 통째로 돌려보는 테스트 환경으로도 탁월합니다.
MotherDuck — 하이브리드 실행이라는 절충
"노트북으로 부족해지면 어떡하나"에 대한 상업적 답이 MotherDuck입니다. DuckDB를 만든 팀과 협력하는 클라우드 서비스로, 로컬 DuckDB와 클라우드 DuckDB가 하나의 쿼리 안에서 협업하는 하이브리드 실행을 제공합니다. 로컬 파일과 클라우드 테이블을 조인하면, 옵티마이저가 어느 쪽에서 어떤 부분을 실행할지 나눠 처리하는 방식입니다.
-- 로컬 CSV와 MotherDuck 클라우드 테이블의 조인 (개념 예시)
ATTACH 'md:my_database';
SELECT l.campaign, m.user_segment, count(*)
FROM 'local_campaign.csv' l
JOIN my_database.users m USING (user_id)
GROUP BY 1, 2;
"임베디드로 시작해서 필요한 만큼만 클라우드로"라는 성장 경로가 생긴 셈입니다. 다만 벤더 종속이 다시 생기는 지점이므로, 코어 파이프라인은 표준 DuckDB와 Parquet에 두고 MotherDuck은 공유와 확장 용도로 한정하는 팀이 많습니다.
DuckLake와 레이크하우스 흐름
2025년 DuckDB 팀은 DuckLake라는 오픈 레이크하우스 포맷을 공개했습니다. Iceberg류 포맷이 메타데이터를 수많은 JSON과 Avro 파일로 관리하는 것과 달리, DuckLake는 카탈로그 메타데이터를 SQL 데이터베이스에 저장하고 데이터는 Parquet으로 두는 단순화를 택했습니다. "메타데이터도 결국 관계형 데이터"라는 발상입니다. 아직 초기이고 생태계 호환(특히 기존 Iceberg 도구들과의)이 관전 포인트지만, 단일 노드부터 시작하는 레이크하우스라는 방향성은 DuckDB의 철학과 일관됩니다. 기존 Iceberg와 Delta 테이블은 각각 확장으로 읽을 수 있으므로, 당장은 "읽기는 표준 포맷, 실험은 DuckLake" 정도의 거리감이 적절해 보입니다.
일상 레시피 모음 — 손에 익혀두면 좋은 것들
DuckDB에는 분석가의 반복 작업을 줄여주는 편의 문법이 많습니다. 자주 쓰게 되는 것들을 모았습니다.
-- 1. SUMMARIZE: 테이블 프로파일링 한 방에
-- 각 컬럼의 min, max, 근사 유니크 수, null 비율까지 자동 요약
SUMMARIZE SELECT * FROM 'events.parquet';
-- 2. EXCLUDE / REPLACE: 컬럼이 많을 때의 구원자
SELECT * EXCLUDE (raw_payload, internal_flag)
FROM wide_table;
SELECT * REPLACE (round(amount, 2) AS amount)
FROM sales;
-- 3. 중복 제거의 정석: 그룹별 최신 1건만 남기기
SELECT * FROM events
QUALIFY row_number() OVER (
PARTITION BY user_id ORDER BY ts DESC
) = 1;
-- 4. 재현 가능한 샘플링
SELECT * FROM big_table USING SAMPLE 1 PERCENT (bernoulli, 42);
-- 5. 리스트 집계와 구조체: 반정규화 데이터 다루기
SELECT user_id, list(distinct category) AS categories
FROM purchases GROUP BY user_id;
특히 SUMMARIZE는 처음 받은 데이터의 품질을 1분 안에 파악하게 해주고, QUALIFY는 윈도우 함수 결과를 서브쿼리 없이 필터링하게 해줍니다. 이 두 가지만 손에 익어도 탐색 속도가 눈에 띄게 달라집니다.
또 하나 강조하고 싶은 것은 친절한 CSV 처리입니다. 구분자 추론, 인코딩 문제, 깨진 행 무시 옵션까지 현실 데이터의 지저분함을 상당히 잘 받아줍니다.
-- 깨진 행을 버리면서 읽고, 어떤 행이 버려졌는지 확인
SELECT * FROM read_csv('messy.csv', ignore_errors = true);
SELECT * FROM read_csv('messy.csv', store_rejects = true);
SELECT * FROM reject_errors; -- 거부된 행의 사유 테이블
한계 — 무엇에 쓰면 안 되는가
DuckDB의 한계는 장점의 뒷면입니다.
첫째, 동시 쓰기에 약합니다. 단일 프로세스 안에서는 멀티스레드로 잘 동작하지만, 여러 프로세스가 같은 DB 파일에 동시에 쓰는 구조는 지원되지 않습니다(읽기 전용 다중 접근은 가능). 다수 사용자가 동시에 쓰는 서비스 백엔드 DB로는 부적합합니다.
둘째, 분산 처리가 없습니다. 단일 머신의 코어와 디스크가 한계입니다. 수 TB를 넘는 정기 배치, 수백 동시 쿼리의 BI 서빙은 ClickHouse나 클라우드 웨어하우스의 영역입니다. 다만 그 경계가 생각보다 높다는 것이 핵심입니다. 요즘 단일 머신은 코어 수십 개에 메모리 수백 GB까지 가능하니까요.
셋째, 장시간 상주 서비스보다 배치와 대화형 분석에 맞습니다. 고가용성, 복제, 장애 조치 같은 운영 데이터베이스의 기능은 애초에 목표가 아닙니다.
선택 가이드 — Postgres, SQLite, ClickHouse와 비교
| 기준 | SQLite | DuckDB | Postgres | ClickHouse |
| --- | --- | --- | --- | --- |
| 배포 형태 | 임베디드 | 임베디드 | 서버 | 서버 또는 분산 |
| 저장 방식 | 행 지향 | 컬럼 지향 | 행 지향 | 컬럼 지향 |
| 최적 워크로드 | 앱 내 OLTP | 단일 노드 OLAP | 범용 OLTP | 대규모 OLAP 서빙 |
| 동시 쓰기 | 단일 작성자 | 단일 프로세스 작성자 | 강력 | 강력 |
| 수평 확장 | 없음 | 없음 | 제한적 | 핵심 강점 |
| 파일 직접 쿼리 | 제한적 | 핵심 강점 | 확장 필요 | 지원 |
| 운영 부담 | 거의 없음 | 거의 없음 | 중간 | 높음 |
| 어울리는 규모 | 기기 로컬 데이터 | GB에서 수백 GB | 서비스 전반 | TB 이상, 고동시성 |
거칠게 요약하면 이렇습니다. 애플리케이션의 상태 저장은 SQLite나 Postgres, 한 사람 또는 한 파이프라인의 분석은 DuckDB, 조직 전체가 두드리는 실시간 분석 서빙은 ClickHouse입니다. 그리고 DuckDB는 나머지 셋 모두의 데이터를 읽을 수 있는 접착제이기도 합니다.
성능 팁
마지막으로 실무에서 효과가 큰 튜닝 포인트를 정리합니다.
-- 1. 메모리와 스레드를 명시적으로 관리
SET memory_limit = '8GB';
SET threads = 8;
-- 2. 실행 계획 확인 습관
EXPLAIN ANALYZE
SELECT category, sum(amount) FROM 'sales/*.parquet' GROUP BY 1;
-- 3. CSV는 한 번 Parquet으로 바꿔두면 이후 모든 쿼리가 빨라진다
COPY (SELECT * FROM 'raw/*.csv') TO 'data.parquet'
(FORMAT parquet, COMPRESSION zstd);
-- 4. 파티션 디렉토리 구조를 활용한 프루닝
-- year=2026/month=06 같은 하이브 파티셔닝을 인식한다
SELECT count(*) FROM read_parquet('events/*/*/*.parquet', hive_partitioning = true)
WHERE year = 2026 AND month = 6;
추가로 세 가지를 기억해두면 좋습니다. 첫째, SELECT 별표를 피하고 필요한 컬럼만 지정하면 컬럼 프루닝 효과를 온전히 받습니다. 둘째, 큰 정렬과 조인이 메모리를 넘으면 자동으로 디스크 스필이 일어나는데, 임시 디렉토리를 빠른 SSD로 지정하면 체감이 다릅니다. 셋째, 같은 데이터를 반복 쿼리한다면 CREATE TABLE AS로 DuckDB 네이티브 포맷에 올려두는 것이 Parquet 재스캔보다 빠릅니다.
마치며
DuckDB의 성공은 화려한 신기술이 아니라 정확한 포지셔닝의 승리입니다. 대부분의 분석 데이터는 한 대의 머신에 들어가고, 대부분의 분석가는 서버 운영을 원하지 않으며, 대부분의 파이프라인은 Parquet 파일을 다룹니다. DuckDB는 이 세 가지 현실에 컬럼 저장과 벡터화 실행이라는 정통 데이터베이스 기술을 정확히 꽂아 넣었습니다.
도입도 부담이 없습니다. 오늘 다루던 CSV 하나를 duckdb 셸로 열어 GROUP BY를 날려보는 것부터 시작하면 됩니다. 그 한 번의 경험이 "이 데이터, 굳이 웨어하우스에 올려야 하나?"라는 건강한 질문으로 이어질 것입니다. 노트북에서 끝나는 분석의 시대는 이미 와 있습니다.
참고 자료
- DuckDB 공식 문서: https://duckdb.org/docs/
- Why DuckDB (공식 소개): https://duckdb.org/why_duckdb
- DuckDB 1.0 릴리스 발표: https://duckdb.org/2024/06/03/announcing-duckdb-100.html
- DuckLake 발표 글: https://duckdb.org/2025/05/27/ducklake.html
- DuckDB GitHub 저장소: https://github.com/duckdb/duckdb
- dbt-duckdb 어댑터: https://github.com/duckdb/dbt-duckdb
- MotherDuck 공식 사이트: https://motherduck.com/
- DuckDB Python API 문서: https://duckdb.org/docs/api/python/overview
- httpfs 확장 문서: https://duckdb.org/docs/extensions/httpfs/overview
- ASOF JOIN 문서: https://duckdb.org/docs/sql/query_syntax/from
- Hacker News의 DuckDB 토론: https://news.ycombinator.com/item?id=24531085
- GeekNews 메인: https://news.hada.io/
현재 단락 (1/229)
Hacker News에는 주기적으로 1면에 올라오는 단골 주제들이 있습니다. SQLite 예찬, Postgres로 충분하다는 글, 그리고 DuckDB 활용기입니다. "노트북에서 수...