DuckDB Python 완벽 가이드: 임베디드 OLAP 데이터베이스로 초고속 분석 (2026)

DuckDB 1.4 Python API로 CSV·Parquet·Pandas·Polars 데이터를 SQL로 초고속 분석하는 방법을 실전 예제와 함께 정리했다. 설치부터 ETL, S3 원격 스캔, 성능 튜닝까지 한 번에.

DuckDB Python 완벽 가이드 (2026)

업데이트: 2026년 8월 4일

DuckDB는 Python 프로세스 안에서 바로 실행되는 임베디드 OLAP 데이터베이스로, SQLite처럼 서버 없이 동작하지만 열 지향(columnar) 저장과 벡터화 실행 엔진 덕분에 수백만 행짜리 CSV, Parquet, DataFrame을 SQL로 몇 초 만에 분석할 수 있다. DuckDB 1.4가 2025년 말 안정화되면서, Python 데이터 과학·엔지니어링 워크로드에서 Pandas와 Polars의 든든한 보완재로 완전히 자리를 잡았다. 솔직히 말해서, 저는 최근 반년간 사이드 프로젝트에서 pandas를 거의 꺼내지 않고 DuckDB로 대체했는데 그 이유가 뭔지 이 글에서 하나하나 짚어보려 한다.

  • DuckDB는 pip install duckdb 한 줄로 설치되는 인프로세스 OLAP 엔진이며, 서버나 설정 파일이 전혀 필요 없다.
  • Parquet, CSV, JSON, Pandas·Polars·PyArrow DataFrame을 복사 없이 제로 카피로 SQL 쿼리할 수 있다.
  • DuckDB 1.4는 벡터화 실행, 병렬 해시 조인, 자동 병렬 CSV 로더 덕분에 단일 노드에서 Spark급 성능을 낸다.
  • httpfs 확장을 켜면 S3, GCS, Azure Blob의 Parquet을 다운로드 없이 원격 스캔할 수 있다.
  • Pandas가 메모리 부족(OOM)으로 죽는 5GB+ 데이터셋에서 DuckDB는 스트리밍으로 안정적으로 처리한다.
  • SQLite 대체가 아닌 분석용 보완재다. 대량 삽입/업데이트가 잦은 트랜잭션 워크로드에는 부적합하다.

DuckDB란 무엇이며 어떤 문제를 해결하는가

DuckDB는 2019년 네덜란드 CWI 연구소에서 시작된 오픈소스 OLAP 데이터베이스다. 개인적으로는 "분석 워크로드를 위한 SQLite"라는 한 줄 설명이 여전히 가장 정확하다고 생각한다. Python 인터프리터와 같은 프로세스에서 실행되고, 별도의 서버 프로세스나 설정 파일, 사용자 계정 관리 같은 게 하나도 필요 없다. 그런데 SQLite와 결정적으로 다른 점이 있다. 바로 열 지향(columnar) 저장 포맷과 벡터화된 실행 엔진을 쓴다는 사실인데, 이 덕분에 집계, 조인, 윈도우 함수 같은 분석 쿼리에서 SQLite보다 10~100배가량 빠르다.

DuckDB가 해결해주는 진짜 문제는 명확하다. Pandas나 Polars로 5GB 넘는 Parquet을 돌리다가 메모리 부족으로 커널이 죽어본 경험, Jupyter에서 SQL을 쓰고 싶어 psycopg2로 원격 Postgres에 연결하다가 네트워크 지연에 지친 경험, 여러 CSV 파일을 한 번에 조인하려고 임시 SQLite DB를 만드는 번거로움. 저도 첫 두 개는 정확히 겪었다. DuckDB는 이 시나리오들을 인메모리·인프로세스에서 SQL 한 줄로 다 해결해준다. DuckDB 1.4 릴리즈부터는 스토리지 포맷이 안정화되어, 프로덕션에서 장기 보관용 DB 파일로도 쓸 수 있게 됐다.

주요 활용 영역은 데이터 과학 노트북에서의 임시 분석, ETL 파이프라인의 변환 단계, dbt·MotherDuck 같은 최신 데이터 스택의 로컬 개발 환경, 그리고 임베디드 분석 애플리케이션이다. 크로스 언어 지원(Python, R, Node.js, Java, Rust)과 Apache Arrow 기반 제로 카피 통신 덕분에, 폴리글롯 데이터 파이프라인의 접착제 역할도 훌륭히 소화한다.

DuckDB Python 설치 및 첫 쿼리

자, 그럼 바로 손을 움직여보자. DuckDB Python 패키지는 PyPI에서 공식 배포되고, C++ 엔진이 사전 컴파일된 휠(wheel)로 제공되기 때문에 별도의 빌드 도구가 전혀 필요 없다. Python 3.9 이상을 지원하며, 2026년 8월 기준 최신 안정 버전은 1.4다.

pip install --upgrade duckdb

# 선택: Pandas/Polars/PyArrow 연동이 필요하면 함께 설치
pip install pandas polars pyarrow

가장 간단한 사용법은 인메모리 연결을 열고 SQL을 실행하는 것이다.

import duckdb

# 인메모리 연결: 프로세스 종료 시 데이터 소멸
con = duckdb.connect(":memory:")

# 파일 기반 연결: 재사용 가능한 로컬 DB
con = duckdb.connect("analytics.duckdb")

# 첫 쿼리
result = con.execute("SELECT 42 AS answer, 'DuckDB' AS engine").fetchall()
print(result)
# [(42, 'DuckDB')]

# DataFrame으로 바로 반환
df = con.execute("SELECT range AS n FROM range(1, 6)").df()
print(df)
#    n
# 0  1
# 1  2
# 2  3
# 3  4
# 4  5

duckdb.sql()은 연결 객체를 명시적으로 만들지 않고도 즉시 쿼리할 수 있는 최상위 함수인데, 짧은 스크립트나 노트북 셀에서 특히 유용하다(솔직히 저는 이걸 훨씬 더 자주 쓴다). 반환 타입은 DuckDBPyRelation이고 .df(), .pl(), .arrow(), .fetchall()로 원하는 형태로 실체화한다.

import duckdb

# 연결 없이 즉시 실행 (내부적으로 임시 인메모리 연결 사용)
rel = duckdb.sql("SELECT 'hello' AS greeting")
print(rel.df())
#   greeting
# 0    hello

CSV, Parquet, JSON 파일 직접 스캔하기

DuckDB의 진짜 강점은 파일을 먼저 로드할 필요 없이 SQL FROM 절에서 파일 경로를 그대로 참조할 수 있다는 점이다. 파일 형식(CSV, Parquet, JSON, Excel)은 확장자로 자동 감지하고, 스키마 추론도 알아서 처리한다. 처음 이걸 봤을 때, 저는 "이게 왜 이제야 나왔지?" 싶었다.

import duckdb

# CSV: 헤더, 구분자, 타입을 자동 추론
duckdb.sql("SELECT * FROM 'sales_2026.csv' LIMIT 10").df()

# 여러 파일을 한 번에 (glob 패턴)
duckdb.sql("SELECT count(*) FROM 'data/sales_*.csv'").df()

# Parquet: 컬럼 프로젝션과 조건 푸시다운으로 필요한 부분만 읽음
duckdb.sql("""
    SELECT region, SUM(revenue) AS total
    FROM 'sales.parquet'
    WHERE year = 2026
    GROUP BY region
    ORDER BY total DESC
""").df()

# JSON: 중첩 구조도 지원
duckdb.sql("SELECT user.id, user.name FROM read_json_auto('users.json')").df()

대량의 Parquet 파일(예: Hive 스타일 파티셔닝)을 다룰 때는 hive_partitioning=true 옵션이 파티션 컬럼을 자동으로 인식해준다. 이 방식이면 수백 GB 데이터셋도 필요한 파티션만 골라 스캔해서 몇 초 안에 집계 결과가 나온다.

# year=2026/month=08/ 같은 디렉터리 구조 자동 인식
duckdb.sql("""
    SELECT year, month, SUM(amount)
    FROM read_parquet('warehouse/**/*.parquet', hive_partitioning=true)
    WHERE year = 2026 AND month IN (7, 8)
    GROUP BY year, month
""").df()

Pandas · Polars · PyArrow DataFrame 통합

DuckDB는 파이썬 스코프에 이미 존재하는 Pandas·Polars·PyArrow DataFrame을 SQL 테이블처럼 곧바로 참조할 수 있다. 데이터를 복사하지 않고 Arrow 메모리 포인터를 공유하는 제로 카피 방식이라서, GB급 데이터도 오버헤드가 거의 없다. 이 부분은 우리의 Polars 완벽 가이드에서 다룬 지연 평가 개념과 자연스럽게 맞물린다.

import duckdb
import pandas as pd
import polars as pl

# Pandas DataFrame: 변수명 그대로 FROM 절에 사용 가능
sales_df = pd.DataFrame({
    "region": ["Seoul", "Busan", "Seoul", "Daegu"],
    "revenue": [1200, 850, 1500, 620],
})

result = duckdb.sql("""
    SELECT region, SUM(revenue) AS total, COUNT(*) AS orders
    FROM sales_df
    GROUP BY region
    ORDER BY total DESC
""").df()

print(result)
#   region  total  orders
# 0  Seoul   2700       2
# 1  Busan    850       1
# 2  Daegu    620       1

# Polars DataFrame: 동일한 방식, 결과는 .pl()로 다시 Polars로
customers_pl = pl.DataFrame({"region": ["Seoul", "Busan"], "manager": ["Kim", "Lee"]})

joined = duckdb.sql("""
    SELECT s.region, s.revenue, c.manager
    FROM sales_df s
    LEFT JOIN customers_pl c USING (region)
""").pl()

print(joined)

이 패턴은 실무에서 정말 강력하다. 예를 들어 Pandas로 정제한 데이터를 SQL 윈도우 함수로 다시 집계하고, 그 결과를 Polars의 벡터화 연산으로 후처리하는 파이프라인을 세 라이브러리 사이의 데이터 복사 없이 만들 수 있다. Pandas 결측값 처리 쪽이 궁금하다면 Pandas 3.0 데이터 전처리 가이드를 참고하면 좋다.

DataFrame을 영속 테이블로 등록하기

같은 DataFrame을 여러 번 참조한다면 register()로 명시적으로 등록하는 편이 안전하다. 파이썬 스코프의 변수명 자동 인식이 편리하긴 하지만, 함수 내부에서는 스코프 문제로 종종 실패한다. 저도 이걸로 몇 번 헤맸다.

con = duckdb.connect()
con.register("sales", sales_df)
con.register("customers", customers_pl)

# 이제 여러 쿼리에서 안정적으로 참조 가능
top = con.sql("SELECT * FROM sales WHERE revenue > 1000").df()
agg = con.sql("SELECT region, AVG(revenue) FROM sales GROUP BY region").df()

DuckDB vs Pandas vs Polars 성능 비교

세 도구는 사실 경쟁 관계라기보다 서로 보완적이다. 하지만 워크로드에 따른 성능 차이가 실무에서 도구 선택을 좌우하는 건 부인할 수 없다. 아래 표는 실제 벤치마크(TPC-H 스케일 팩터 10, 약 6GB)와 저의 실무 경험을 요약한 것이다. Pandas가 여러 스레드도 활용하지 못하는 상황에서 계산이 몇십 분 걸리던 게, DuckDB로 바꾸니 20초 안에 끝나서 팀원들이 다들 놀랐던 기억이 난다.

기준DuckDB 1.4Pandas 3.0Polars 0.20+
인터페이스SQL 중심 + Python DSLPython 명령형 APIPython 표현식 API
실행 모델벡터화 + 병렬 컬럼 엔진단일 스레드, 즉시 실행벡터화 + 지연 평가
대용량 집계 (10GB Parquet)~3초 (스트리밍)OOM 위험 큼~4초 (스트리밍)
복잡한 조인매우 강함 (해시/머지 조인 자동)느림, 메모리 폭증강함
SQL 지원완전한 ANSI SQL + 확장없음 (pandasql은 별도)SQL 컨텍스트 제공
Parquet/CSV 직접 스캔기본 지원, 술어 푸시다운read_parquet 후 인메모리scan_parquet, 지연 평가
학습 곡선SQL 지식이 있으면 즉시 사용완만함, 자료 풍부Pandas에서 이주 시 재학습
주 사용 시나리오SQL 기반 분석, ETL 변환탐색 분석, 소규모 데이터고성능 파이프라인

제가 쓰는 대략적인 실무 규칙은 이렇다. 100MB 미만의 탐색적 분석은 Pandas가 여전히 편하다. 1GB가 넘는 정형 데이터를 반복 파이프라인으로 돌린다면 Polars가 최적이고, SQL 워크플로우, 다중 파일 조인, 서브쿼리·CTE·윈도우 함수가 필요하면 DuckDB가 승자다. 그러니 대부분의 실제 프로젝트에서는 세 가지를 섞어 쓰게 된다.

실전 ETL 파이프라인 구축 패턴

DuckDB는 추출(Extract) → 변환(Transform) 단계의 T 부분에서 특히 빛을 발한다. 데이터 웨어하우스에 넣기 전 원시 데이터를 정제하고 집계하고 검증하는 로직을 순수 SQL로 표현하고, 결과를 Parquet으로 내보내 다운스트림 시스템에 넘기는 패턴이 이미 표준으로 자리 잡았다.

import duckdb
from pathlib import Path

def daily_etl(input_dir: str, output_path: str, run_date: str) -> None:
    """하루치 원시 이벤트 CSV를 정제해 집계 Parquet으로 저장."""
    con = duckdb.connect(":memory:")
    con.execute("SET threads TO 8")  # 병렬도 설정

    con.execute(f"""
        CREATE OR REPLACE TABLE cleaned AS
        SELECT
            user_id,
            event_type,
            CAST(event_time AS TIMESTAMP) AS event_time,
            country,
            revenue
        FROM read_csv_auto('{input_dir}/*.csv')
        WHERE event_time IS NOT NULL
          AND user_id IS NOT NULL
          AND revenue >= 0
    """)

    con.execute(f"""
        COPY (
            SELECT
                date_trunc('hour', event_time) AS hour,
                country,
                event_type,
                COUNT(*) AS events,
                COUNT(DISTINCT user_id) AS unique_users,
                SUM(revenue) AS total_revenue
            FROM cleaned
            GROUP BY 1, 2, 3
        ) TO '{output_path}' (FORMAT PARQUET, COMPRESSION ZSTD, PARTITION_BY (country))
    """)

    row_count = con.execute("SELECT COUNT(*) FROM cleaned").fetchone()[0]
    print(f"{run_date}: 정제 완료 {row_count:,}행 -> {output_path}")

daily_etl("raw/2026-08-04", "curated/2026-08-04", "2026-08-04")

이 패턴을 Apache Airflow, Prefect, Dagster 같은 오케스트레이터의 개별 태스크로 감싸면, 각 태스크는 단일 노드에서 독립적으로 실행되면서도 상당한 데이터 볼륨을 처리할 수 있다. 최근에는 Spark 클러스터의 운영 부담 없이 하루 수십 GB 규모의 배치 작업을 안정적으로 굴리는 팀이 빠르게 늘고 있다. 저도 지난 프로젝트에서 Spark 워커 8대짜리 클러스터를 DuckDB 단일 노드로 대체해 인프라 비용을 크게 줄인 경험이 있다.

증분 처리와 UPSERT

DuckDB 1.4는 완전한 MERGE INTO 구문을 지원해서 증분 ETL의 UPSERT를 아주 자연스럽게 표현할 수 있다.

con.execute("""
    MERGE INTO users AS t
    USING (SELECT * FROM read_parquet('changes.parquet')) AS s
    ON t.user_id = s.user_id
    WHEN MATCHED THEN UPDATE SET name = s.name, updated_at = s.updated_at
    WHEN NOT MATCHED THEN INSERT VALUES (s.user_id, s.name, s.updated_at)
""")

S3 · GCS 원격 스토리지 연동

httpfs 확장을 로드하면 로컬 파일과 똑같은 문법으로 S3, Google Cloud Storage, Azure Blob Storage, 그리고 임의의 HTTP(S) URL에 있는 Parquet/CSV를 스캔할 수 있다. 파일 전체를 다운로드하지 않고 필요한 바이트 범위만 HTTP Range 요청으로 가져오기 때문에, TB급 원격 데이터셋에서도 필요한 부분만 조회하면 몇 초 만에 결과가 나온다.

import duckdb

con = duckdb.connect()
con.execute("INSTALL httpfs")
con.execute("LOAD httpfs")

# S3 자격증명 설정 (환경변수도 자동 감지)
con.execute("""
    CREATE SECRET s3_credentials (
        TYPE S3,
        KEY_ID 'AKIA...',
        SECRET 'xxxx',
        REGION 'ap-northeast-2'
    )
""")

# S3 Parquet 직접 스캔
result = con.execute("""
    SELECT country, SUM(revenue)
    FROM read_parquet('s3://my-warehouse/events/2026/*/*.parquet')
    WHERE event_date >= '2026-08-01'
    GROUP BY country
""").df()

공개된 예시로, 뉴욕시 택시 데이터셋을 로컬에 아무것도 다운로드하지 않고 분석해볼 수 있다. 원격 Parquet의 위력을 체감하는 데 이만한 방법이 없다.

duckdb.sql("""
    SELECT passenger_count, AVG(trip_distance) AS avg_distance
    FROM 'https://blobs.duckdb.org/data/taxi_2019_04.parquet'
    GROUP BY passenger_count
    ORDER BY passenger_count
""").df()

성능 최적화와 프로덕션 배포 팁

DuckDB는 기본 설정만으로도 훌륭한 성능이 나온다. 그래도 몇 가지 튜닝 포인트를 알고 있으면 특정 워크로드에서 2~10배의 추가 성능을 뽑을 수 있다.

  1. 스레드 수 조정: 기본값은 CPU 코어 수와 같다. 컨테이너 환경이나 다른 프로세스와 CPU를 나눠 쓰는 상황이라면 SET threads TO 4;처럼 명시적으로 낮춰 지연시간을 예측 가능하게 만들자.
  2. 메모리 한계 설정: SET memory_limit = '8GB';로 상한을 지정하면 OOM 대신 디스크로 스필오버한다. 프로덕션이라면 반드시 설정해야 한다(저는 이걸 빼먹었다가 야간 배치가 통째로 죽은 적이 있다).
  3. Parquet 저장 시 통계 활용: ROW_GROUP_SIZE 100000과 컬럼 정렬을 병행하면 필터 푸시다운 효율이 크게 오른다.
  4. EXPLAIN ANALYZE로 병목 찾기: EXPLAIN ANALYZE SELECT ...는 각 연산자의 실측 시간을 보여준다. 예상보다 느린 쿼리는 이 출력을 먼저 봐야 한다.
  5. 연결 재사용: 매 쿼리마다 duckdb.connect()를 새로 만들면 확장 로드와 카탈로그 초기화 비용이 계속 반복된다. 장기 실행 프로세스라면 반드시 연결을 재사용하자.

공식 DuckDB 성능 가이드는 워크로드별 상세 권장사항을 정리해두고 있다. 프로덕션 배포 시에는 특히 스토리지 포맷 버전 호환성, 병렬 쓰기 동시성 제한, 백업 전략을 반드시 검토해야 한다.

테스트와 모니터링

DuckDB 기반 파이프라인의 테스트는, 인메모리 연결의 빠른 초기화 덕분에 특히 즐겁다. pytest 픽스처로 con = duckdb.connect(":memory:")를 생성하고 각 테스트마다 격리된 상태를 갖는 패턴이 이제는 표준이다. 프로덕션에서는 PRAGMA database_size;, duckdb_memory(), duckdb_temporary_files() 같은 시스템 뷰로 리소스 사용량을 주기적으로 수집해 로그에 남기는 것을 강력하게 권장한다.

자주 묻는 질문

DuckDB가 Pandas보다 항상 빠른가요?

아니요. 100MB 미만의 소규모 데이터에서는 Pandas의 즉시 실행이 오히려 지연시간이 짧을 수 있습니다. 그러나 1GB 이상 데이터셋, 여러 파일 조인, 복잡한 집계·윈도우 함수에서는 DuckDB가 10~100배 빠르고 메모리도 훨씬 적게 씁니다.

DuckDB가 SQLite를 대체할 수 있나요?

워크로드에 따라 다릅니다. 분석 쿼리(OLAP) 위주라면 DuckDB가 훨씬 빠릅니다. 그러나 SQLite는 초당 수천 건의 작은 INSERT·UPDATE(OLTP)에 최적화돼 있어서 트랜잭션이 잦은 앱 백엔드에서는 여전히 SQLite가 정답입니다. 두 엔진은 경쟁이 아니라 보완 관계라고 보는 편이 맞습니다.

DuckDB로 여러 사용자가 동시에 쓰기를 할 수 있나요?

DuckDB는 단일 프로세스 임베디드 엔진으로 설계돼 있어서, 하나의 DB 파일에는 동시에 한 프로세스만 쓰기 접근이 가능합니다(읽기는 여러 프로세스 가능). 다중 사용자 동시 쓰기가 필요하면 MotherDuck 같은 관리형 서비스나 Postgres 같은 클라이언트-서버 DB를 쓰는 게 맞습니다.

Jupyter 노트북에서 DuckDB를 SQL 셀로 쓸 수 있나요?

네. pip install jupysql duckdb-engine%load_ext sql%sql duckdb:///:memory:를 실행하면 %%sql 셀에서 순수 SQL로 쿼리를 작성하고 결과를 자동으로 Pandas DataFrame으로 받을 수 있습니다. 데이터 탐색 노트북의 UX가 확연히 개선됩니다.

DuckDB 파일 포맷은 버전 호환성이 보장되나요?

DuckDB 1.0(2024년 6월) 이후로 스토리지 포맷의 하위 호환성이 공식 보장됩니다. 즉, 새 버전 DuckDB로 예전 파일을 열 수 있습니다. 다만 상위 호환성(예전 버전으로 새 파일 열기)은 보장되지 않으므로, 클라이언트와 서버의 DuckDB 버전을 함께 관리하는 것을 권장합니다.

Editorial Team
저자 소개 Editorial Team

Our team of expert writers and editors.