pandas 대신 DuckDB를 쓸 시점, 조인·집계·메모리 기준으로 정리

pandas 대신 DuckDB를

결론부터 말씀드리면, pandas 대신 DuckDB를 쓸 시점은 데이터 크기가 아니라 작업의 종류로 판단하는 게 정확합니다. 단일 파일을 훑어보는 수준이라면 pandas로 충분하지만, 여러 테이블을 조인하거나 그룹 집계를 반복하는 순간, 혹은 데이터가 메모리 용량에 슬슬 닿기 시작하는 시점부터는 DuckDB 쪽이 코드도 짧아지고 처리도 안정적입니다. 이 글에서는 조인, 집계, 메모리라는 세 기준으로 전환 시점을 구체적인 코드와 함께 짚어보겠습니다.

DuckDB가 뭐길래 pandas 코드를 그대로 읽나요

DuckDB는 서버 설치 없이 파이썬 프로세스 안에서 바로 동작하는 임베디드 분석용 데이터베이스입니다. SQLite가 트랜잭션 처리(OLTP)에 최적화된 것과 달리, DuckDB는 집계·분석 질의(OLAP)에 특화된 컬럼형 엔진으로 설계됐습니다.

가장 실용적인 특징은 pandas DataFrame을 별도 변환 없이 테이블처럼 바로 조회할 수 있다는 점입니다. duckdb.sql() 함수 안에서 파이썬 변수명을 그대로 FROM 절에 적으면, DuckDB가 해당 DataFrame을 자동으로 인식해 SQL로 질의할 수 있습니다. 이 기능은 공식 가이드인 판다스 데이터프레임 조회 문서에 정리돼 있습니다.

import pandas as pd
import duckdb

df = pd.read_csv("orders.csv")

# df를 별도 등록 없이 그대로 SQL 테이블처럼 사용
result = duckdb.sql("""
    SELECT category, COUNT(*) AS cnt
    FROM df
    WHERE sales > 1000
    GROUP BY category
    ORDER BY cnt DESC
""").df()

print(result)

이 코드는 DuckDB 1.x 버전(2024년 6월 1.0 정식 출시 이후 계열) 기준으로 문제없이 동작하며, 결과는 .df() 호출로 다시 pandas DataFrame으로 받을 수 있어 기존 시각화 코드와 그대로 연결됩니다.

조인이 두 번 이상 겹치면 merge 코드가 무너지는 이유

노트북 화면에 파이썬 데이터 분석 코드를 작성하는 개발자

pandas에서 테이블 세 개 이상을 조인하려면 merge()를 연쇄적으로 호출해야 하고, 그때마다 컬럼명 충돌을 막기 위해 suffixes 옵션을 신경 써야 합니다. 조인 키가 늘어날수록 중간 결과 DataFrame이 계속 메모리에 쌓이는 구조입니다.

# pandas 방식 — 조인이 늘어날수록 중간 객체가 계속 생성됨
merged = orders.merge(customers, on="customer_id", how="inner")
merged = merged.merge(products, on="product_id", how="inner")
result = merged[merged["sales"] > 1000][["order_id", "sales", "customer_name"]]

# DuckDB 방식 — 한 번의 SQL로 옵티마이저가 조인 순서를 결정
result = duckdb.sql("""
    SELECT o.order_id, o.sales, c.customer_name
    FROM orders o
    JOIN customers c ON o.customer_id = c.customer_id
    JOIN products p ON o.product_id = p.product_id
    WHERE o.sales > 1000
""").df()

DuckDB 쪽은 조인 순서를 옵티마이저가 통계 기반으로 재배치하기 때문에, 조인 키 순서를 사람이 직접 고민하지 않아도 됩니다. 조인 테이블이 2개를 넘어가는 시점부터 SQL 한 블록으로 작성하는 쪽이 코드 가독성과 실행 안정성 모두에서 유리합니다.

메모리 한계에 닿는 시점을 판단하는 법

