본문 바로가기
  • plotly로 바로쓰는 동적시각화 in R & 파이썬
EPL과 유튜브 데이터로 배우는 DuckDB

Python 분석을 강화하는 DuckDB 활용 팁 10가지

by 아참형인간 2026. 5. 31.

https://aladin.kr/p/MSt8I

 

LUVIT♥ EPL과 유튜브 데이터로 배우는 DuckDB | 이기준

DuckDB를 활용한 SQL 기반 데이터 분석 입문서다. SQL 기초부터 고급 활용, 파이썬 연동, 데이터 시각화와 대시보드 제작까지 단계적으로 학습할 수 있다. EPL 데이터, 유튜브 트렌드, 「케이팝 데몬

www.aladin.co.kr

 

Python 분석을 더 빠르고, 더 저렴하고, 더 안정적으로 만드세요 — Pandas 루프에 맡기는 일을 줄이고 DuckDB의 벡터화 엔진에 더 많은 작업을 넘기는 방식으로.

 

DuckDB로 Python 분석을 한 단계 끌어올리기: 더 빠른 조인, Parquet 스캔, Arrow 파이프라인, 안정적인 메모리 사용, 그리고 실전 성능 향상 팁 10가지

 

솔직히 말해 봅시다. Python에서 분석이 느려지는 이유는 대부분 신비로운 문제가 아닙니다.

대개는 다음 세 가지 중 하나입니다.

너무 많은 데이터를 메모리로 가져오거나, 행 단위(row-by-row) 연산을 너무 많이 수행하거나, “잠깐만 쓰자”던 간단한 변환 작업이 조용히 O(n) 규모의 후회로 바뀌는 경우입니다.

 

DuckDB는 이러한 문제에 대한 해답입니다.

DuckDB는 원격 데이터 웨어하우스도 아니고 단순한 라이브러리도 아닙니다. SQL을 지원하고, Parquet·CSV·JSON 같은 현대적인 데이터 형식을 읽을 수 있으며, Python 생태계와도 놀라울 정도로 잘 통합되는 인프로세스(in-process) OLAP 엔진입니다. 이를 제대로 활용하면 별도의 서버 없이도 노트북이 작은 분석 서버처럼 동작하기 시작합니다.

 

아래에서 소개하는 10가지 팁은 즉시 적용할 수 있는 실전 기법들입니다. 이론적인 이야기가 아닙니다. 체감만 좋아지는 것이 아니라 실제 실행 시간을 줄여 주는 방법들입니다.

DuckDB가 Python 스택에서 차지하는 위치: 올바른 사고방식

팁을 살펴보기 전에, 먼저 전체 아키텍처를 머릿속에 정리해 두는 것이 중요합니다. 많은 성능 문제는 잘못된 도구를 사용하는 것이 아니라, 올바른 도구를 잘못된 위치에서 사용하기 때문에 발생합니다.

실용적인 아키텍처 흐름 (군더더기 없이)

  • 저장소 계층(Storage Layer): Parquet, CSV, JSON 파일, 오브젝트 스토리지 또는 로컬 디스크
  • 계산 계층(Compute Layer, DuckDB): 스캔, 필터링, 조인, 집계 작업을 DuckDB의 벡터화 엔진 내부에서 수행
  • 상호운용 계층(Interop Layer): Arrow, Polars, Pandas를 사용해 최종 데이터 가공, 시각화, 머신러닝 피처 생성, 결과 내보내기 수행
  • 출력 계층(Outputs): DataFrame, 차트, 피처 행렬(feature matrix), 보고서 생성

가장 효과적인 패턴은 간단합니다. 무거운 연산은 DuckDB에서 수행하고, 최종 표현과 활용은 Python에서 수행하는 것입니다.

팁 1: DuckDB를 “데이터프레임 전처리기”로 활용하라

3GB짜리 CSV 파일을 단순히 필터링하기 위해 Pandas로 불러오고 있다면, 시작부터 전체 비용을 지불하고 있는 셈입니다. 대신 먼저 DuckDB에서 필터링을 수행하세요.

 

