Mở rộng quy mô vượt xa CSV: Kỹ thuật dữ liệu hiệu năng cao với Parquet, Arrow và DuckDB

Database tutorial - IT technology blog
Database tutorial - IT technology blog

Chi phí I/O: Tại sao các phương thức lưu trữ truyền thống thất bại

Tôi vẫn còn nhớ lần đầu tiên mình cố gắng xử lý một tệp CSV nặng 10GB trên máy tính xách tay. Quạt tản nhiệt bắt đầu kêu rú lên, dung lượng RAM chạm mức 95%, và cuối cùng script Python đã “chết” với lỗi MemoryError thường thấy.

Đó là một hồi chuông cảnh tỉnh. Nếu bạn đã dành cả sự nghiệp làm việc với các cơ sở dữ liệu quan hệ như MySQL hay PostgreSQL, sự thất vọng này giống như một thử thách mà ai cũng phải trải qua. Những hệ thống này là những “cỗ xe kéo” đáng tin cậy, nhưng chúng thường tỏ ra chậm chạp khi bạn cần quét 100 triệu hàng chỉ để tính một giá trị trung bình duy nhất.

Nút thắt cổ chai thường không nằm ở chính engine của cơ sở dữ liệu. Nó nằm ở cách sắp xếp vật lý của dữ liệu trên đĩa. Các cơ sở dữ liệu truyền thống và tệp CSV lưu trữ theo kiểu “hướng dòng” (row-oriented), nghĩa là mọi mẩu dữ liệu của một bản ghi được lưu trữ cùng nhau. Nếu truy vấn của bạn chỉ cần cột “Price” (Giá), máy tính vẫn phải đọc cả tên, địa chỉ và các mô tả dài đi kèm với mỗi hàng. Điều này tạo ra chi phí I/O khổng lồ, ngay cả khi bạn đã sử dụng Materialized Views trong PostgreSQL để tối ưu hiệu năng hệ thống.

Cuộc cách mạng lưu trữ dạng cột (Columnar)

Lưu trữ dạng cột giải quyết vấn đề này bằng cách nhóm dữ liệu theo cột thay vì theo dòng. Đối với các tác vụ phân tích, sự thay đổi kiến trúc duy nhất này sẽ làm thay đổi mọi thứ.

Apache Parquet: Ông vua về hiệu quả lưu trữ trên đĩa

Hãy coi Apache Parquet như một kho lưu trữ hiệu quả cao. Đây là một định dạng hướng cột nguồn mở, hoạt động như một phiên bản thông minh hơn nhiều của CSV, giúp hiện đại hóa Data Lakehouse và tối ưu hóa tài nguyên.

Vì dữ liệu trong một cột duy nhất thường tương đồng nhau—ví dụ như một danh sách các số nguyên 64-bit—Parquet có thể nén nó hiệu quả hơn nhiều so với bất kỳ định dạng hướng dòng nào. Nó cũng hỗ trợ “predicate pushdown”. Điều này cho phép engine bỏ qua toàn bộ các khối dữ liệu không khớp với bộ lọc của bạn mà không cần đọc chúng từ đĩa. Kết quả là lượng dữ liệu cần di chuyển ít hơn và kết quả trả về nhanh hơn.

Apache Arrow: Tốc độ bộ nhớ Zero-Copy

Nếu Parquet xử lý việc lưu trữ trên đĩa, thì Apache Arrow quản lý bộ nhớ. Theo truyền thống, việc di chuyển dữ liệu giữa các công cụ như cơ sở dữ liệu và script Python đòi hỏi quá trình “serialization” (tuần tự hóa). Quá trình này bao gồm việc đóng gói và giải nén dữ liệu sang một định dạng mà cả hai công cụ đều hiểu, gây lãng phí đáng kể chu kỳ CPU.

Arrow cung cấp một định dạng bộ nhớ dạng cột chuẩn hóa. Nó cho phép các hệ thống khác nhau chia sẻ dữ liệu ngay lập tức mà không cần sao chép hoặc chuyển đổi. Chúng tôi gọi đây là đọc “zero-copy”, và nó loại bỏ hiệu quả nút thắt cổ chai về bộ nhớ.

DuckDB: Engine phân tích hiện đại

Để kết hợp Parquet và Arrow lại với nhau, bạn cần một engine. DuckDB thường được gọi là “SQLite cho Phân tích”. Đây là một cơ sở dữ liệu SQL OLAP (Xử lý phân tích trực tuyến) chạy trong tiến trình (in-process) mà không cần thiết lập máy chủ. Nó được thiết kế để truy vấn các tệp Parquet bằng cách thực thi dựa trên Arrow. Đối với việc xây dựng các pipeline dữ liệu cục bộ, đây là đề xuất hàng đầu của tôi.

Thực hành: Xây dựng một Pipeline phân tích tốc độ cao