pandas는 연산 과정에서 원본 DataFrame과 중간 결과를 동시에 메모리에 올려두는 구조입니다. groupby().agg()처럼 그룹 수가 많은 집계를 수행할 때는 원본 크기의 몇 배에 달하는 메모리를 일시적으로 요구할 수 있습니다. 그래서 파일 크기가 실제 RAM의 30~50%를 넘어서면 스왑이 발생하거나 커널이 프로세스를 강제 종료하는 상황을 마주하게 됩니다.

DuckDB는 벡터화 실행 엔진을 기반으로 데이터를 스트리밍 방식으로 처리하고, 메모리가 부족해지면 디스크로 중간 데이터를 스필하는 방식을 점진적으로 지원해왔습니다. 다만 이 동작은 PRAGMA memory_limit 설정값과 디스크 여유 공간에 의존하므로, 무조건 메모리 부족 문제가 해결된다고 보장하지는 않습니다.

판단 기준 pandas DuckDB
조인 2개 이상 merge 체이닝 필요 단일 SQL로 처리
그룹 집계 컬럼 다수 메모리 스파이크 발생 가능 스트리밍 집계
파일 크기 > RAM OOM 위험 스필 옵션으로 완화 가능
단일 열 변환·필터 충분히 빠름 차이 크지 않음

Parquet 여러 개를 한 번에 읽을 때의 차이

데이터베이스 성능 비교 벤치마크 차트

로그 파일이나 일자별로 쪼개진 Parquet가 수십~수백 개 쌓여 있는 경우, pandas는 pd.read_parquet()를 파일마다 호출한 뒤 pd.concat()으로 합쳐야 합니다. 이 과정에서 모든 파일이 한 번에 메모리에 올라갑니다.

# DuckDB는 glob 패턴으로 여러 Parquet를 한 번에 스캔
result = duckdb.sql("""
    SELECT date, SUM(revenue) AS daily_revenue
    FROM read_parquet('data/logs_*.parquet')
    GROUP BY date
    ORDER BY date
""").df()

read_parquet에 와일드카드를 넘기면 DuckDB가 필요한 컬럼과 조건에 맞는 행만 골라 읽는 프로젝션·필터 푸시다운을 적용합니다. 즉 GROUP BY에 쓰이지 않는 컬럼은 애초에 디스크에서 읽지 않으므로, 파일 전체를 pandas로 로드하는 것보다 I/O 자체가 줄어듭니다.

DuckDB가 오히려 손해인 경우

모든 상황에서 DuckDB가 유리한 건 아닙니다. 행 단위로 반복하며 조건부 로직을 적용하는 작업, 예를 들어 이전 행 값을 참조해 순차적으로 누적 계산을 하는 로직은 SQL로 표현하기 번거롭고 pandas의 apply()나 벡터 연산이 더 직관적입니다.

데이터가 애초에 수만 행 이하로 작고 한 번만 쓰고 버리는 스크립트라면, SQL 쿼리를 새로 작성하는 학습 비용이 pandas 한 줄보다 더 클 수 있습니다. 또한 DuckDB는 단일 프로세스 내에서 동작하는 구조라, 여러 대의 서버로 분산 처리해야 하는 수억 행 규모의 배치라면 Spark 같은 분산 처리 프레임워크가 더 적합합니다. 즉 pandas 대신 DuckDB를 선택하는 기준은 “크다/작다”가 아니라 “조인·집계가 반복되는가, 한 서버 메모리 안에서 끝나는 작업인가”로 좁혀서 판단하는 게 맞습니다.

지금 쓰는 pandas 코드에 한 줄만 추가해서 확인하는 법

기존 pandas 파이프라인을 당장 갈아엎을 필요는 없습니다. import duckdb만 추가한 뒤, 메모리 부담이 큰 groupby나 merge 구간만 duckdb.sql("... FROM 기존_df ...")로 바꿔 결과를 비교해보는 방식으로 시작할 수 있습니다. 조인 테이블이 2개를 넘거나, 집계 결과를 보기까지 체감상 오래 걸리거나, 메모리 경고가 뜨기 시작하는 시점이 바로 pandas 대신 DuckDB를 검토할 시점입니다. 그 전까지는 지금 쓰는 pandas 코드를 유지하는 편이 더 실용적입니다.

Parquet CSV 전환 시점 판단법: 용량·읽기 속도·스키마

Leave a Comment