패턴

  • DuckDB를 사용해 스캔 → 필터링 → 필요한 컬럼만 선택
  • 최종 단계에서만 작은 결과를 Pandas 또는 Polars로 변환
import duckdb

con = duckdb.connect()

df = con.execute("""
    SELECT user_id, event_time, event_type
    FROM read_parquet('events/*.parquet')
    WHERE event_time >= now() - INTERVAL '30 days'
      AND event_type IN ('purchase', 'refund')
""").df()

 

이것은 “SQL 대 Python”의 문제가 아닙니다. 엔진과 인터프리터의 차이입니다. 대용량 데이터 처리는 엔진에 맡기고, Python은 마지막 단계의 활용성과 편의성을 담당하도록 하세요.

팁 2: Parquet을 우선 사용하고, Predicate Pushdown이 일을 하게 하라

DuckDB는 읽지 않아도 되는 데이터를 건너뛸 수 있을 때 가장 뛰어난 성능을 발휘합니다. Parquet은 이를 가능하게 해주는 두 가지 핵심 기능을 제공합니다.

  • 컬럼 프루닝(Column Pruning): 필요한 컬럼만 읽기
  • 프레디케이트 푸시다운(Predicate Pushdown): min/max 통계를 이용해 불필요한 row group 건너뛰기

만약 원본 데이터가 대부분 CSV라면, 한 번 Parquet으로 변환한 뒤 계속 재사용하는 것을 고려해 보세요.

빠른 변환 워크플로

con.execute("""
    COPY (SELECT * FROM read_csv_auto('raw/events.csv'))
    TO 'curated/events.parquet' (FORMAT PARQUET);
""")

실제 현업에서는 이 한 가지 변화만으로도 “주피터 노트북이 더 이상 죽지 않는다”는 순간을 경험하는 경우가 많습니다.

팁 3: 모든 조인을 Pandas에서 처리하려고 하지 마라

Pandas의 merge()는 데이터 규모가 커질수록 빠르게 비용이 증가합니다. 특히 대용량 테이블을 조인하거나 여러 번의 조인을 수행할 때 더욱 그렇습니다. DuckDB의 조인 알고리즘은 분석 워크로드를 위해 설계되었기 때문에 이러한 작업을 훨씬 효율적으로 처리할 수 있습니다.

query = """
SELECT
  o.order_id,
  o.user_id,
  o.total_amount,
  u.country
FROM read_parquet('curated/orders.parquet') o
JOIN read_parquet('curated/users.parquet') u
  ON o.user_id = u.user_id
WHERE o.order_date >= DATE '2025-01-01'
"""
result = con.execute(query).df()

파이프라인에 3~5개의 조인이 포함되어 있다면, 먼저 DuckDB에서 조인을 수행하는 것이 좋습니다. 데이터가 이미 원하는 형태로 가공된 이후에만 Python으로 가져오세요.

팁 4: 반복적인 스캔을 피하기 위해 임시 테이블을 활용하라

노트북의 여러 셀에서 동일한 파일을 반복적으로 스캔하는 것은 눈에 잘 띄지 않는 성능 비용입니다.

패턴

  • 기본이 되는 데이터 범위(base slice)를 TEMP 테이블 또는 VIEW로 생성
  • 이후 분석은 모두 이를 기반으로 수행
con.execute("""
CREATE TEMP VIEW recent_events AS
SELECT *
FROM read_parquet('events/*.parquet')
WHERE event_time >= now() - INTERVAL '90 days';
""")

daily = con.execute("""
SELECT date_trunc('day', event_time) AS day, count(*) AS n
FROM recent_events
GROUP BY 1
ORDER BY 1
""").df()

이렇게 하면 노트북이 훨씬 안정적으로 동작합니다. 위로 스크롤한 뒤 셀을 다시 실행할 때마다 데이터셋을 처음부터 다시 읽고 처리하는 일이 줄어들기 때문입니다.

팁 5: EXPLAIN을 학술 도구가 아니라 손전등처럼 활용하라

이쯤 되면 이런 의문이 들 수 있습니다.

“DuckDB가 정말로 필터를 푸시다운하고 있는지 어떻게 알 수 있을까?”

답은 간단합니다. 실행 계획을 확인하면 됩니다.

plan = con.execute("""
EXPLAIN
SELECT *
FROM read_parquet('events/*.parquet')
WHERE event_type = 'purchase'
""").fetchall()

print(plan[0][1])

다음과 같은 신호를 확인해 보세요.

  • 스캔 단계 근처에서 필터가 적용되는지
  • 실제로 필요한 컬럼만 참조하는지
  • 조인 순서가 합리적으로 구성되어 있는지

쿼리 옵티마이저 전문가가 될 필요는 없습니다. 중요한 것은 “왜 모든 데이터를 읽고 있지?”와 같은 명백한 비효율을 발견하는 것입니다.

EXPLAIN은 성능 분석을 위한 학술적인 도구가 아니라, 쿼리가 실제로 무엇을 하고 있는지 비춰주는 손전등에 가깝습니다.

팁 6: 가능하면 Arrow 형식으로 결과를 유지하라 (복사 감소, 예측 가능한 메모리 사용)

분석 파이프라인에서 의외로 자주 발생하는 병목은 계산(computation)이 아니라 데이터 변환(conversion)입니다. DuckDB → Pandas → NumPy를 반복적으로 오가다 보면 불필요한 메모리 복사와 메모리 사용량 증가가 발생할 수 있습니다.

만약 후속 처리 스택이 Arrow 또는 Polars를 지원한다면, 가능한 한 해당 경로를 사용하는 것이 좋습니다. 이렇게 하면 데이터 복사를 줄이고 더 효율적인 데이터 전달이 가능합니다.

개념적으로 살펴보면 다음과 같습니다.

  • DuckDB는 컬럼 기반(columnar) 결과를 매우 효율적으로 생성합니다.
  • Arrow는 이러한 컬럼형 메모리 구조를 유지한 채 다른 도구로 전달할 수 있습니다.
  • 따라서 불필요한 데이터 변환 없이 빠르게 후속 처리를 수행할 수 있습니다.

대규모 피처 엔지니어링(feature engineering)을 수행하는 팀이라면 Arrow 또는 Polars를 “쿼리 이후(post-query)” 처리 계층으로 사용하는 것을 고려해 보세요. 그리고 Pandas는 실제로 필요한 경우에만 사용하는 것이 좋습니다. 예를 들어 특정 시각화 라이브러리를 사용하거나, 기존 레거시 코드와 연동해야 하는 경우가 이에 해당합니다.

성능 최적화는 단순히 계산 속도를 높이는 것이 아닙니다. 데이터를 얼마나 적게 복사하느냐도 중요한 요소입니다.

팁 7: 반복 가능한 파이프라인을 위해 COPY와 CREATE TABLE AS를 활용하라

분석 파이프라인이 느려지는 가장 흔한 이유 중 하나는 실행할 때마다 원본 데이터부터 모든 작업을 다시 수행하기 때문입니다. DuckDB는 중간 결과를 정제된 테이블이나 파일로 저장하여 재사용할 수 있도록 해줍니다.

정제된 테이블(Materialized Table) 생성

con.execute("""
CREATE TABLE fact_purchases AS
SELECT *
FROM read_parquet('events/*.parquet')
WHERE event_type = 'purchase';
""")

안정적인 결과물을 파일로 저장

con.execute("""
COPY (
  SELECT user_id, sum(amount) AS revenue
  FROM fact_purchases
  GROUP BY 1
)
TO 'marts/user_revenue.parquet' (FORMAT PARQUET);
""")

