•7 min read

Khoa học Dữ liệu Tốc độ Cao: DuckDB và Polars cho Phân tích Hiệu suất Cao

Khoa học Dữ liệu Tốc độ Cao: DuckDB và Polars cho Phân tích Hiệu suất Cao

Nếu bạn vẫn đang dùng Pandas để biến đổi dữ liệu nhiều gigabyte trong Python, bạn đang phải trả một "thuế Pandas" khổng lồ: thực thi CPU đơn luồng, sao chép bộ nhớ tức thì, và phình to bộ nhớ đòi hỏi RAM gấp 5 đến 10 lần kích thước tập dữ liệu thô trước khi báo lỗi OutOfMemoryError.

Trong các pipeline dữ liệu hiện đại, Polars và DuckDB đã thay thế Pandas trở thành bộ công cụ tiêu chuẩn cho phân tích hiệu suất cao trên một node. Cả hai đều tận dụng lưu trữ theo cột, thực thi SIMD vector hóa, và Apache Arrow, mang lại tốc độ nhanh hơn 10 đến 50 lần trong khi xử lý các tập dữ liệu lớn hơn RAM vật lý.

Dưới đây là cách chúng hoạt động, cách chúng so sánh, và cách sử dụng chúng cùng nhau trong môi trường sản xuất.


Audio Briefing
0:00 / 0:00

Vấn đề cốt lõi với Pandas

Dataset on Disk (Parquet/CSV): 2.0 GB
Pandas in RAM:                12.0 GB - 18.0 GB (Peak during merge/groupby)
Polars in RAM:                 2.3 GB (Zero-copy Arrow + streaming)
DuckDB in RAM:                 0.8 GB (Chunked vectorized execution engine)

Pandas được thiết kế vào năm 2008 xoay quanh các mảng NumPy 1D. Nó có ba nút thắt cổ chai cơ bản:

  1. GIL và Đơn luồng: Các hoạt động chạy trên một lõi CPU duy nhất trừ khi sử dụng các wrapper bên ngoài.
  2. Đánh giá tức thì (Eager Evaluation): Mỗi bước trung gian tạo ra một bản sao DataFrame hoàn toàn mới trong bộ nhớ.
  3. Chi phí dữ liệu thiếu: Pandas theo lịch sử đã chuyển đổi các cột số nguyên với NaN sang float64, làm tăng gấp đôi mức sử dụng bộ nhớ.

Advertisement

1. Polars: Engine DataFrame được hỗ trợ bởi Rust

Polars được viết lại từ đầu bằng Rust và được xây dựng trực tiếp trên định dạng bộ nhớ cột Apache Arrow. Nó xử lý dữ liệu bằng cách sử dụng tất cả các lõi CPU có sẵn mà không bị hạn chế bởi Python GIL.

Thực thi tức thì (Eager) so với Thực thi lười (Lazy)

Trong môi trường sản xuất, bạn hầu như luôn nên sử dụng API Lazy của Polars (LazyFrame). Thay vì chạy các phép tính ngay lập tức, Polars xây dựng một Kế hoạch Logic, tối ưu hóa nó (predicate pushdown, projection pushdown, slice pushdown), và thực thi nó song song:

import polars as pl

# Construct lazy query plan — zero disk I/O occurs here
lazy_query = (
    pl.scan_parquet("s3://analytics-bucket/events/*.parquet")
    .filter(pl.col("timestamp") >= pl.date(2026, 1, 1))
    .filter(pl.col("event_type").is_in(["purchase", "subscription"]))
    .with_columns([
        (pl.col("amount_cents") / 100.0).alias("amount_usd"),
        pl.col("user_id").n_unique().over("country").alias("unique_users_per_country")
    ])
    .group_by(["country", "event_type"])
    .agg([
        pl.col("amount_usd").sum().alias("total_revenue"),
        pl.col("amount_usd").mean().alias("avg_order_value"),
        pl.len().alias("transaction_count")
    ])
    .sort("total_revenue", descending=True)
)

# Inspect the optimized query plan
print(lazy_query.explain())

# Execute optimized plan in parallel across all CPU cores
result_df = lazy_query.collect(streaming=True)
print(result_df)

Tại sao tối ưu hóa truy vấn của Polars lại quan trọng

Khi bạn gọi .scan_parquet() với các bộ lọc:

  • Predicate Pushdown: Polars kiểm tra footer metadata của Parquet và bỏ qua toàn bộ các nhóm hàng không khớp với timestamp >= 2026-01-01 mà không đọc dữ liệu từ đĩa.
  • Projection Pushdown: Polars chỉ đọc 4 cột được tham chiếu trong truy vấn (timestamp, event_type, amount_cents, country), bỏ qua 50 cột còn lại trong tệp.
  • Streaming Engine (streaming=True): Xử lý dữ liệu theo các micro-batch streaming, cho phép biến đổi trên các tập dữ liệu vượt quá RAM vật lý của máy bạn.

2. DuckDB: "SQLite cho phân tích theo cột"

Trong khi Polars cung cấp API DataFrame, DuckDB là một cơ sở dữ liệu OLAP SQL nhúng trong tiến trình. Nó chạy bên trong tiến trình Python của bạn mà không có bất kỳ phụ thuộc máy chủ bên ngoài nào, không có độ trễ mạng, và hỗ trợ ngôn ngữ SQL gốc.