Hãy cùng xem sự chênh lệch về hiệu năng trong thực tế. Chúng ta sẽ tạo một bộ dữ liệu, lưu dưới hai định dạng và so sánh kết quả bằng Python.

1. Thiết lập môi trường

Cài đặt bộ công cụ cốt lõi bằng môi trường ảo (virtual environment) để giữ cho cài đặt Python hệ thống luôn sạch sẽ.

pip install pandas pyarrow duckdb numpy

2. Tạo 5 triệu hàng dữ liệu

Chúng ta sẽ tạo một bộ dữ liệu giả lập với 5 triệu hàng dữ liệu để mô phỏng kịch bản thực tế. Bộ dữ liệu này bao gồm ID, mốc thời gian, danh mục và giá cả.

import pandas as pd
import numpy as np
import time

# Tạo dữ liệu mẫu
num_rows = 5_000_000
data = {
    'timestamp': pd.date_range('2023-01-01', periods=num_rows, freq='S'),
    'user_id': np.random.randint(1000, 9999, size=num_rows),
    'category': np.random.choice(['Điện tử', 'Sách', 'Sân vườn', 'Đồ chơi'], size=num_rows),
    'price': np.random.uniform(10.0, 500.0, size=num_rows),
    'quantity': np.random.randint(1, 10, size=num_rows)
}

df = pd.DataFrame(data)

# Lưu dưới dạng CSV
start = time.time()
df.to_csv('data.csv', index=False)
print(f"Thời gian ghi CSV: {time.time() - start:.2f}s")

# Lưu dưới dạng Parquet
start = time.time()
df.to_parquet('data.parquet', engine='pyarrow', compression='snappy')
print(f"Thời gian ghi Parquet: {time.time() - start:.2f}s")

3. Hiệu quả về không gian lưu trữ

Hãy kiểm tra thư mục sau khi chạy script. Bạn sẽ nhận thấy một sự khác biệt đáng kể. Trong các thử nghiệm của tôi, tệp CSV chiếm khoảng 280MB. Còn tệp Parquet? Chỉ có 60MB. Đó là mức giảm 75% dung lượng trên đĩa, đạt được hoàn toàn nhờ vào việc mã hóa dữ liệu thông minh hơn.

4. Tốc độ truy vấn: CSV vs. Parquet

Bây giờ chúng ta sẽ tính tổng doanh thu cho mỗi danh mục. Chúng ta sẽ so sánh việc đọc CSV thô với việc quét Parquet đã được tối ưu hóa của DuckDB.

import duckdb

# Truy vấn tệp CSV
start = time.time()
csv_result = duckdb.query("""
    SELECT category, SUM(price * quantity) as revenue 
    FROM 'data.csv' 
    GROUP BY category
""").to_df()
print(f"Thời gian truy vấn CSV: {time.time() - start:.4f}s")

# Truy vấn tệp Parquet
start = time.time()
parquet_result = duckdb.query("""
    SELECT category, SUM(price * quantity) as revenue 
    FROM 'data.parquet' 
    GROUP BY category
""").to_df()
print(f"Thời gian truy vấn Parquet: {time.time() - start:.4f}s")

Trên phần cứng tiêu chuẩn, truy vấn Parquet thường nhanh hơn từ 10 đến 50 lần. DuckDB không cần tải toàn bộ tệp. Nó chỉ lấy các cột category, price, và quantity vào bộ nhớ và bỏ qua phần còn lại.

5. Zero-Copy với Apache Arrow

Nếu dữ liệu của bạn đã có sẵn trong bộ nhớ dưới dạng bảng Arrow, DuckDB có thể truy vấn nó với chi phí chuyển đổi bằng không. Điều này cực kỳ quan trọng đối với các pipeline phức tạp.

import pyarrow as pa

# Chuyển đổi Pandas sang bảng Arrow
table = pa.Table.from_pandas(df)

# Truy vấn trực tiếp bảng Arrow
start = time.time()
arrow_result = duckdb.query("SELECT AVG(price) FROM table").to_df()
print(f"Thời gian truy vấn bộ nhớ Arrow: {time.time() - start:.4f}s")

Kết luận

Chuyển từ các định dạng hướng dòng như CSV sang lưu trữ dạng cột là một trong những cách dễ nhất để nâng cấp kỹ năng Data Engineering của bạn và vượt qua giới hạn mở rộng của các hệ thống cũ. Nó cắt giảm chi phí lưu trữ và biến những phút chờ đợi thành những giây thực thi. Nó giúp một chiếc máy tính xách tay bình thường hoạt động như một kho dữ liệu (data warehouse) cao cấp.

Hãy ngừng sử dụng CSV cho các bộ dữ liệu lớn hơn vài megabyte. Lưu dữ liệu trung gian của bạn dưới dạng Parquet. Sử dụng DuckDB khi bạn cần chạy SQL. Một khi bạn trải nghiệm những lợi ích về hiệu năng này, bạn sẽ không bao giờ muốn quay lại cách xử lý dữ liệu cũ nữa.

Share: