Polars로 대용량 데이터 빠르고 효율적으로 처리하기

조현민·2025년 4월 30일
post-thumbnail

참조 : https://techblog.woowahan.com/18632/

🐼 배경

실무에서 대용량 CSV 데이터를 처리하던 중,
기존에 사용하던 Pandas가 점점 느려지고 리소스(CPU, 메모리) 소모가 커지면서
보다 효율적인 데이터 처리 라이브러리를 찾게 되었습니다.

  • Pandas의 한계점
    • 처리 속도가 느림
    • 멀티코어 활용 부족
    • 메모리 사용량 많고, 대용량 처리 시 한계
    • 복잡한 문법으로 인해 가독성 저하

이후 여러 대안을 살펴보다가,
최근 빠른 속도로 주목받고 있는 Polars를 직접 사용해보기로 결정했습니다.


💡 Polars란?

Rust로 개발된 고성능 멀티스레드 기반의 열 지향(columnar) 데이터프레임 라이브러리입니다.

  • 대용량 데이터를 빠르고 효율적으로 처리할 수 있도록 설계됨
  • 직관적인 API, 낮은 메모리 사용량, 병렬 연산 지원
  • Pandas보다 훨씬 빠른 성능을 보이며, 특히 CSV/Parquet 등의 처리에 강력

📌 Polars는 왜 좋은가

🚀 Rust 기반의 고성능

Polars는 이름에서도 알 수 있듯이 Pola + rs(Rust), 즉 Rust 언어로 구현되었음을 강조하고 있습니다.
Rust는 메모리 안전성과 성능을 모두 갖춘 시스템 프로그래밍 언어로, 다음과 같은 특징들이 Polars의 성능에 직접적으로 기여합니다:

  • 불필요한 메모리 오버헤드 없이 정밀한 메모리 관리
  • 안전한 동시성(concurrency)효율적인 병렬 처리 지원
  • 메모리 캐싱 및 재사용에 유리한 구조

이러한 Rust의 특성 덕분에 Polars는 대규모 데이터 처리 시에도 높은 성능과 안정성을 보장합니다.


🧱 Apache Arrow 기반

Apache Arrow는 열(columnar) 기반 데이터를 효율적으로 처리하기 위한, 언어에 구애받지 않는 오픈소스 소프트웨어 프레임워크입니다.
Polars는 이 Apache Arrow 메모리 모델을 기반으로 다음과 같은 최적화를 구현합니다:

  • 메모리 상에서 데이터를 칼럼 구조로 정의
  • 이를 활용한 벡터화(vectorized) 연산
  • SIMD(Single Instruction Multiple Data) 명령어를 활용한 CPU 최적화

Arrow의 핵심 장점 중 하나는 zero-copy 데이터 공유가 가능하다는 점으로,
이를 통해 직렬화/역직렬화 과정 없이 여러 코어나 프로세스 간에 빠른 데이터 전달이 가능하여,
병렬 처리 성능과 효율성이 크게 향상됩니다.

또한, 최근 Pandas(2.0 이후), Dask, Ray 등 주요 오픈소스 프로젝트들이 Apache Arrow를 채택하고 있으며,
이들을 위해 개발된 Python용 구현체인 PyArrow가 널리 사용되고 있습니다.
Polars는 내부적으로 Rust로 개발된 Arrow 구현체를 사용하여,
ArrowTable 형태로 다른 오픈소스와의 데이터 호환성을 높이고 있습니다.


📂 IO 기능

Polars는 로컬 파일, 클라우드 스토리지, 데이터베이스 등 다양한 데이터 소스와의 연동을 지원하며,
이와 관련된 IO 성능 또한 매우 우수한 편입니다.

  • 기본적으로 CSV, JSON, Parquet, Avro 등 다양한 포맷에 대한 읽기/쓰기 기능을 제공합니다.
  • 파일을 읽을 때는 *와 같은 Globs 패턴을 사용할 수 있어, 여러 파일을 쉽게 병합할 수 있습니다.
  • 또한, 실무에서는 데이터베이스Trino 같은 쿼리 엔진에 SQL 쿼리를 제출하고,
    그 결과를 polars.DataFrame 형태로 반환받을 수 있는 read_database() 함수도 매우 유용하게 사용됩니다.

아래는 실제로 테스트해 본 IO 성능 비교 결과입니다.


💤 Lazy API와 쿼리 최적화

Polars의 Lazy API는 데이터를 즉시 연산하지 않고,
먼저 쿼리 계획(Query Plan)을 수립한 후,
최적의 시점에 한 번에 연산을 수행하는 지연 평가(Lazy Evaluation) 방식을 따릅니다.

이 방식의 장점은 다음과 같습니다:

  • 불필요한 중간 연산 제거로 메모리 사용량 최소화
  • 필터 푸시다운(Filter Pushdown), 프로젝션 푸시다운(Project Pushdown) 등 다양한 쿼리 최적화 적용
  • 필요한 데이터만 읽어 처리해 연산 속도 극대화

Polars에서는 일반적인 polars.DataFrame 외에도 polars.LazyFrame 타입을 제공하며,
LazyFrame은 실제 데이터를 즉시 처리하지 않고 연산 계획만 구성한 뒤,
최종 결과가 필요할 때 collect()를 호출해 한 번에 실행됩니다.

이를 통해 복잡한 데이터 파이프라인도 효율적이고 유연하게 구성할 수 있습니다.


🚀 Lazy API 성능 비교 예제

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)에 대해 간략히 소개합니다.


⚙️ 푸시다운(Pushdown)이란?

푸시다운은 쿼리 연산을 스토리지 계층 가까이로 내리는 기법으로,
데이터를 읽는 비용을 최소화하는 데 효과적입니다.

  • 대부분의 분석 엔진과 데이터베이스에서 사용하는 보편적인 최적화 방식이며,
  • Polars 역시 이 기법을 통해 불필요한 데이터 접근을 최소화합니다.

Polars에서 주로 적용되는 푸시다운 기법은 다음과 같습니다:

  1. Predicate Pushdown (Filter Pushdown)
    • 조건에 맞는 데이터만 읽도록 필터링을 데이터 읽기 단계에서 적용
  2. Project Pushdown
    • 필요한 열(column)만 불러와서 메모리와 IO 비용 절감
  3. Slice Pushdown
    • 일부 행(row)만 읽도록 제한

더 자세한 내용
📘 Polars 공식 User Guide - Lazy Optimizations를 참고하세요.

🚀 Out-of-Core 방식 (Streaming API)

Polars는 Streaming API를 통해 Out-of-Core 방식의 연산을 지원합니다.
여기서 Out-of-Core메모리에 전부 담기 어려운 대용량 데이터를 처리할 때 사용하는 기법으로,
데이터를 일정 단위로 잘라 디스크나 네트워크에서 불러오며 처리하는 방식입니다.
즉, 전체 데이터를 한 번에 메모리에 올리지 않고 조각 단위로 처리하고 결과를 이어붙이는 방식이죠.


✅ 사용법 예시

# 기존 Lazy 연산 방식
df.lazy().group_by(...).agg(...).collect(streaming=True)

collect() 호출 시 streaming=True 옵션을 추가하면 Streaming API가 활성화됩니다.


📊 Streaming API 성능 예시

연산 방식메모리 사용량 (Peak)실행 시간
Streaming API3.71 GB6.8 초
일반 Lazy API약 5배 더 많은 데이터 처리 시, 더 많은 메모리 소모 및 느림
Pandas처리 불가 (메모리 부족)-

📎 참고

Polars 공식에 따르면, Streaming 기능을 활용하면 개인용 노트북에서도 200GB 규모의 데이터 처리가 가능하다고 합니다.


🧪 Polars 사용 — Pandas와 비교

Pandas에 익숙한 사용자 입장에서 Polars를 처음 접했을 때 가장 인상적이었던 점은
처리 속도와 메모리 효율성입니다.

아래는 동일한 CSV 데이터를 로드하고, 특정 조건으로 필터링 후 그룹화하여 평균을 계산하는 작업을
Pandas와 Polars로 비교한 예시입니다.


📁 데이터: large_dataset.csv

  • 500만 건 레코드, 10개 컬럼
  • 연산: 특정 카테고리 필터 후 날짜별 그룹화 및 평균 계산

🐍 Pandas 코드

import pandas as pd

df = pd.read_csv("large_dataset.csv")
filtered = df[df["category"] == "A"]
grouped = filtered.groupby("date")["value"].mean().reset_index()
  • ✅ 익숙하고 간단한 문법
  • ❌ CPU 사용률 급증
  • ❌ 16GB RAM 환경에서 OOM(메모리 부족) 발생 가능성

🐻 Polars Eager API

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"))
)
  • ✅ 연산 속도 3~4배 빠름
  • ✅ 메모리 사용량 대폭 감소
  • ✅ 메서드 체이닝으로 코드 가독성 우수

💤 Polars Lazy + Streaming API

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 대비 5~8배 이상 빠름
  • ✅ Lazy Evaluation + Pushdown 최적화로 불필요한 중간 연산 최소화
  • ✅ Streaming 모드로 수 GB 대용량도 안정적 처리 가능

📊 성능 비교 (대략적 기준)

항목PandasPolars (Eager)Polars (Lazy + Streaming)
실행 시간12.4초3.1초1.7초
최대 메모리 사용량3.4GB1.2GB0.7GB
코드 가독성★★★☆☆★★★★☆★★★★☆
병렬 처리 지원❌ 없음⭕ 일부 지원⭕⭕ 적극 지원

📌 언제 무엇을 쓰면 좋을까?

✅ Pandas 추천

  • 데이터 크기 수만 행 이하
  • 익숙한 환경에서 빠른 프로토타이핑 필요 시
  • 다양한 라이브러리 연동 및 생태계 활용이 중요할 때

🚀 Polars 추천

  • 데이터 크기 수십만~수백만 행 이상
  • 반복 테스트 또는 대규모 배치 처리 시
  • 빠른 파이프라인 구축과 실시간 스트리밍 분석 필요 시
  • Lazy Evaluation 및 최적화 기능 활용 시
profile
주니어 개발자입니다.

0개의 댓글