Hãy tưởng tượng bạn đang làm việc với những tập dữ liệu khổng lồ—hàng triệu dòng, hàng trăm cột—và bạn cần một cách lưu trữ thật hiệu quả mà vẫn đảm bảo tốc độ và khả năng truy xuất. Lúc này, định dạng Parquet xuất hiện như một vị cứu tinh. Đây là định dạng lưu trữ theo cột, rất được ưa chuộng trong thế giới dữ liệu lớn nhờ khả năng nén và mã hóa thông minh. Nhưng bên trong nó hoạt động như thế nào? Hôm nay, chúng ta sẽ cùng khám phá một đoạn mã Python sử dụng thư viện PyArrow để minh họa hai kỹ thuật mã hóa quan trọng trong Parquet: Run-Length Encoding (RLE) và Dictionary Encoding. Thắt dây an toàn nhé—chuyến hành trình tối ưu dữ liệu này hứa hẹn sẽ rất thú vị!
1. Mã hóa quan trọng đến mức nào?
Trước khi đi vào phần mã nguồn, hãy cùng làm rõ bối cảnh. Parquet không đơn thuần chỉ là một định dạng để lưu trữ dữ liệu—mà là lưu trữ một cách thông minh. Và chính các kỹ thuật mã hóa là “bí quyết đặc biệt” giúp điều đó trở thành hiện thực.
Mã hóa trong Parquet không chỉ giúp giảm kích thước tệp, mà còn tăng tốc độ xử lý, tối ưu hiệu suất khi làm việc với dữ liệu lớn. Chính nhờ những kỹ thuật này, Parquet mới có thể vượt trội so với các định dạng lưu trữ truyền thống.
Nâng cấp hiệu quả: Khi RLE kết hợp với Dictionary Encoding
Nhưng Parquet không dừng lại ở đó—nó còn có một “chiêu trò” lợi hại hơn. Khi xử lý dữ liệu dạng chuỗi, Parquet thường kết hợp RLE (Run-Length Encoding) với Dictionary Encoding để tối ưu hóa hiệu quả lên một tầm cao mới.
Hãy hình dung Dictionary Encoding như một cuốn sổ mã bí mật: mỗi chuỗi duy nhất sẽ được gán cho một ID số nguyên nhỏ gọn. Sau đó, thay vì lưu toàn bộ chuỗi, Parquet chỉ lưu các ID này. Và RLE sẽ nén chính chuỗi ID đó, thay vì nén chuỗi gốc. Kết quả? Vừa tiết kiệm dung lượng, vừa tăng tốc độ xử lý.
Giờ hãy cùng xem cách bộ đôi này hoạt động qua một ví dụ thực tế:
Original strings: ["a", "a", "b", "b", "b", "c"]
Step 1: Dictionary encoding
"a" → 0
"b" → 1
"c" → 2
Data becomes: [0, 0, 1, 1, 1, 2]
Step 2: RLE on the codes
[0, 0] → (0, 2)
[1, 1, 1] → (1, 3)
[2] → (2, 1)
Kết quả là gì? Một bảng từ điển nhỏ gọn ["a", "b", "c"]
cùng với một danh sách nén ngắn gồm các cặp: (0, 2)
, (1, 3)
, (2, 1)
. Điều này chỉ chiếm một phần rất nhỏ so với kích thước dữ liệu ban đầu!
Khi bạn cần truy xuất lại dữ liệu, Parquet sẽ thực hiện thao tác ngược lại:
nó giải nén RLE thành [0, 0, 1, 1, 1, 2]
, rồi tra lại từ điển để khôi phục thành ["a", "a", "b", "b", "b", "c"]
.
Mọi thứ diễn ra mượt mà và thông minh đến bất ngờ.
Đoạn mã mà chúng ta sẽ tìm hiểu hôm nay chính là ví dụ minh họa rõ nét cho hai kỹ thuật này trong thực tế. Hãy cùng phân tích từng bước và khám phá xem tác giả muốn truyền đạt điều gì!
Bước 1: Tạo ra bộ dữ liệu "chuẩn chỉnh"
import pandas as pd
import pyarrow as pa
import pyarrow.parquet as pq
from itertools import groupby
from collections import Counter
# Define two columns with distinct characteristics
col1_nle = [1] * 25 + [2] * 25 + [3] * 25 + [4] * 25 # 100 rows of repeating numbers
col2_dictionary = ['a', 'b', 'c'] * 33 + ['a'] # 100 rows with few unique values
# Create a DataFrame
df = pd.DataFrame({'col1_nle': col1_nle, 'col2_dictionary': col2_dictionary})
df['col2_dictionary'] = df['col2_dictionary'].astype('category') # Prep for dictionary encoding
Giải thích chi tiết: Có gì đang diễn ra ở đây?
col1_rle
: Đây là một danh sách gồm 100 số nguyên, được chia thành 4 nhóm lặp lại đều nhau: 25 số 1, 25 số 2, 25 số 3 và 25 số 4.
👉 Dữ liệu kiểu này chính là "ứng viên hoàn hảo" cho Run-Length Encoding (RLE) — vì có các chuỗi giá trị giống nhau lặp lại liên tục. Tác giả đã rất khéo léo thiết kế dữ liệu theo cách này để cho thấy RLE sẽ hoạt động hiệu quả ra sao trong Parquet.col2_dictionary
: Cột này chứa 100 chuỗi ký tự, được lặp theo vòng “a, b, c” với 33 chu kỳ và thêm một giá trị “a” nữa để làm tròn 100 phần tử:
👉 Tổng cộng có 34 “a”, 33 “b”, và 33 “c” — tức chỉ có 3 giá trị duy nhất, rất lý tưởng cho Dictionary Encoding.
Ngoài ra, việc chuyển cột này sang kiểu category của Pandas cũng ngầm báo hiệu rằng đây là dữ liệu sẽ được mã hóa bằng từ điển.Biến
n = 100000
: Bạn có để ý không? Biến này có vẻ “thừa thãi”. Dù được khai báo, nhưng bộ dữ liệu thực tế chỉ có 100 dòng. Có lẽ đây là phần sót lại từ một lần kiểm thử quy mô lớn trước đó—giờ chỉ giữ lại phiên bản nhỏ gọn để dễ minh họa.
👉 Tóm lại, bộ dữ liệu được chuẩn bị kỹ lưỡng để làm nổi bật điểm mạnh của hai kỹ thuật mã hóa chính mà Parquet sử dụng: RLE cho số nguyên và Dictionary Encoding cho chuỗi
Bước 2: Ghi dữ liệu vào Parquet một cách có chủ đích
Tiếp theo, đoạn script sẽ chuyển đổi DataFrame sang định dạng PyArrow Table, rồi ghi nó thành file Parquet. Đây là bước trung gian quan trọng để tận dụng tối đa các tính năng của PyArrow và Parquet.
# Convert to Arrow Table
table = pa.Table.from_pandas(df, preserve_index=False)
# Write to Parquet with specific encoding instructions
pq.write_table(
table,
'example.parquet',
use_dictionary=['col2_dictionary'], # Force dictionary encoding here
compression=None # Focus on encodings, not compression
)
Những điểm cốt lõi trong đoạn ghi file:
use_dictionary=['col2_dictionary']
:
Tác giả chỉ rõ rằng chỉ muốn áp dụng dictionary encoding cho cộtcol2_dictionary
. Điều này đảm bảo chúng ta thấy rõ hiệu ứng của dictionary encoding trong kết quả, thay vì để Parquet tự quyết định cột nào cần mã hóa.compression=None
:
Việc không sử dụng nén (nhưsnappy
haygzip
) là một lựa chọn có chủ đích. Nó giúp tách biệt rõ ràng hiệu quả của các kỹ thuật mã hóa (RLE và dictionary) khỏi hiệu ứng của việc nén, tránh gây nhiễu khi phân tích kích thước và hiệu suất.
Bước 3: Nhìn vào bên trong — "Peeking Under the Hood"
Giờ đến phần thú vị nhất: cùng kiểm tra metadata của file Parquet để xem các cột đã được mã hóa ra sao thật sự!
parquet_file = pq.ParquetFile('example.parquet')
for i in range(parquet_file.metadata.num_row_groups):
row_group = parquet_file.metadata.row_group(i)
print(f"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}")
Kết quả đầu ra hé lộ “ma thuật” bên trong:
Row Group 0:
Column: col1_nle
Encodings: ('RLE', 'PLAIN')
Dictionary Page Offset: None
--------------------------------------------------
Column: col2_dictionary
Encodings: ('PLAIN', 'RLE', 'RLE_DICTIONARY')
Dictionary Page Offset: 875
col1_rle
:
Mã hóa:
RLE
vàPLAIN
Ý nghĩa:
Dữ liệu gồm các chuỗi dài giống nhau (25 số 1, rồi 25 số 2, v.v...) → đây là trường hợp lý tưởng cho RLE.
Không có dictionary page offset, tức là Parquet không dùng dictionary encoding, chỉ dùng RLE thuần để nén giá trị lặp lại.
Kết quả là một dạng mã hóa cực kỳ hiệu quả và gọn nhẹ nhờ các chuỗi lặp đều đặn.
🧠 Tóm lại: Cột này tận dụng tốt RLE, không cần dictionary.
🔹 col2_dictionary
:
Mã hóa:
PLAIN
,RLE
, vàRLE_DICTIONARY
Ý nghĩa:
Có dictionary page offset (ví dụ: 875), chứng tỏ Parquet đã tạo một trang từ điển để lưu trữ các chuỗi
"a"
,"b"
,"c"
bằng mã hóaPLAIN
.Dữ liệu chính được mã hóa bằng
**RLE_DICTIONARY**
, tức:Các chuỗi
"a"
,"b"
,"c"
được ánh xạ thành chỉ số0
,1
,2
.Sau đó, danh sách chỉ số này được nén bằng RLE.
Tuy nhiên, do chuỗi này luôn thay đổi theo mẫu “a, b, c, a, b, c...”, nên không có nhiều chỉ số lặp lại liên tiếp → RLE không phát huy hết hiệu quả nén.
Bước 4: Đọc và trực quan hóa dữ liệu
The script reads the Parquet file back and analyzes the columns:
table = pq.read_table('example.parquet')
col1_nle_array = table.column('col1_nle').chunk(0)
# Custom RLE display function
def display_rle(array):
rle_result = []
for value, group in groupby(array):
count = sum(1 for _ in group)
rle_result.append(f"{value},{count}")
return "[\n " + ",\n ".join(rle_result) + "\n]"
print("col1_nle array (RLE-style):")
print(display_rle(col1_nle_array))
Output:
col1_nle array (RLE-style):
[
1,25,
2,25,
3,25,
4,25
]
This matches perfectly—25 of each value, exactly as RLE would represent it. For col2_dictionary:
col2_dictionary_array = table.column('col2_dictionary').chunk(0)
print("\ncol2_dictionary array:", col2_dictionary_array)
print(f"Is col2_dictionary Dictionary Encoded? {pa.types.is_dictionary(col2_dictionary_array.type)}")
print("col2 array (RLE-style):")
value_counts = Counter(col2_dictionary_array.indices)
print(value_counts)
Output:
col2_dictionary array:
-- dictionary:
["a", "b", "c"]
-- indices:
[0, 1, 2, 0, 1, 2, ... , 0]
Is col2_dictionary Dictionary Encoded? True
col2 array (RLE-style):
Counter({<pyarrow.Int32Scalar: 0>: 34, <pyarrow.Int32Scalar: 1>: 33, <pyarrow.Int32Scalar: 2>: 33})
Dưới đây là phần phân tích:
Từ điển được tạo ra là ["a", "b", "c"]
, và các chỉ số lặp lại theo thứ tự (0, 1, 2, …)
. Điều này xác nhận rằng dữ liệu đã được mã hóa bằng dictionary encoding.
Bộ đếm (Counter
) cho thấy chỉ số 0 (tương ứng với “a”) xuất hiện 34 lần, còn 1 (“b”) và 2 (“c”) mỗi giá trị xuất hiện 33 lần — hoàn toàn khớp với dữ liệu đầu vào. Tuy nhiên, đây không phải là RLE thực sự (vốn theo dõi các chuỗi lặp liên tiếp), mà chỉ là thống kê tần suất xuất hiện. Nhiều khả năng, phần này để làm nổi bật cấu trúc của dictionary encoding, chứ không nhằm mục đích minh họa RLE nén các chỉ số.
Mã hóa trong thực tiễn
Vậy, điều rút ra là gì? Nhấn mạnh cách Parquet tối ưu hóa lưu trữ thông qua mã hóa:
RLE cho dữ liệu lặp lại liên tục: Cột
col1_rle
cho thấy Run-Length Encoding hoạt động hiệu quả thế nào khi có nhiều giá trị giống nhau liên tiếp — ví dụ, chuỗi"1, 1, 1..."
được nén thành đơn giản chỉ"1, 25"
.Dictionary Encoding cho dữ liệu ít giá trị duy nhất (low cardinality): Cột
col2_dictionary
minh họa cách các giá trị lặp lại được thay thế bằng một từ điển nhỏ cùng với chỉ số ánh xạ, rất phù hợp với những cột chỉ có vài giá trị khác nhau.
Việc bỏ qua nén và sử dụng tập dữ liệu nhỏ, dễ hiểu giúp giữ trọng tâm vào hai kỹ thuật mã hóa này. Phần kiểm tra metadata và trực quan hóa càng làm rõ thêm cách Parquet áp dụng mã hóa, biến đây thành một bài học thực hành hữu ích trong kỹ thuật dữ liệu (data engineering).
Tại sao bạn nên quan tâm?
Nếu bạn đang làm việc với dữ liệu lớn — trong data lake, pipeline phân tích, hay hệ thống machine learning — thì việc hiểu các kỹ thuật mã hóa của Parquet có thể thay đổi hoàn toàn cách bạn xử lý dữ liệu:
✅ File nhẹ hơn: Nhờ RLE và dictionary encoding, nhu cầu lưu trữ giảm đáng kể.
⚡ Truy vấn nhanh hơn: Kết hợp lưu trữ theo cột và mã hóa thông minh giúp tăng tốc độ đọc dữ liệu.
🔧 Linh hoạt: Các công cụ như PyArrow cho phép bạn tùy chỉnh mã hóa phù hợp với đặc điểm riêng của dữ liệu.
🧠 Lần tới khi bạn lưu một tập dữ liệu, hãy cân nhắc dùng Parquet.
Chạy thử đoạn script này — thay đổi dữ liệu, thêm nén, kiểm tra metadata — và quan sát cách các kỹ thuật mã hóa điều chỉnh theo. Đây là một bước nhỏ nhưng thiết thực để bạn tiến gần hơn đến việc tối ưu hiệu quả xử lý dữ liệu lớn.
Sẵn sàng đào sâu hơn?
Tải đoạn code, chạy thử và thử nghiệm ngay!
Tăng số lượng giá trị giống nhau trong
col1_rle
Thêm nhiều giá trị khác nhau vào
col2_dictionary
Kiểm tra kích thước file
Bật/tắt chế độ nén
Quan sát cách các phương pháp mã hóa thay đổi
Sức mạnh của Parquet nằm trong tay bạn — hãy bắt đầu tối ưu ngay hôm nay!
📝 Bài blog này đã chắt lọc ý tưởng từ đoạn code thành một câu chuyện sinh động, dùng markdown để dẫn dắt người đọc qua các khái niệm, đoạn mã và hiểu biết chuyên sâu.
Nó vừa kỹ thuật, vừa dễ tiếp cận — lý tưởng cho những người yêu dữ liệu muốn khám phá “bên trong” sức mạnh của Parquet.