Hướng dẫn dùng DuckDB với Python: Truy vấn file CSV/Parquet chục GB siêu tốc, không lo tràn RAM

Python tutorial - IT technology blog
Python tutorial - IT technology blog

Cơn ác mộng Out-of-Memory lúc 2 giờ sáng

Chuông PagerDuty réo liên hồi lúc nửa đêm. Con bot tổng hợp log giao dịch trên server staging bị Linux OOM Killer hạ gục. Kiểm tra terminal, mình thấy nguyên nhân quen thuộc: script Python đang dùng Pandas nạp thẳng file CSV 8.5GB (khoảng 35 triệu dòng) vào con VPS chỉ có 4GB RAM.

Trước đây, cách chữa cháy phổ biến là đẩy dữ liệu vào SQLite rồi chạy SQL. Nhưng với tác vụ gom nhóm (GROUP BY, AVG, COUNT DISTINCT) trên hàng chục triệu bản ghi, SQLite chạy rất chậm do lưu trữ theo dòng (row-oriented). Dùng Pandas thì sập RAM ngay tắp lự. Lúc này, giải pháp cứu cánh phù hợp nhất là DuckDB: một database nhúng nhỏ gọn như SQLite nhưng sở hữu sức mạnh xử lý phân tích (OLAP) vượt trội.

DuckDB có gì khác biệt so với SQLite?

Nhiều người gọi DuckDB là “SQLite phiên bản Analytics”. Nó không cần cài service daemon, không cấu hình user/password và chạy trực tiếp trong tiến trình Python của bạn.

Sức mạnh thực sự của DuckDB đến từ 3 yếu tố kiến trúc:

  • Lưu trữ dạng cột (Columnar Storage): Khác với SQLite quét toàn bộ từng hàng, DuckDB chỉ đọc đúng các cột được yêu cầu. Khi bạn tính SUM(revenue) trên 30 triệu dòng, engine chỉ đọc cột revenue từ đĩa và bỏ qua toàn bộ các cột còn lại, giảm I/O tới 80–90%.
  • Vectorized Execution Engine: Dữ liệu được xử lý theo từng khối vector (khoảng 2048 giá trị) nằm trọn trong CPU L1/L2 cache, tận dụng tối đa tập lệnh SIMD của chip hiện đại.
  • Xử lý Out-of-Core (Streaming to Disk): Nếu tập dữ liệu lớn hơn RAM vật lý, DuckDB tự động chia nhỏ và ghi bộ đệm tạm xuống ổ cứng thay vì khiến chương trình bị crash.

Thực hành DuckDB với Python

1. Cài đặt môi trường

DuckDB được đóng gói thành file binary độc lập, không phụ thuộc thư viện C++ ngoài:

pip install duckdb pandas pyarrow

2. Truy vấn trực tiếp file CSV / Parquet trên ổ cứng

Bạn không cần INSERT dữ liệu vào database trước. DuckDB có thể đọc và quét trực tiếp file từ đĩa:

import duckdb

# Khởi tạo kết nối in-memory tạm thời
con = duckdb.connect(database=':memory:')

# Quét trực tiếp file CSV 8.5GB mà không load toàn bộ vào RAM
query_csv = """
SELECT 
    status_code,
    COUNT(*) AS total_requests,
    ROUND(AVG(response_time_ms), 2) AS avg_latency
FROM 'server_logs.csv'
GROUP BY status_code
HAVING total_requests > 1000
ORDER BY total_requests DESC;
"""

# Trả kết quả về DataFrame chỉ sau khoảng 2-3 giây
df_result = con.execute(query_csv).fetchdf()
print(df_result)

DuckDB tự động nhận diện kiểu dữ liệu và tận dụng toàn bộ số core CPU hiện có để xử lý song song.

3. Tương tác Zero-Copy với Pandas DataFrame

Nếu đã có sẵn DataFrame trong bộ nhớ, DuckDB có thể truy vấn biến này trực tiếp thông qua con trỏ bộ nhớ Apache Arrow mà không tốn chi phí clone dữ liệu.

import pandas as pd
import duckdb

# Giả lập DataFrame đơn hàng
orders_df = pd.DataFrame({
    'order_id': range(1, 6),
    'customer_id': ['C101', 'C102', 'C101', 'C103', 'C102'],
    'amount': [250.0, 120.5, 310.0, 89.9, 450.0]
})

# DuckDB tự động nhận diện biến orders_df trong scope hiện tại
query = """
SELECT 
    customer_id,
    SUM(amount) AS total_spent,
    COUNT(order_id) AS total_orders
FROM orders_df
GROUP BY customer_id
ORDER BY total_spent DESC;
"""

summary = duckdb.sql(query).df()
print(summary)

4. Lưu trữ dữ liệu bền vững (Persistent Storage)

Để lưu kết quả phân tích xuống đĩa cho các tiến trình khác tái sử dụng, bạn chỉ cần thay ':memory:' bằng đường dẫn file cụ thể:

import duckdb

con = duckdb.connect('analytics.duckdb')

# Gom nhiều file Parquet theo pattern vào một bảng duy nhất
con.execute("""
CREATE TABLE IF NOT EXISTS daily_metrics AS 
SELECT * FROM 'logs/metrics_2026_*.parquet';
"""
)

total_rows = con.execute("SELECT COUNT(*) FROM daily_metrics;").fetchone()[0]
print(f"Đã nạp thành công {total_rows:,} bản ghi.")

con.close()

5. Kiểm soát RAM khi xử lý dữ liệu lớn trên server cấu hình thấp

Trên các máy chủ tài nguyên hạn chế, bạn nên chủ động giới hạn trần bộ nhớ và số luồng:

import duckdb

con = duckdb.connect('warehouse.duckdb')

# Giới hạn mức RAM tối đa 2GB và dùng 4 luồng CPU
con.execute("SET max_memory = '2GB';")
con.execute("SET threads = 4;")

# Xuất thẳng kết quả tổng hợp ra file Parquet nén ZSTD
con.execute("""
COPY (
    SELECT 
        date_trunc('day', timestamp) AS report_date,
        user_id,
        COUNT(event_id) AS purchase_count,
        SUM(total_amount) AS revenue
    FROM 'raw_events_*.csv'
    WHERE event_type = 'PURCHASE'
    GROUP BY 1, 2
) TO 'daily_purchase_summary.parquet' (FORMAT PARQUET, COMPRESSION ZSTD);
"""
)

print("Hoàn tất xử lý và xuất file Parquet an toàn, không quá ngưỡng 2GB RAM!")

Nên chọn DuckDB, SQLite hay PostgreSQL?

Mỗi công cụ phục vụ một bài toán riêng biệt:

  • Chọn SQLite khi: Ứng dụng di động, app desktop cục bộ, lưu cấu hình, hoặc hệ thống CRUD đơn giản (OLTP). SQLite tối ưu cho việc đọc/ghi từng dòng bản ghi riêng lẻ.
  • Chọn PostgreSQL / MySQL khi: Hệ thống web cần nhiều client kết nối đồng thời qua mạng, phân quyền người dùng phức tạp và yêu cầu giao dịch ACID chặt chẽ.
  • Chọn DuckDB khi: Chạy phân tích thống kê, xử lý file log lớn (CSV, Parquet, JSON), xây dựng ETL pipeline cục bộ bằng Python, hoặc làm query engine cho dashboard mà không muốn dựng cụm Spark phức tạp.

Tổng kết

Thay thế Pandas bằng DuckDB đã giúp pipeline xử lý log của mình thoát khỏi lỗi tràn RAM, đồng thời rút ngắn thời gian chạy từ 15 phút xuống chỉ còn 8 giây trên cùng một con VPS giá rẻ. Nếu bạn đang vật lộn với các file dữ liệu vài gigabyte trên máy local, DuckDB chắc chắn là công cụ đáng thử ngay hôm nay.

Share: