DuckDB 완벽 가이드 — 인메모리 OLAP, Parquet 직접 쿼리, Python/Node 실전 활용

DuckDB 완벽 가이드 — 인메모리 OLAP, Parquet 직접 쿼리, Python/Node 실전 활용

이 글의 핵심

DuckDB는 "OLAP용 SQLite" 라 불리는 인메모리 분석 DB입니다. 단일 바이너리·의존성 0·파일 하나·Parquet/CSV 직접 쿼리로 데이터 분석가의 워크플로우를 극적으로 단순화합니다. 이 글은 설치·핵심 SQL·Python/Node 통합·메모리 관리·프로덕션 패턴까지 실전 위주로 정리합니다.

이 글의 핵심

DuckDB는 “OLAP용 SQLite”로 불리는 임베디드 분석 DB입니다. 2019년 CWI Amsterdam에서 시작해 2024년 v1.0, 2026년 현재 1.1/1.2 기준으로 columnar 저장 + vectorized 실행 + SQL 표준 지원을 모두 갖춘 성숙한 도구입니다.

핵심 매력:

  • 의존성 0, 단일 바이너리pip install duckdb 하나로 끝
  • Parquet/CSV/JSON을 SQL로 직접 쿼리 — 전처리 없이
  • pandas DataFrame과 양방향 상호운용
  • 메모리보다 큰 데이터도 out-of-core 처리
  • S3/GCS/HTTP 원격 파일 스트리밍 쿼리

이 글은 설치부터 실전 분석 패턴까지 데이터 엔지니어·분석가·백엔드 개발자 모두를 겨냥해 정리합니다.

설치

CLI

# macOS
brew install duckdb

# Linux / WSL
curl -L https://github.com/duckdb/duckdb/releases/download/v1.1.3/duckdb_cli-linux-amd64.zip -o duckdb.zip
unzip duckdb.zip
sudo mv duckdb /usr/local/bin/

# Windows
scoop install duckdb
duckdb            # 인메모리 세션 시작
duckdb my.db      # 파일 저장

Python

pip install duckdb pandas pyarrow

Node.js

npm install @duckdb/node-api  # v1.1+ 공식 neo-API

기타 바인딩

R, Rust, Go, Java, Swift, C/C++ 공식 바인딩이 있으며 WebAssembly(@duckdb/duckdb-wasm)로 브라우저에서도 실행됩니다.

첫 쿼리: 5분 만에 이해

-- CSV를 다운로드 없이 바로 쿼리
SELECT *
FROM 'https://raw.githubusercontent.com/datasets/population/master/data/population.csv'
LIMIT 5;

-- Parquet를 직접 쿼리 (S3도 가능)
SELECT country, year, value
FROM 's3://my-bucket/population/*.parquet'
WHERE year >= 2020
ORDER BY value DESC
LIMIT 10;

-- 로컬 폴더의 모든 CSV를 하나의 테이블로
SELECT * FROM read_csv('data/*.csv', union_by_name=true);

-- JSON도 자연스럽게
SELECT json->>'$.name', json->>'$.age'
FROM read_json('users.jsonl');

별도 ETL 없이 소스 파일을 SQL로 바로 질의하는 것이 DuckDB의 핵심 경험입니다.

SQL 기능: Postgres와 매우 유사

DuckDB는 Postgres 호환 SQL 방언을 지향합니다. 윈도우 함수, CTE, LATERAL JOIN, UNNEST, ARRAY/STRUCT, 정규표현식, 시계열 함수, 피벗 등 대부분 지원됩니다.

-- CTE + 윈도우 함수
WITH daily_sales AS (
  SELECT
    date_trunc('day', created_at) AS day,
    SUM(amount) AS total
  FROM orders
  GROUP BY 1
)
SELECT
  day,
  total,
  AVG(total) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d,
  LAG(total, 7) OVER (ORDER BY day) AS same_day_last_week
FROM daily_sales
ORDER BY day DESC;
-- PIVOT (SQL 표준 구현체 중 드묾)
PIVOT sales
ON category
USING SUM(amount)
GROUP BY year, month;
-- ARRAY / STRUCT / MAP (복합 타입)
SELECT
  user_id,
  list_filter(tags, t -> t.weight > 0.5) AS strong_tags,
  struct_extract(profile, 'email') AS email
FROM users;

pandas와 통합

import duckdb
import pandas as pd

df = pd.read_csv('large_file.csv')

# DataFrame을 바로 FROM 절에서 사용
result = duckdb.sql("""
  SELECT country, AVG(gdp_per_capita) AS avg_gdp
  FROM df
  WHERE year >= 2020
  GROUP BY country
  ORDER BY avg_gdp DESC
  LIMIT 10
""").df()

print(result)
# 반대로 DuckDB 결과를 DataFrame으로
con = duckdb.connect('analytics.db')
df = con.sql("SELECT * FROM orders WHERE amount > 1000").df()

pandas 대비 속도 이점: 메모리보다 큰 데이터, 복잡한 조인, 그룹 집계에서 DuckDB가 2~50배 빠른 경우가 흔합니다. 단순한 DataFrame 조작은 pandas가 편합니다.

Parquet 중심 워크플로우

# Parquet 읽기·쓰기
con.sql("""
  COPY (SELECT * FROM events WHERE created_at >= '2026-01-01')
  TO 'events_2026.parquet' (FORMAT 'parquet', COMPRESSION 'zstd')
""")

# 파티션된 쓰기
con.sql("""
  COPY events
  TO 'events/'
  (FORMAT PARQUET, PARTITION_BY (year, month), OVERWRITE_OR_IGNORE)
""")
events/
├── year=2026/
│   ├── month=1/data_0.parquet
│   ├── month=2/data_0.parquet
│   └── month=3/data_0.parquet

파티션 경로 자체를 predicate로 활용해 “2026년 3월만 읽기”가 즉시 가능해집니다.

SELECT COUNT(*) FROM 'events/year=2026/month=3/*.parquet';

S3 / GCS 원격 파일

-- httpfs 확장 로드
INSTALL httpfs;
LOAD httpfs;

-- AWS 자격증명 설정
SET s3_region='ap-northeast-2';
SET s3_access_key_id='...';
SET s3_secret_access_key='...';

-- 또는 IAM role / 환경변수 / ~/.aws/credentials 자동 감지
CREATE SECRET aws_creds (
  TYPE S3,
  PROVIDER CREDENTIAL_CHAIN,
  CHAIN 'env;config;sso'
);

-- 원격 쿼리
SELECT
  date_trunc('day', created_at) AS day,
  COUNT(*) AS events
FROM 's3://my-lake/events/year=2026/*.parquet'
WHERE event_type = 'purchase'
GROUP BY 1
ORDER BY 1;

필요한 컬럼·row group만 다운로드하므로 100GB Parquet에서 1GB 미만만 전송하고 결과를 얻는 일이 흔합니다. data lake를 DW 도입 없이도 분석할 수 있게 만드는 킬러 기능입니다.

메모리 관리: 대용량 처리의 핵심

-- 사용 메모리 상한 (기본 시스템 메모리 80%)
SET memory_limit = '8GB';

-- 스풀링 디렉터리 (메모리 초과 시 디스크로)
SET temp_directory = '/tmp/duckdb';

-- 스레드 수
SET threads = 8;

-- 현재 설정 확인
SELECT current_setting('memory_limit'), current_setting('threads');

DuckDB는 streaming/out-of-core 실행을 지원해 메모리를 넘는 데이터도 처리합니다. 다만 일부 연산(큰 해시 조인, 정렬)은 디스크 스풀링이 발생하면 느려지므로 조인 순서·필터 순서를 고려해 plan을 최적화하세요.

EXPLAIN ANALYZE
SELECT ...

EXPLAIN ANALYZE로 실제 실행 계획과 각 연산의 시간·행 수를 확인할 수 있습니다.

실전 패턴 1: 로그 분석 대시보드

import duckdb
import streamlit as st

con = duckdb.connect('logs.db')

# Parquet 원본은 건드리지 않고 View로 조합
con.sql("""
  CREATE OR REPLACE VIEW requests AS
  SELECT * FROM 's3://my-logs/requests/*.parquet'
""")

@st.cache_data(ttl=300)
def top_endpoints(days: int):
    return con.sql(f"""
      SELECT endpoint, COUNT(*) AS hits, AVG(latency_ms) AS avg_latency
      FROM requests
      WHERE ts >= now() - INTERVAL '{days} days'
      GROUP BY endpoint
      ORDER BY hits DESC
      LIMIT 20
    """).df()

st.dataframe(top_endpoints(7))

Streamlit/Dash/Panel + DuckDB는 DW 없이도 분석 대시보드를 만드는 데 강력한 조합입니다.

실전 패턴 2: ETL 중간 단계

# Airflow/Dagster 태스크 안에서
import duckdb

def transform_daily_sales(ds: str):
    con = duckdb.connect(':memory:')
    con.sql(f"""
      CREATE TABLE daily_sales AS
      SELECT
        date_trunc('day', created_at) AS day,
        product_category,
        SUM(amount) AS revenue,
        COUNT(DISTINCT user_id) AS unique_buyers
      FROM read_parquet('s3://raw/orders/dt={ds}/*.parquet')
      WHERE status = 'completed'
      GROUP BY 1, 2
    """)

    con.sql("""
      COPY daily_sales
      TO 's3://curated/daily_sales/' (FORMAT PARQUET, PARTITION_BY (day))
    """)

Python 메모리 점유 최소화, pandas 대비 10~100배 빠른 집계, SQL로 가독성 개선을 동시에 얻습니다.

실전 패턴 3: Node.js 서버에서 분석 쿼리

import { DuckDBInstance } from '@duckdb/node-api';

const db = await DuckDBInstance.fromFile('analytics.db');
const conn = await db.connect();

export async function fetchTopProducts(days: number) {
  const reader = await conn.runAndReadAll(
    `SELECT product_id, SUM(quantity) AS sold
     FROM sales
     WHERE sold_at >= now() - INTERVAL ? DAYS
     GROUP BY product_id
     ORDER BY sold DESC
     LIMIT 10`,
    [days],
  );
  return reader.getRowObjects();
}

API의 무거운 분석 쿼리를 Postgres에 보내는 대신 DuckDB에 저장된 Parquet/로컬 DB에 보내면 Postgres OLTP 부하가 줄고 응답 시간도 개선됩니다.

확장(extensions): 생태계

-- 자주 쓰는 확장
INSTALL spatial;   LOAD spatial;    -- 지리 연산 (PostGIS 호환)
INSTALL httpfs;    LOAD httpfs;     -- HTTP/S3/GCS
INSTALL json;      LOAD json;       -- JSON (기본 포함)
INSTALL iceberg;   LOAD iceberg;    -- Apache Iceberg 테이블
INSTALL delta;     LOAD delta;      -- Delta Lake
INSTALL postgres;  LOAD postgres;   -- Postgres 직접 연결
INSTALL mysql;     LOAD mysql;      -- MySQL
-- Postgres의 테이블을 DuckDB에서 직접 쿼리
ATTACH 'host=db.internal dbname=app' AS pg (TYPE POSTGRES, READ_ONLY);
SELECT COUNT(*) FROM pg.public.orders;

Iceberg/Delta Lake 읽기는 2024-2025에 걸쳐 성숙해져 프로덕션에서도 쓰입니다. 데이터 레이크 접근을 범용화합니다.

성능 팁

  1. 컬럼 프루닝: SELECT * 지양, 필요한 컬럼만 지정해 Parquet I/O 절감
  2. predicate 먼저: WHERE의 날짜/ID 필터가 파티션·row group을 프루닝
  3. PRAGMA threads=N: CPU 수 기반 조정, 과도한 스레드는 메모리 압박
  4. ORDER BY는 필요할 때만 — 대용량 정렬은 메모리 부담
  5. LIMITSAMPLE로 탐색 쿼리 가속
  6. EXPLAIN ANALYZE로 실제 실행 프로파일 확인
-- 샘플링으로 수백만 행에서 추정 통계
SELECT AVG(amount), STDDEV(amount)
FROM orders
USING SAMPLE 1%;

데이터 이동 없이 혼합 쿼리

-- 로컬 CSV + 원격 Parquet + Postgres 테이블을 한 쿼리로
ATTACH 'pg' AS pg (TYPE POSTGRES, READ_ONLY);

SELECT
  c.segment,
  COUNT(DISTINCT o.user_id) AS buyers,
  SUM(o.amount) AS revenue
FROM 's3://lake/orders/2026/*.parquet' o
JOIN pg.public.customers c ON o.user_id = c.id
JOIN 'campaigns_2026.csv' camp ON camp.campaign_id = o.campaign_id
GROUP BY c.segment
ORDER BY revenue DESC;

ETL 없이 폴리글롯 쿼리가 가능하다는 점이 데이터 팀 워크플로를 크게 단순화합니다.

트러블슈팅

Out of Memory Error

  • SET memory_limit = '4GB';로 상한 조정
  • SET temp_directory = '...' 지정해 디스크 스풀링 허용
  • 큰 조인은 한쪽을 미리 필터링/샘플링

Parquet 파티션이 왜 다 읽힐까

파일 이름/디렉터리 규칙이 key=value 형태인지 확인. hive_partitioning=1 옵션으로 강제 힌트.

S3 자격증명이 안 잡힐 때

CREATE SECRET ... PROVIDER CREDENTIAL_CHAIN이 가장 단순. 환경변수·~/.aws/credentials·IAM role 순으로 자동 검색.

쿼리가 한번 느리고 다음엔 빠름

운영체제 파일 캐시 영향. 원격 Parquet는 DuckDB의 HTTP metadata cache가 구축된 후 가속됩니다.

언제 쓰고 언제 쓰지 말아야 하나

쓰기 좋은 경우

  • 로컬/노트북 분석 탐색
  • ETL 중간 계산 (pandas 대체)
  • 분석 대시보드의 뷰/캐시 계층
  • 데이터 레이크(Parquet) 쿼리 엔진
  • 배치 리포트 생성
  • 테스트/개발 환경 분석 DB

안 맞는 경우

  • 다중 사용자 동시 쓰기 OLTP → Postgres/MySQL
  • 초저지연 단건 read 키-값 스토어 → Redis
  • 실시간 스트리밍 집계 → ClickHouse, Materialize
  • 페타바이트급 분산 분석 → Snowflake, BigQuery, Spark

실전 체크리스트

  • Python/Node 프로젝트에서 pandas 무거운 집계 부분을 DuckDB로 교체해 벤치마크
  • 기존 S3 Parquet 분석 워크플로우에 DuckDB CLI 도입해 ad-hoc 쿼리 가속
  • 로그/이벤트 테이블을 Parquet로 덤프해 Postgres 부하 분리
  • 대시보드(Streamlit/Metabase) 쿼리 엔진을 DuckDB로 교체 실험
  • CI에서 데이터 검증(row count, 유니크 키, 스키마)을 DuckDB로 자동화

마무리

DuckDB는 “SQL을 쓰는 데이터 도구 중 가장 설치·운영이 쉬운 고성능 엔진” 입니다. 인프라·DBA·별도 서버가 필요 없고, pandas를 쓰던 자리·ad-hoc SQL을 쓰던 자리·대시보드 백엔드 모두에 자연스럽게 들어갑니다. 데이터 분석 워크플로우에서 한 번이라도 “pandas가 터진다” 또는 “Postgres 분석 쿼리가 OLTP 영향을 준다” 경험이 있다면, 오늘 pip install duckdb 하고 한 쿼리 돌려보세요. 바뀌는 생산성의 크기가 놀라울 겁니다.

관련 글

  • PostgreSQL 완벽 가이드
  • SQLite 완벽 가이드
  • Python pandas 완벽 가이드
  • 데이터 분석 파이프라인 가이드