Parquet là gì và hoạt động ra sao?

Parquet là một định dạng file mã nguồn mở được thiết kế riêng cho việc xử lý các tập dữ liệu lớn. Khác với các định dạng lưu trữ theo hàng truyền thống (như CSV), Parquet lưu dữ liệu theo cột, nhờ đó mang lại hiệu quả vượt trội trong việc nén và truy vấn dữ liệu.

Đây là định dạng rất phổ biến và được hỗ trợ rộng rãi trong các hệ thống như Apache Spark, Apache Hive, cũng như nhiều công cụ phân tích dữ liệu hiện đại khác.

Cùng xem Parquet hoạt động như thế nào qua đoạn code sau:

import pyarrow as pa
import pyarrow.parquet as pq
import random

# Define the number of rows, row groups, and rows per group
num_rows = 1_000_000
num_row_groups = 10
rows_per_group = num_rows // num_row_groups  

# Create sample data
data = {
    "name": ["jonny_" + str(i) for i in range(num_rows)],  
    "score": [i for i in range(num_rows)]
}

# Convert data to a PyArrow table and write to a Parquet file
table = pa.Table.from_pydict(data)
pq.write_table(table, "example.parquet", row_group_size=rows_per_group)
print("✅ Parquet file has been created with 10 Row Groups!")


1.Đọc và hiển thị thông tin metadata của file Parquet:

# Read and display Parquet file metadata
parquet_file = "example.parquet"  
pf = pq.ParquetFile(parquet_file) 

print(pf.metadata)

# Loop through each row group and print column metadata
for i in range(pf.metadata.num_row_groups):
    row_group = pf.metadata.row_group(i)
    for j in range(row_group.num_columns):
        column = row_group.column(j)
        print(f"Column: {column.path_in_schema}")
        print(f"  Encodings: {column.encodings}")
        print(f"  Dictionary Page Offset: {column.dictionary_page_offset}")
        print("-" * 50)


<pyarrow._parquet.FileMetaData object at 0x7ee042065e90>
  created_by: parquet-cpp-arrow version 18.1.0
  num_columns: 2
  num_rows: 1000000
  num_row_groups: 10
  format_version: 2.6
  serialized_size: 2618
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 4
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 643237
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 1256289
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 1881067
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 2494106
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 3118848
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 3731882
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 4356685
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 4969728
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 5594555
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 6207595
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 6832396
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 7445449
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 8070256
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 8683308
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 9308108
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 9921156
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 10545963
--------------------------------------------------
Column: name
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 11158990
--------------------------------------------------
Column: score
  Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
  Dictionary Page Offset: 11783811
--------------------------------------------------

1.1 Thông tin chung về metadata của file Parquet:

Phần đầu của kết quả hiển thị là cái nhìn tổng quan về tệp:

<pyarrow._parquet.FileMetaData object at 0x7ee042065e90>
  created_by: parquet-cpp-arrow version 18.1.0
  num_columns: 2
  num_rows: 1000000
  num_row_groups: 10
  format_version: 2.6
  serialized_size: 2618 

created_by: parquet-cpp-arrow phiên bản 18.1.0:

File Parquet này được tạo ra bởi thư viện parquet-cpp-arrow phiên bản 18.1.0. Thông tin này giúp xác định công cụ hoặc phiên bản đã tạo ra file, rất hữu ích khi kiểm tra khả năng tương thích hoặc xử lý lỗi.

num_columns: 2:

File này có 2 cột dữ liệu, cụ thể là "name" và "score" (sẽ được liệt kê ở phần sau của kết quả).

num_rows: 1.000.000:

File chứa tổng cộng 1.000.000 dòng dữ liệu.

num_row_groups: 10:

Dữ liệu trong file được chia thành 10 nhóm dòng (row group). Với 1 triệu dòng và 10 nhóm, mỗi nhóm sẽ có khoảng 100.000 dòng.

format_version: 2.6:

File sử dụng định dạng Parquet phiên bản 2.6, là phiên bản tiêu chuẩn tại thời điểm file được tạo.

serialized_size: 2618:

Phần metadata (thông tin mô tả cấu trúc file) khi được tuần tự hóa có kích thước 2.618 byte. Đây là kích thước của phần metadata, không bao gồm dữ liệu thực tế.

1.2 Thông tin chi tiết về các cột trong từng nhóm dòng (Row Group):

Sau phần tổng quan, đoạn hiển thị tiếp theo liệt kê thông tin về hai cột "name" và "score" trong từng nhóm dòng (row group). Vì có tổng cộng 10 row group, nên bạn sẽ thấy thông tin của mỗi cột được lặp lại 10 lần, tương ứng với từng nhóm.

Tôi sẽ giải thích ý nghĩa của các trường thông tin này và phân tích sự thay đổi của chúng giữa các nhóm dòng.

Cột: name và Cột: score
Đây là hai cột dữ liệu: "name" (có thể là chuỗi, ví dụ như tên người) và "score" (có thể là số, ví dụ như điểm số).

Mã hóa (Encodings): ('PLAIN', 'RLE', 'RLE_DICTIONARY')
Cả hai cột đều sử dụng ba phương pháp mã hóa sau:

  • PLAIN: Lưu dữ liệu theo dạng thô, không nén – dùng trong trường hợp nén không mang lại lợi ích rõ rệt.

  • RLE (Run-Length Encoding): Nén các giá trị lặp lại liên tục bằng cách lưu giá trị và số lần lặp – rất hiệu quả cho các giá trị trùng nhau liền kề.

  • RLE_DICTIONARY: Mã hóa dữ liệu bằng từ điển (dictionary encoding) – thay giá trị bằng chỉ số trong danh sách giá trị duy nhất, kết hợp với RLE để xử lý các chỉ số lặp lại – phù hợp với dữ liệu lặp không liên tiếp.

Vị trí trang từ điển (Dictionary Page Offset)
Đây là vị trí tính theo byte trong file, nơi bắt đầu trang từ điển (chứa danh sách các giá trị duy nhất) cho mỗi cột.

Ví dụ:

  • Cột "name" trong nhóm dòng đầu tiên: bắt đầu tại byte 4.

  • Cột "score" trong nhóm dòng đầu tiên: bắt đầu tại byte 643.237.

1.3 Vị trí trang từ điển (Dictionary Page Offsets) qua các nhóm dòng:

Kết quả được lặp lại cho cả 10 nhóm dòng, với các vị trí offset khác nhau. Dưới đây là bảng tóm tắt:

1.3.1 Nhận xét:
  • Các giá trị offset tăng dần theo từng nhóm dòng, cho thấy mỗi nhóm có từ điển riêng.
  • Offset của "score" luôn lớn hơn offset của "name" trong cùng một nhóm, cho thấy dữ liệu của "name" được ghi trước "score".
  • Sự chênh lệch giữa các offset thay đổi, phản ánh sự khác nhau về dữ liệu hoặc kích thước từ điển giữa các nhóm.
1.3.2 Ý nghĩa của các phương pháp mã hóa:

Việc sử dụng nhiều phương pháp mã hóa giúp tối ưu hóa việc lưu trữ:

  • PLAIN: Dành cho dữ liệu duy nhất hoặc khó nén.

  • RLE: Dành cho các giá trị lặp lại liên tiếp.

  • RLE_DICTIONARY: Dành cho các giá trị lặp lại không liên tiếp, sử dụng từ điển để mã hóa.

Sự linh hoạt này cho phép Parquet tự động điều chỉnh cách nén theo đặc tính của dữ liệu, giúp cải thiện hiệu quả lưu trữ và tăng tốc độ truy vấn.

1.3.4 Kết luận:

Cấu trúc của file Parquet như sau:

  • Tổng quan: File chứa 1.000.000 dòng, được chia thành 10 nhóm dòng (mỗi nhóm 100.000 dòng), với 2 cột là "name" và "score".

  • Mã hóa: Cả hai cột đều sử dụng các phương pháp mã hóa PLAINRLE và RLE_DICTIONARY để nén dữ liệu một cách hiệu quả.

  • Từ điển: Mỗi nhóm dòng có từ điển riêng cho từng cột, thể hiện qua các giá trị offset khác nhau.

  • Mục đích: Cấu trúc này giúp tối ưu lưu trữ và tăng tốc truy xuất dữ liệu, rất phù hợp với các tác vụ xử lý dữ liệu lớn (Big Data).

2. Hiểu cách Parquet quét dữ liệu một cách hiệu quả:

import pyarrow.parquet as pq

parquet_file = "example.parquet"  
pf = pq.ParquetFile(parquet_file) 

for i in range(pf.metadata.num_row_groups):
    row_group = pf.metadata.row_group(i)
    print(f"📌 Row Group {i + 1}/{pf.metadata.num_row_groups}")
    for j in range(row_group.num_columns):
        column = row_group.column(j)
        print(f"  🏷 Column: {column.path_in_schema}")
        print(f"    📉 Min: {column.statistics.min if column.statistics else 'N/A'}")
        print(f"    📈 Max: {column.statistics.max if column.statistics else 'N/A'}")
        print("-" * 50)
Row Group 1/10
  🏷 Column: name
    📉 Min: jonny_0
    📈 Max: jonny_99999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 0
    📈 Max: 99999
--------------------------------------------------
📌 Row Group 2/10
  🏷 Column: name
    📉 Min: jonny_100000
    📈 Max: jonny_199999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 100000
    📈 Max: 199999
--------------------------------------------------
📌 Row Group 3/10
  🏷 Column: name
    📉 Min: jonny_200000
    📈 Max: jonny_299999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 200000
    📈 Max: 299999
--------------------------------------------------
📌 Row Group 4/10
  🏷 Column: name
    📉 Min: jonny_300000
    📈 Max: jonny_399999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 300000
    📈 Max: 399999
--------------------------------------------------
📌 Row Group 5/10
  🏷 Column: name
    📉 Min: jonny_400000
    📈 Max: jonny_499999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 400000
    📈 Max: 499999
--------------------------------------------------
📌 Row Group 6/10
  🏷 Column: name
    📉 Min: jonny_500000
    📈 Max: jonny_599999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 500000
    📈 Max: 599999
--------------------------------------------------
📌 Row Group 7/10
  🏷 Column: name
    📉 Min: jonny_600000
    📈 Max: jonny_699999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 600000
    📈 Max: 699999
--------------------------------------------------
📌 Row Group 8/10
  🏷 Column: name
    📉 Min: jonny_700000
    📈 Max: jonny_799999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 700000
    📈 Max: 799999
--------------------------------------------------
📌 Row Group 9/10
  🏷 Column: name
    📉 Min: jonny_800000
    📈 Max: jonny_899999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 800000
    📈 Max: 899999
--------------------------------------------------
📌 Row Group 10/10
  🏷 Column: name
    📉 Min: jonny_900000
    📈 Max: jonny_999999
--------------------------------------------------
  🏷 Column: score
    📉 Min: 900000
    📈 Max: 999999
--------------------------------------------------

Dựa trên đoạn mã này, chúng ta có thể phân tích kết quả và nhận thấy rằng file Parquet được chia thành 10 nhóm dòng (row group), và mỗi nhóm dòng chỉ lưu giá trị nhỏ nhất (min) và lớn nhất (max) cho từng cột.

Cấu trúc này rất có lợi cho việc tối ưu hóa truy vấn, bởi vì khi đọc file Parquet và áp dụng điều kiện lọc (filter), hệ thống không cần quét toàn bộ dữ liệu. Thay vào đó, nó chỉ quét những nhóm dòng thỏa mãn điều kiện lọc, từ đó tăng hiệu suất truy vấn một cách đáng kể.

Nhờ tận dụng đặc điểm này, Parquet giúp giảm số lần truy xuất I/O, rất phù hợp trong các bài toán xử lý dữ liệu lớn (Big Data)tính toán phân tán, và phân tích dữ liệu chuyên sâu, nơi mà hiệu suất và khả năng mở rộng là yếu tố then chốt.

3. Cách Parquet tối ưu hóa việc quét dữ liệu:

Cấu trúc nhóm dòng (row group) của Parquet được thiết kế để giảm thiểu việc quét dữ liệu không cần thiết. Dưới đây là cách hoạt động:

Chia nhỏ tập dữ liệu bằng Row Group:

Thay vì lưu toàn bộ dữ liệu trong một file lớn, Parquet chia nhỏ dữ liệu thành các nhóm dòng (row group).

Mỗi row group chứa một phần dữ liệu cùng với thống kê min/max cho từng cột.

Lọc dữ liệu với kỹ thuật "Predicate Pushdown":

Khi một truy vấn áp dụng điều kiện lọc (ví dụ: WHERE score > 500000), Parquet sẽ kiểm tra giá trị min/max của từng row group trước.

Nếu phạm vi giá trị (min/max) của row group không thỏa mãn điều kiện, thì toàn bộ row group đó sẽ bị bỏ qua, không cần đọc.

Quét dữ liệu hiệu quả:

Nhờ đó, Parquet không cần quét toàn bộ dữ liệu, mà chỉ đọc những row group liên quan đến điều kiện truy vấn.

Điều này giúp giảm đáng kể số lần truy xuất I/O và nâng cao hiệu suất truy vấn.

Cơ chế thông minh này là một trong những lý do khiến Parquet trở thành định dạng lý tưởng cho các bài toán dữ liệu lớn và phân tích hiệu năng cao.

Ví dụ minh họa:

Hãy tưởng tượng bạn có một file Parquet dung lượng 1GB, được chia thành 10 nhóm dòng, mỗi nhóm chứa 100.000 dòng dữ liệu.

Khi áp dụng một bộ lọc đơn giản như WHERE score > 700000, Parquet sẽ:

  • Bỏ qua các nhóm dòng mà giá trị score lớn nhất (max) nhỏ hơn 700000.

  • Chỉ quét các nhóm dòng liên quan (ví dụ: row group 8, 9 và 10).

Điều này giúp giảm đáng kể lượng dữ liệu cần đọc, từ đó tăng tốc độ truy vấn một cách rõ rệt.

Kết luận:

Khả năng lưu trữ thống kê min/max theo từng nhóm dòng của Parquet cho phép áp dụng kỹ thuật lọc hiệu quả (predicate pushdown), giúp giảm đáng kể thời gian quét dữ liệu.

Nhờ tính năng này, các ứng dụng xử lý dữ liệu lớn có thể hoạt động hiệu quả hơn nhiều, khiến Parquet trở thành một định dạng được ưa chuộng trong các hệ thống phân tích hiện đại.

Nếu bạn đang làm việc với dữ liệu quy mô lớn, hiểu rõ cách Parquet quét dữ liệu sẽ giúp bạn viết truy vấn tối ưu hơn và cải thiện hiệu suất quy trình làm việc của mình!