이러한 방식은 단순한 성능 최적화 기법이 아닙니다. 분석 결과를 재현 가능하게 만들고, 파이프라인을 더 안정적으로 운영하기 위한 핵심 패턴입니다.

월요일에 빠르게 실행되던 노트북이 금요일에도 여전히 빠르게 실행되기를 원한다면, 매번 원본 데이터를 다시 처리하는 대신 중간 결과를 물리적으로 저장하는 습관을 들이세요.

팁 8: 데이터 타입을 의도적으로 관리하라 (“자동 추론”이 항상 최선은 아니다)

자동 타입 추론은 편리하지만, 분석 성능은 일관성을 좋아합니다.

  • 안정적인 숫자형 데이터 타입
  • 정리된 타임스탬프 형식
  • 통제된 범주형 데이터

예를 들어 숫자가 문자열로 저장되어 있거나, 타임스탬프 형식이 데이터마다 다르다면 분석할 때마다 이를 수정하려고 하지 말고 적재(ingestion) 단계에서 한 번에 정리하는 것이 좋습니다.

실용적인 규칙

조인, 필터링, 그룹화에 사용되는 컬럼이라면 가능한 한 빨리 올바른 데이터 타입으로 변환하고 유지하세요.

예를 들어 다음과 같은 컬럼들이 대표적입니다.

  • user_id
  • product_id
  • event_time
  • order_date
  • country_code

이러한 컬럼의 타입이 일관되지 않으면 DuckDB는 쿼리 실행 과정에서 암묵적인 형 변환을 수행해야 하며, 이는 성능 저하뿐 아니라 예상치 못한 결과를 초래할 수 있습니다.

이 팁은 극단적인 성능 튜닝을 위한 미세 최적화가 아닙니다. 오히려 불필요한 형 변환 비용을 줄이고, 새벽 2시에 발생하는 이해하기 어려운 오류와 예외 상황을 예방하기 위한 실용적인 습관에 가깝습니다.

좋은 데이터 타입 설계는 빠른 분석의 시작점이자, 안정적인 분석 파이프라인의 기반입니다.

팁 9: 필요 이상의 데이터를 가져오지 마라 — 정말 필요한 컬럼만 조회하라

당연한 이야기처럼 들릴 수 있지만, 분석 성능을 개선하는 데 있어 가장 투자 대비 효과(ROI)가 높은 습관 중 하나입니다.

좋지 않은 패턴

SELECT *로 모든 컬럼을 가져온 뒤 Python에서 80%를 버리는 것

좋은 패턴

처음부터 필요한 컬럼만 선택하는 것

con.execute("""
SELECT user_id, event_time, amount
FROM read_parquet('events/*.parquet')
WHERE event_type = 'purchase'
""")

불필요한 컬럼 하나하나는 다음과 같은 비용을 발생시킵니다.

  • 추가적인 디스크 I/O
  • 더 많은 메모리 사용
  • 데이터 디코딩을 위한 추가 CPU 작업

특히 Parquet과 같은 컬럼 지향 형식에서는 필요한 컬럼만 읽을 수 있다는 것이 큰 장점입니다. 하지만 SELECT *를 사용하면 이러한 장점을 스스로 포기하게 됩니다.

사용자 행동을 분석하고 있는데도 “혹시 나중에 필요할지 몰라서” 40개의 메타데이터 컬럼을 함께 가져오는 경우가 많습니다. 그러나 대부분은 실제 분석에 사용되지 않고 메모리와 처리 시간만 낭비하게 됩니다.

분석 성능을 높이는 가장 쉬운 방법 중 하나는 단순합니다.

필요한 컬럼만 읽고, 나머지는 읽지 마세요.

팁 10: 쿼리만 측정하지 말고 전체 워크플로를 벤치마크하라

DuckDB가 쿼리를 300ms 만에 실행했는데도 노트북이 느리게 느껴질 수 있습니다. 이유는 종종 쿼리 자체가 아니라 그 이후 과정에 있기 때문입니다.

예를 들어 다음과 같은 상황이 있습니다.

  • 거대한 결과를 Pandas로 변환한다.
  • Python에서 정렬을 수행한다.
  • 한 번에 너무 많은 데이터를 시각화한다.
  • 원본 데이터를 다시 스캔하는 셀을 반복 실행한다.

따라서 성능 측정은 쿼리만이 아니라 전체 파이프라인을 대상으로 해야 합니다.

간단한 측정 패턴

import time

t0 = time.time()
df = con.execute("""
  SELECT user_id, sum(amount) AS revenue
  FROM read_parquet('events/*.parquet')
  WHERE event_type = 'purchase'
  GROUP BY 1
""").df()
t1 = time.time()

print("Query + to_df seconds:", round(t1 - t0, 3))
print("Rows:", len(df))

만약 측정 결과에서 Pandas 변환 과정이 대부분의 시간을 차지한다면, SQL을 더 복잡하게 최적화하려고 할 필요는 없습니다. 대신 워크플로 자체를 바꾸는 것이 더 효과적입니다.

  • 결과 크기를 줄인다.
  • 더 많은 집계를 DuckDB 내부에서 수행한다.
  • 결과를 더 오랫동안 컬럼형(Arrow, Polars) 상태로 유지한다.

이것이 진짜 최적화입니다.

많은 사람들이 쿼리 실행 시간을 밀리초 단위로 줄이는 데 집중하지만, 실제 체감 성능은 데이터 이동과 변환 과정에서 결정되는 경우가 더 많습니다. 가장 빠른 데이터는 읽지 않는 데이터이고, 가장 빠른 변환은 수행하지 않는 변환입니다.

작은 사례 연구: "Pandas 우선(Pandas-first)" 함정

흔히 볼 수 있는 파이프라인은 다음과 같습니다.

  • 원본 파일을 Pandas로 읽기
  • Pandas에서 필터링 및 조인
  • groupby 및 집계
  • 내보내기

처음에는 잘 작동합니다. 하지만 어느 순간부터는 그렇지 않게 됩니다. 그러면 더 많은 RAM을 투입하게 되지만, 실행 시간은 여전히 늘어납니다.

팀들이 다음과 같이 방향을 바꾸면:

  • DuckDB에서 원본 파일 스캔
  • DuckDB에서 필터링/조인/집계
  • 최종 데이터셋만 Python으로 내보내기

보통 다음과 같은 효과를 보게 됩니다.

  • 메모리 부족(OOM) 오류 감소
  • 더 빠른 반복 작업 주기
  • 더 재현 가능한 노트북 (상태 혼란 감소)

그 이유는 DuckDB가 마법이어서가 아닙니다. 엔진이 가장 잘하는 일을 엔진이 수행하고 있기 때문입니다.

결론: DuckDB는 "더 빠른 SQL"이 아니라 워크플로 업그레이드다

Python 분석에서 DuckDB의 핵심은 Pandas를 대체하는 것이 아닙니다. 비싼 작업이 어디에서 수행되는지를 바꾸는 것입니다.

오늘 단 한 가지를 실천한다면, 이것을 해보세요.

가장 느린 노트북을 하나 고릅니다.

처음으로 "너무 많은" 데이터를 불러오는 지점을 찾습니다.

그 필터링, 조인 또는 집계 작업을 DuckDB로 옮깁니다.

그런 다음 다시 실행해 보세요. 더 이상 노트북과 싸우지 않아도 되는 조용한 만족감을 느끼게 될 것입니다.

이번 주에 DuckDB 기반 파이프라인을 구축했다면, 사용한 데이터셋 크기와 실행 시간의 "이전 → 이후" 변화를 댓글로 공유해 주세요. 그리고 이런 종류의 글—성능 최적화, 분석 엔지니어링, 실전 Python—을 더 보고 싶다면 계속 함께해 주세요.

 

<출처: https://medium.com/@ThinkingLoop/10-duckdb-tricks-to-supercharge-python-analytics-df55103836f7>

댓글