Truy vấn trực tiếp Parquet và S3 từ xa bằng SQL

DuckDB có thể thực thi các truy vấn SQL trực tiếp trên các tệp Parquet, CSV, hoặc JSON đã nén trên đĩa hoặc S3 từ xa mà không cần tải chúng vào các bảng cơ sở dữ liệu trước:

import duckdb

# Connect to in-process DuckDB instance (or persist to 'analytics.duckdb')
con = duckdb.connect()

# Enable S3 / HTTP filesystem extension
con.execute("INSTALL httpfs; LOAD httpfs;")
con.execute("""
    SET s3_region='us-east-1';
    SET s3_access_key_id='YOUR_KEY';
    SET s3_secret_access_key='YOUR_SECRET';
""")

# Query 100GB of remote Parquet files using Vectorized SQL
query = """
    SELECT 
        country,
        event_type,
        COUNT(DISTINCT user_id) AS unique_users,
        ROUND(SUM(amount_cents) / 100.0, 2) AS total_revenue_usd,
        ROUND(AVG(amount_cents) / 100.0, 2) AS aov_usd
    FROM read_parquet('s3://analytics-bucket/events/year=2026/*/*.parquet')
    WHERE event_type IN ('purchase', 'subscription')
    GROUP BY country, event_type
    HAVING total_revenue_usd > 10000
    ORDER BY total_revenue_usd DESC
    LIMIT 20;
"""

# Execute and fetch directly to Arrow, Polars, or Python dictionaries
results = con.execute(query).pl()  # Returns native Polars DataFrame
print(results)

3. Tương tác Zero-Copy: Polars + DuckDB + Apache Arrow

Vì cả Polars và DuckDB đều sử dụng Apache Arrow để biểu diễn bộ nhớ, bạn có thể truyền dữ liệu giữa chúng với không có chi phí tuần tự hóa hoặc sao chép bộ nhớ:

import polars as pl
import duckdb

# 1. Load and clean data with Polars
df = pl.DataFrame({
    "user_id": [101, 102, 103, 104],
    "scores": [88.5, 92.0, 79.5, 95.0],
    "tier": ["gold", "platinum", "gold", "platinum"]
})

# 2. Run complex analytical window SQL in DuckDB directly on the Polars DataFrame
con = duckdb.connect()

# DuckDB can reference the 'df' Python variable directly in the FROM clause!
sql_result = con.execute("""
    SELECT 
        user_id,
        scores,
        tier,
        RANK() OVER (PARTITION BY tier ORDER BY scores DESC) as rank_in_tier,
        AVG(scores) OVER (PARTITION BY tier) as tier_avg
    FROM df
""").arrow()  # Zero-copy Arrow Table

# 3. Convert back to Polars instantaneously
final_df = pl.from_arrow(sql_result)
print(final_df)

Advertisement

4. Benchmark: 10 triệu hàng (1.2 GB Parquet)

Tổng hợp và lọc một tập dữ liệu 10 triệu hàng trên một máy tính xách tay của nhà phát triển 8 lõi, 16GB RAM:

Thao tácPandas 2.2Polars (Eager)Polars (Lazy)DuckDB (SQL)
Đọc Parquet + Lọc4.82s0.82s0.29s0.31s
Group By + 4 Aggs3.15s0.41s0.28s0.24s
Window Function2.40s0.35s0.26s0.21s
Mức sử dụng RAM cao nhất~4.6 GB~1.4 GB~0.7 GB~0.4 GB

5. Ma trận quyết định kiến trúc: Khi nào sử dụng cái gì

                                Dataset Scale & Use Case
                                           │
         ┌─────────────────────────────────┴─────────────────────────────────┐
         ▼                                                                   ▼
  Single Machine (< 500GB)                                          Distributed Cluster (> 1TB)
         │                                                                   │
    ┌────┴──────────────────────────┐                              ┌─────────┴─────────┐
    ▼                               ▼                              ▼                   ▼
SQL-Heavy / S3 Parquet       DataFrame Transformations      Batch Pipeline       Real-time OLAP
    ▼                               ▼                              ▼                   ▼
 DuckDB                          Polars                       Apache Spark         ClickHouse
 (Embedded SQL)              (Rust Multi-thread)               / Ray               / StarRocks
  • Chọn Polars khi: Bạn đang viết các pipeline kỹ thuật tính năng, tiền xử lý dữ liệu ML, script ETL, hoặc logic dataframe thủ tục phức tạp nơi an toàn kiểu và biểu thức Pythonic phát huy tác dụng.
  • Chọn DuckDB khi: Bạn muốn SQL tiêu chuẩn, phân tích ad-hoc trên các tệp Parquet cục bộ/S3, bảng điều khiển phân tích nhúng trong ứng dụng, hoặc tích hợp với các công cụ BI thông qua các trình kết nối ODBC/JDBC/Python tiêu chuẩn.
  • Tránh Spark cho dữ liệu dưới 100GB: Một máy đa lõi hiện đại chạy Polars hoặc DuckDB thường nhanh hơn, rẻ hơn và dễ bảo trì hơn 10 lần so với việc khởi động một cụm Spark phân tán với JVM và chi phí tuần tự hóa mạng.

Bạn cũng có thể thích

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement