실무에서 대용량 CSV 데이터를 처리하던 중,
기존에 사용하던 Pandas가 점점 느려지고 리소스(CPU, 메모리) 소모가 커지면서
보다 효율적인 데이터 처리 라이브러리를 찾게 되었습니다.
이후 여러 대안을 살펴보다가,
최근 빠른 속도로 주목받고 있는 Polars를 직접 사용해보기로 결정했습니다.

Rust로 개발된 고성능 멀티스레드 기반의 열 지향(columnar) 데이터프레임 라이브러리입니다.
Polars는 이름에서도 알 수 있듯이 Pola + rs(Rust), 즉 Rust 언어로 구현되었음을 강조하고 있습니다.
Rust는 메모리 안전성과 성능을 모두 갖춘 시스템 프로그래밍 언어로, 다음과 같은 특징들이 Polars의 성능에 직접적으로 기여합니다:
이러한 Rust의 특성 덕분에 Polars는 대규모 데이터 처리 시에도 높은 성능과 안정성을 보장합니다.
Apache Arrow는 열(columnar) 기반 데이터를 효율적으로 처리하기 위한, 언어에 구애받지 않는 오픈소스 소프트웨어 프레임워크입니다.
Polars는 이 Apache Arrow 메모리 모델을 기반으로 다음과 같은 최적화를 구현합니다:
Arrow의 핵심 장점 중 하나는 zero-copy 데이터 공유가 가능하다는 점으로,
이를 통해 직렬화/역직렬화 과정 없이 여러 코어나 프로세스 간에 빠른 데이터 전달이 가능하여,
병렬 처리 성능과 효율성이 크게 향상됩니다.
또한, 최근 Pandas(2.0 이후), Dask, Ray 등 주요 오픈소스 프로젝트들이 Apache Arrow를 채택하고 있으며,
이들을 위해 개발된 Python용 구현체인 PyArrow가 널리 사용되고 있습니다.
Polars는 내부적으로 Rust로 개발된 Arrow 구현체를 사용하여,
ArrowTable 형태로 다른 오픈소스와의 데이터 호환성을 높이고 있습니다.
Polars는 로컬 파일, 클라우드 스토리지, 데이터베이스 등 다양한 데이터 소스와의 연동을 지원하며,
이와 관련된 IO 성능 또한 매우 우수한 편입니다.
*와 같은 Globs 패턴을 사용할 수 있어, 여러 파일을 쉽게 병합할 수 있습니다. polars.DataFrame 형태로 반환받을 수 있는 read_database() 함수도 매우 유용하게 사용됩니다.아래는 실제로 테스트해 본 IO 성능 비교 결과입니다.
Polars의 Lazy API는 데이터를 즉시 연산하지 않고,
먼저 쿼리 계획(Query Plan)을 수립한 후,
최적의 시점에 한 번에 연산을 수행하는 지연 평가(Lazy Evaluation) 방식을 따릅니다.
이 방식의 장점은 다음과 같습니다:
Polars에서는 일반적인 polars.DataFrame 외에도 polars.LazyFrame 타입을 제공하며,
LazyFrame은 실제 데이터를 즉시 처리하지 않고 연산 계획만 구성한 뒤,
최종 결과가 필요할 때 collect()를 호출해 한 번에 실행됩니다.
이를 통해 복잡한 데이터 파이프라인도 효율적이고 유연하게 구성할 수 있습니다.
import polars as pl
import time
# 샘플 데이터 생성
df = pl.DataFrame({
"id": range(1_000_000),
"value": range(1_000_000)
})
# Eager 방식
start = time.time()
result_eager = df.filter(pl.col("value") > 500_000).select(pl.col("value") * 2)
print(f"Eager 실행 시간: {time.time() - start:.4f}초")
# Lazy 방식
start = time.time()
result_lazy = (
df.lazy()
.filter(pl.col("value") > 500_000)
.select(pl.col("value") * 2)
.collect()
)
print(f"Lazy 실행 시간: {time.time() - start:.4f}초")
동일한 연산을 Polars에서 Lazy 방식과 Eager 방식으로 처리한 결과
→ Lazy 방식에서 훨씬 빠르고 적은 리소스를 사용함을 확인할 수 있었습니다.
이처럼 Lazy 연산의 성능이 뛰어난 이유는 내부적으로 다양한 쿼리 최적화가 적용되기 때문입니다.
이번 글에서는 그중에서도 핵심 최적화 기법인 푸시다운(Pushdown)에 대해 간략히 소개합니다.
푸시다운은 쿼리 연산을 스토리지 계층 가까이로 내리는 기법으로,
데이터를 읽는 비용을 최소화하는 데 효과적입니다.
Polars에서 주로 적용되는 푸시다운 기법은 다음과 같습니다:
더 자세한 내용
📘 Polars 공식 User Guide - Lazy Optimizations를 참고하세요.
Polars는 Streaming API를 통해 Out-of-Core 방식의 연산을 지원합니다.
여기서 Out-of-Core란 메모리에 전부 담기 어려운 대용량 데이터를 처리할 때 사용하는 기법으로,
데이터를 일정 단위로 잘라 디스크나 네트워크에서 불러오며 처리하는 방식입니다.
즉, 전체 데이터를 한 번에 메모리에 올리지 않고 조각 단위로 처리하고 결과를 이어붙이는 방식이죠.

✅ 사용법 예시
# 기존 Lazy 연산 방식
df.lazy().group_by(...).agg(...).collect(streaming=True)
collect() 호출 시 streaming=True 옵션을 추가하면 Streaming API가 활성화됩니다.
| 연산 방식 | 메모리 사용량 (Peak) | 실행 시간 |
|---|---|---|
| Streaming API | 3.71 GB | 6.8 초 |
| 일반 Lazy API | 약 5배 더 많은 데이터 처리 시, 더 많은 메모리 소모 및 느림 | |
| Pandas | 처리 불가 (메모리 부족) | - |
Polars 공식에 따르면, Streaming 기능을 활용하면 개인용 노트북에서도 200GB 규모의 데이터 처리가 가능하다고 합니다.
Pandas에 익숙한 사용자 입장에서 Polars를 처음 접했을 때 가장 인상적이었던 점은
처리 속도와 메모리 효율성입니다.
아래는 동일한 CSV 데이터를 로드하고, 특정 조건으로 필터링 후 그룹화하여 평균을 계산하는 작업을
Pandas와 Polars로 비교한 예시입니다.
large_dataset.csvimport pandas as pd
df = pd.read_csv("large_dataset.csv")
filtered = df[df["category"] == "A"]
grouped = filtered.groupby("date")["value"].mean().reset_index()
import polars as pl
df = pl.read_csv("large_dataset.csv")
result = (
df
.filter(pl.col("category") == "A")
.group_by("date")
.agg(pl.col("value").mean().alias("mean_value"))
)
lazy_df = (
pl.read_csv("large_dataset.csv").lazy()
.filter(pl.col("category") == "A")
.group_by("date")
.agg(pl.col("value").mean().alias("mean_value"))
)
result = lazy_df.collect(streaming=True)
| 항목 | Pandas | Polars (Eager) | Polars (Lazy + Streaming) |
|---|---|---|---|
| 실행 시간 | 12.4초 | 3.1초 | 1.7초 |
| 최대 메모리 사용량 | 3.4GB | 1.2GB | 0.7GB |
| 코드 가독성 | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
| 병렬 처리 지원 | ❌ 없음 | ⭕ 일부 지원 | ⭕⭕ 적극 지원 |