Pandas가 느리다고 바로 Spark로 넘어가야 할까? Polars·DuckDB로 10억 행을 5초 만에 처리한 벤치마크

들어가며
데이터가 수십 GB로 커지면 Pandas는 메모리가 부족하고 계산도 느려집니다. 이때 많은 팀이 Spark, Databricks, Snowflake, Dask 같은 분산 시스템으로 옮겨 갑니다.
그런데 정말 분산 시스템이 필요할까요? GeekNews에 소개된 글 「Pandas는 멸종해야 한다」 는 제목이 꽤 도발적이지만, 주장 자체는 차분합니다. 우리가 다루는 데이터 대부분은 빅데이터가 아니라 중간 규모 데이터(Medium Data) 이고, 이 정도는 Polars나 DuckDB 같은 단일 머신 도구로 충분히 빠르게 처리할 수 있다는 내용입니다.
핵심 요약
- 약 100GB 이하 작업은 분산 시스템 없이 Polars, DuckDB로 처리할 수 있습니다.
- Amazon Redshift 통계를 바탕으로 추산하면 테이블의 94.68%는 100GB 미만, 쿼리의 86.9%는 80GB 이하를 다룹니다. 다만 직접 잰 값이 아니라 가정을 적용한 계산입니다.
- 10억 행 CSV 벤치마크 결과는 Pandas 4분 28초, Polars 5.04초, DuckDB 5.19초입니다.
- Apache Arrow를 쓰면 코드를 전부 다시 쓰지 않고도 세 도구를 섞어 쓸 수 있습니다.
- 정답은 없습니다. 작업 특성과 전환 비용을 따져 직접 비교해 보는 것이 중요합니다.
1. Pandas와 분산 시스템 사이의 빈틈
분석 도구는 보통 이런 순서로 옮겨 갑니다.
Excel → Pandas → 분산 처리 도구
데이터가 GB 단위가 되면 Pandas로 넘어가고, 수십 GB에 이르면 메모리 부족, 느린 계산, 복잡한 API 같은 문제를 만납니다. 그러면 흔히 빅데이터용 도구를 다음 선택지로 봅니다.
문제는 Pandas가 버티지 못하는 규모와 분산 시스템이 정말 필요한 규모 사이의 간격이 점점 넓어지고 있다는 점입니다. 저자는 이 빈틈을 채울 도구로 Polars와 DuckDB를 제안합니다.
2. 우리 데이터는 정말 빅데이터일까?
저자는 Amazon의 2024년 논문 Why TPC is not enough: An analysis of the Amazon Redshift fleet에 공개된 운영 통계를 사용했습니다. 여기에 두 가지 가정을 적용합니다.
- Redshift 테이블의 평균 행 크기는 1KB입니다.
- 모든 클러스터는 머신 10대로 구성되고, 각 머신은 S3에서 초당 8GB를 읽습니다. 이 수치는 오래된 S3 벤치마크에서 가져왔습니다.
이 가정을 적용한 결과는 다음과 같습니다.
- 쿼리의 86.9% 가 1초 안에 끝납니다.
10대 × 8GB/s × 1초 = 80GB이므로 이 쿼리들은 80GB 이하를 처리한다고 볼 수 있습니다. - 테이블의 94.68% 는 1억 행 미만입니다. 행당 1KB로 계산하면 100GB 미만입니다.
- 행당 1KB가 너무 낙관적이라 10KB로 잡아도 1TB 수준입니다.
💡 참고: MotherDuck의 Jordan Tigani도 비슷한 분석을 내놓았습니다. 다만 MotherDuck은 DuckDB 호스팅을 파는 회사라는 점을 감안해서 읽어야 합니다.
3. 비교 대상과 실험 조건
| 도구 | 특징 | |---|---| | Pandas | 파이썬 데이터 분석의 사실상 표준 DataFrame 라이브러리 | | Polars | Rust로 만든 DataFrame 라이브러리이며, Pandas 사용자에게 익숙한 API를 제공 | | DuckDB | 분석용 SQLite라고 할 수 있는 인메모리 분석 데이터베이스 |
실험은 1 Billion Row Challenge를 바탕으로 했습니다. 기상 관측소 데이터 10억 행이 담긴 CSV에서 관측소별 최솟값, 평균, 최댓값을 구하는 대회입니다. 원래는 Java 대회이고, 가장 빠른 구현은 1.5초를 기록했습니다.
- 서버: AWS m7a.8xlarge (AMD 32코어, RAM 128GB, Debian 12). 원래 대회 환경과 사양은 비슷하지만 베어메탈이 아니라서 재현성에 차이가 있을 수 있습니다.
- 대회의 출력 형식이 비표준이라 출력 직렬화는 측정에서 뺐습니다.
4. 비슷한 코드가 왜 이렇게 다르게 돌까?
Pandas: 즉시 평가
CSV 전체를 읽고, 그룹화하고, 집계합니다. 각 단계가 바로 순서대로 실행되기 때문에 집계 전에 전체 데이터가 메모리에 올라갑니다.
Polars: 지연 평가와 스트리밍
scan_csv로 지연 평가 파이프라인을 만들고 .collect(new_streaming=True)를 호출할 때 실행합니다.
- 데이터를 청크 단위로 읽고 여러 스레드로 병렬 처리합니다.
- 전체 계산을 미리 알고 있어서 최적화된 쿼리 그래프를 만들 수 있습니다.
.explain(streaming=True)와.explain(streaming=True, optimized=False)로 최적화 전후 실행 계획을 비교할 수 있습니다.- 필터가 있는 쿼리라면 집계 전에 행을 걸러내는 조건절 푸시다운(predicate pushdown) 이 적용됩니다. 모든 행을 메모리에 올린 뒤 필터링하는 Pandas와 다른 점입니다.
DuckDB: 어디에 있는 데이터든 SQL로
데이터가 어디에 어떤 형태로 있든 SQL로 조회할 수 있습니다. 메모리에 있는 Python 객체도 쿼리할 수 있습니다. 엔진이 알아서 지연 평가, 멀티스레딩, 청크 처리를 적용하고, .explain()으로 실행 계획을 볼 수 있습니다.
5. 벤치마크 결과
직접 만든 벤치마크 도구로 각 스크립트를 새 Python 인터프리터에서 실행했습니다. psutil로 50ms마다 메모리와 CPU를 기록했고, 준비 실행 2회 후 30회 반복한 중앙값을 사용했습니다.
🖥️ 서버 (32코어, 128GB)
| 도구 | 실행 시간 | CPU | 메모리(USS) | 스왑 | |---|---|---|---|---| | Pandas | 4분 28초 | 113.0% | 38.12GB | 0MB | | Polars | 5.04초 | 3202.60% | 18.02GB | 0MB | | DuckDB | 5.19초 | 3174.64% | 1.93GB | 0MB |
- 두 대안 모두 손으로 최적화한 Java 구현에 가까운 속도를 냈습니다.
- 메모리는 Pandas 대비 Polars가 약 절반, DuckDB는 약 19분의 1입니다.
- Polars는 메모리 사용량이 여전히 높습니다. 일부 계산이 스트리밍되지 않았을 수 있지만 원인은 확인되지 않았습니다.
- DuckDB는 짧은 코드와 별도 튜닝 없이 가장 좋은 메모리 효율을 보였습니다.
💻 노트북 (Framework 13, i5-1135G7, 16GB)
| 도구 | 실행 시간 | CPU | 메모리(USS) | 스왑 | |---|---|---|---|---| | Pandas | 12분 15초 | 110.35% | 15.67GB | 21.02GB | | Polars | 39초 | 765.75% | 15.22GB | 35.85MB | | DuckDB | 47초 | 807.0% | 546.87MB | 0MB |
노트북에서도 차이가 큽니다. Pandas는 스왑을 21GB나 쓰면서 12분 넘게 걸렸고, Polars와 DuckDB는 1분 안에 끝났습니다. 로컬에서 코드를 고치고 다시 돌리는 개발 주기가 크게 짧아집니다.
6. 따로 신경 쓰지 않아도 얻는 최적화
Polars와 DuckDB는 다음 기능을 기본으로 제공합니다.
- ⚡ 자동 멀티스레딩: 스레드나 프로세스를 직접 관리하지 않아도 CPU 코어를 모두 씁니다.
- 🧠 메모리 효율과 스트리밍: 청크 단위로 처리해 메모리 사용량을 줄입니다.
- 🗺️ 지연 평가: 전체 계산을 먼저 정의하므로 조건절 푸시다운 같은 방식으로 실행 계획을 최적화합니다.
- 💾 디스크로 중간 결과 내보내기: RAM이 부족하면 중간 결과를 디스크로 옮깁니다. 보통 운영체제 스왑보다 효율적입니다.
📌 표준 비교는 TPC-H 벤치마크 결과에서도 볼 수 있습니다. 다만 이 벤치마크를 돌린 Coiled는 Dask 호스팅 회사로 Polars, DuckDB와 경쟁하는 입장입니다. 이해관계가 있다고 결과가 틀린 것은 아니지만, 벤치마크는 늘 비판적으로 읽을 필요가 있습니다.
7. Apache Arrow로 조금씩 옮겨 가기
기존 Pandas 코드를 한 번에 버리기는 어렵습니다. 이때 Apache Arrow를 쓰면 됩니다.
- Arrow는 컬럼형 데이터를 메모리에 표현하는 사실상의 표준입니다. Pandas를 처음 만든 Wes McKinney가 구상했습니다.
- Pandas는 2023년 4월 2.0부터 Arrow를 지원하고, Polars와 DuckDB는 Arrow를 기본으로 지원합니다.
- Arrow 기반 DataFrame은 세 도구 사이를 메모리 복사 없이 오갈 수 있습니다.
⚠️ 주의할 점: Pandas는 기본값으로 Arrow 기반 DataFrame을 만들지 않습니다. 생성할 때 꼭 지정해야 합니다.
df = pd.read_parquet(path, dtype_backend="pyarrow")
8. NYC 택시 데이터로 본 혼합 사용 효과
NYC Taxi 데이터셋에서 약 3GB 분량의 Parquet 파일을 골라 2019~2022년 팬데믹 기간에 현금 결제 비율이 줄었는지 분석했습니다. Pandas와 DuckDB로 읽기와 계산을 각각 구현해 조합했습니다. (Polars 버전은 없습니다.)
- Pandas 경로: 필요한 두 열을 Arrow 기반으로 읽어 합친 뒤, 날짜를 제한하고 연도와 월을 추출해 결제 방식별로 집계합니다.
- DuckDB 경로: SQL로 Parquet 파일을 조회하면서 필터링과 집계를 합니다.
.df()로 DataFrame을 만드는 시점에 실제 연산이 실행됩니다.
| 조합 | 실행 시간 | CPU | 메모리(USS) | 스왑 | |---|---|---|---|---| | Pandas만 사용 | 41.88초 | 146.10% | 14.52GB | 1.92GB | | DuckDB 읽기 + Pandas 계산 | 28.39초 | 793.7% | 14.79GB | 1.22GB | | Pandas 읽기 + DuckDB 계산 | 29.25초 | 765.4% | 12.39GB | 0MB | | DuckDB만 사용 | 21.70초 | 814.95% | 216.76MB | 0MB |
- 일부만 바꿔도 실행 시간이 30% 넘게 줄어듭니다.
- 다만 Pandas가 끼어 있는 조합은 메모리 사용량이 여전히 높습니다.
- DuckDB만 쓰면 CPU를 충분히 활용하면서 메모리는 약 200MB 수준으로 떨어집니다.
분석 기간 동안 현금 결제 비율은 실제로 줄었습니다. 하지만 상관관계가 인과관계는 아니므로 팬데믹 때문이라고 단정할 수는 없습니다.
9. 옮기기 전에 확인할 점
- 이 벤치마크는 개인이 수행한 비교입니다. 공개된 소스 코드를 직접 보고 결함이 없는지 확인하는 것이 좋습니다.
- Pandas 생태계에 깊이 묶여 있다면 전환 비용이 너무 클 수 있습니다.
- 그래도 Arrow 덕분에 전환 비용의 기준 자체가 크게 낮아졌습니다. 전부 옮기지 않고 병목 구간만 바꿔 보는 방법도 있습니다.
- Pandas도 계속 개선되고 있습니다.
에디터 한마디 ✍️
제목과 달리 이 글은 Pandas를 당장 버리라는 이야기가 아닙니다. Pandas가 느리다는 이유만으로 분산 시스템을 도입하지는 말자는 이야기입니다.
수십 GB 데이터 때문에 Spark 클러스터를 검토하고 있다면, 먼저 노트북에 pip install duckdb polars를 설치하고 병목이 되는 부분 하나만 바꿔 보세요. 몇 분 걸리던 작업이 몇 초 만에 끝날 수도 있고, 인프라 비용과 운영 부담도 줄일 수 있습니다.