Data Engineering 2 — Định dạng & Lưu trữ dữ liệu
Data Engineering 2 — Định dạng & Lưu trữ dữ liệu
Khi dữ liệu chảy qua một pipeline, nó phải "đậu" lại ở đâu đó dưới một hình hài cụ thể: một file CSV, một blob JSON, một bảng Parquet, một hàng trong cơ sở dữ liệu. Lựa chọn định dạng (format) và cách lưu trữ (storage) tưởng là chuyện nhỏ, nhưng nó quyết định trực tiếp đến tốc độ truy vấn, chi phí lưu trữ, khả năng tiến hóa schema và cả việc hệ thống của bạn có scale được hay không.
Bài này đi từ những định dạng quen thuộc nhất (CSV, JSON) đến các định dạng cột hiện đại (Parquet, ORC), giải thích vì sao lưu theo cột lại nhanh đến vậy cho phân tích, rồi mở rộng ra nén, schema evolution, OLTP vs OLAP, object storage và phân vùng dữ liệu.
1. Định dạng văn bản (text-based)
Định dạng văn bản lưu dữ liệu dưới dạng ký tự con người đọc được. Chúng phổ biến vì đơn giản, dễ debug, hầu như công cụ nào cũng mở được.
1.1 CSV (Comma-Separated Values)
CSV là một bảng dạng phẳng: mỗi dòng là một bản ghi, các cột ngăn cách bởi dấu phẩy.
id,name,age,city
1,An,28,Hanoi
2,Binh,34,HCMC
Ưu điểm:
- Cực kỳ đơn giản, mọi ngôn ngữ và công cụ (Excel, pandas, Spark) đều đọc được.
- Nhẹ với dữ liệu dạng bảng đơn giản, dễ stream từng dòng.
Nhược điểm:
- Không có schema/kiểu dữ liệu:
28là số hay chuỗi? Ngày tháng theo định dạng nào? Mọi thứ đều là text, phải tự đoán. - Không xử lý tốt dữ liệu lồng nhau (nested): không thể biểu diễn mảng hay object một cách tự nhiên.
- Khó với ký tự đặc biệt: dấu phẩy, xuống dòng, dấu nháy trong giá trị phải escape — nguồn gốc của vô số bug.
- Không nén theo cấu trúc, đọc cả file mới lấy được vài cột.
1.2 JSON (JavaScript Object Notation)
JSON biểu diễn dữ liệu dưới dạng object lồng nhau (key-value) và mảng.
{ "id": 1, "name": "An", "address": { "city": "Hanoi", "zip": "10000" }, "tags": ["vip", "new"] }
Ưu điểm:
- Hỗ trợ dữ liệu lồng nhau rất tốt — phù hợp cho API, event log, document.
- Tự mô tả (self-describing): mỗi giá trị đi kèm tên key.
- Là "ngôn ngữ chung" của web/REST API.
Nhược điểm:
- Cồng kềnh: tên key lặp lại ở mọi bản ghi, tốn dung lượng.
- Parsing chậm so với định dạng nhị phân.
- Kiểu dữ liệu hạn chế (không phân biệt int/float rõ ràng, không có kiểu ngày/giờ chuẩn, không có decimal chính xác).
- Với phân tích quy mô lớn, đọc và parse JSON là một nút thắt cổ chai.
Một biến thể quan trọng trong Data Engineering là JSON Lines (JSONL/NDJSON): mỗi dòng là một object JSON độc lập, rất tiện cho stream và xử lý song song.
1.3 XML
XML dùng thẻ (tag) lồng nhau, từng thống trị thời SOAP/enterprise.
Ưu điểm: rất biểu cảm, hỗ trợ schema (XSD), namespace, attribute. Phù hợp cấu hình phức tạp, tài liệu có cấu trúc.
Nhược điểm: rất dài dòng (verbose) — mỗi thẻ phải có thẻ đóng, parsing nặng, ngày nay hiếm dùng cho data pipeline mới (chủ yếu gặp ở hệ thống cũ, dữ liệu tài chính, ngân hàng legacy).
2. Định dạng nhị phân & định dạng cột (binary / columnar)
Khi dữ liệu lớn lên hàng TB và bài toán chính là phân tích (analytics), định dạng văn bản trở nên quá chậm và quá tốn. Đây là lúc các định dạng nhị phân, đặc biệt là định dạng cột, lên ngôi.
2.1 Avro — nhị phân theo hàng
Avro là định dạng nhị phân row-oriented, do hệ sinh thái Hadoop sinh ra. Đặc trưng nổi bật là schema được lưu kèm (dưới dạng JSON) ngay trong file.
- Lưu theo hàng → ghi nhanh, phù hợp cho streaming/ingest (ví dụ Kafka thường dùng Avro).
- Schema evolution rất mạnh: thêm/bớt/đổi tên field với quy tắc tương thích rõ ràng.
- Gọn hơn JSON nhiều vì không lặp tên field trong dữ liệu.
2.2 Parquet — nhị phân theo cột
Parquet là định dạng columnar phổ biến nhất trong data lake/lakehouse hiện nay (Spark, Trino, BigQuery, Snowflake, DuckDB đều đọc được).
- Lưu dữ liệu theo cột thay vì theo hàng.
- Nén cực tốt vì giá trị cùng một cột thường giống nhau (cùng kiểu, phân bố hẹp).
- Lưu kèm metadata thống kê (min/max, số lượng null) theo từng "row group" → engine có thể bỏ qua (skip) cả khối dữ liệu không liên quan.
2.3 ORC — nhị phân theo cột
ORC (Optimized Row Columnar) cũng là định dạng cột, tối ưu mạnh cho hệ sinh thái Hive. Tư tưởng tương tự Parquet: chia thành stripe, có index nhẹ, hỗ trợ nén và predicate pushdown. Trong thực tế, Parquet phổ biến hơn ở các nền tảng đám mây hiện đại, ORC mạnh ở môi trường Hadoop/Hive truyền thống.
3. Lưu theo HÀNG vs theo CỘT — trái tim của vấn đề
Đây là khái niệm quan trọng nhất của bài. Hãy hình dung một bảng:
| id | name | age | salary |
|---|---|---|---|
| 1 | An | 28 | 1000 |
| 2 | Binh | 34 | 1500 |
| 3 | Cuong | 41 | 2000 |
3.1 Lưu theo hàng (row-oriented)
Dữ liệu trên đĩa được xếp lần lượt từng dòng:
[1, An, 28, 1000] [2, Binh, 34, 1500] [3, Cuong, 41, 2000]
Khi bạn cần toàn bộ một bản ghi (ví dụ: "lấy hết thông tin nhân viên id=2"), engine chỉ cần đọc một vùng liên tục — rất nhanh. Khi bạn ghi/cập nhật một bản ghi, cũng chỉ cần chạm vào một vùng. Đây chính là lý do row storage tối ưu cho OLTP (giao dịch, đọc/ghi từng record).
3.2 Lưu theo cột (columnar)
Dữ liệu được xếp theo từng cột:
id: [1, 2, 3]
name: [An, Binh, Cuong]
age: [28, 34, 41]
salary: [1000, 1500, 2000]
Bây giờ giả sử câu hỏi phân tích là: "Lương trung bình của tất cả nhân viên là bao nhiêu?"
- Với row storage: engine phải đọc cả bảng (id, name, age, salary của mọi dòng) chỉ để lấy cột
salary→ lãng phí I/O khổng lồ khi bảng có 200 cột và hàng tỷ dòng. - Với columnar: engine chỉ đọc đúng khối
salary: [1000, 1500, 2000]→ đọc ít hơn rất nhiều, cộng dồn cũng nhanh hơn (dữ liệu cùng kiểu, nằm liền nhau, tận dụng tốt CPU cache và vectorization).
Thêm hai lợi thế lớn của columnar:
- Nén tốt hơn nhiều: một cột
citycó thể chỉ gồm vài giá trị lặp đi lặp lại (Hanoi,HCMC...). Lưu liền nhau cho phép dùng run-length encoding, dictionary encoding rất hiệu quả. Trong khi lẫn lộn các kiểu khác nhau (row) thì khó nén. - Column pruning + predicate pushdown: truy vấn
SELECT salary WHERE age > 30chỉ cần đọc cộtagevàsalary, và nhờ metadata min/max có thể bỏ qua nguyên cả block không thỏa điều kiện.
3.3 Bảng so sánh row vs columnar
| Tiêu chí | Row-oriented (theo hàng) | Columnar (theo cột) |
|---|---|---|
| Tối ưu cho | OLTP, ghi/cập nhật từng bản ghi | OLAP, đọc & quét vài cột |
| Lấy 1 bản ghi đầy đủ | Rất nhanh | Chậm (phải ghép nhiều cột) |
| Quét/tổng hợp 1-vài cột | Chậm (đọc thừa) | Rất nhanh |
| Khả năng nén | Trung bình | Rất cao |
| Ghi/insert lẻ | Hiệu quả | Kém (thường ghi theo lô) |
| Ví dụ | PostgreSQL, MySQL, Avro | Parquet, ORC, ClickHouse |
Kết luận trực giác: Ghi nhiều, đọc từng dòng → hàng. Đọc ít cột nhưng quét rất nhiều dòng để phân tích → cột.
4. Nén (compression) & Schema Evolution
4.1 Nén
Nén giảm dung lượng đĩa và lượng dữ liệu phải đọc (giảm I/O). Hai codec phổ biến nhất với Parquet:
- Snappy: nén/giải nén rất nhanh, tỉ lệ nén vừa phải. Mặc định trong nhiều hệ thống vì cân bằng tốc độ — đặc biệt phù hợp khi CPU là tài nguyên quý và truy vấn cần tốc độ.
- gzip (hoặc zstd): tỉ lệ nén cao hơn nhưng tốn CPU hơn khi giải nén. Phù hợp khi muốn tiết kiệm tối đa dung lượng/băng thông và chấp nhận đọc chậm hơn một chút.
zstdngày càng được ưa chuộng vì cho tỉ lệ nén tốt với tốc độ hợp lý.
Với định dạng cột, nén áp dụng theo từng cột nên hiệu quả vượt trội so với nén nguyên file CSV/JSON.
4.2 Schema Evolution
Dữ liệu thực tế luôn thay đổi: hôm nay thêm cột email, mai đổi tên cột, mốt cho phép một field nhận null. Schema evolution là khả năng thay đổi schema mà không phá vỡ dữ liệu cũ và các consumer cũ.
- Avro nổi tiếng về điểm này: có khái niệm rõ ràng về tương thích backward (reader mới đọc data cũ) và forward (reader cũ đọc data mới), nhờ schema kèm theo và giá trị mặc định (default).
- Parquet/ORC cũng hỗ trợ thêm cột, nhưng các thao tác như đổi tên/đổi kiểu thường được quản lý qua tầng table format như Apache Iceberg, Delta Lake, Hudi — vốn lưu lịch sử schema và metadata riêng.
Quy tắc an toàn: thêm cột có default thường an toàn; xóa cột hay đổi kiểu cần cẩn trọng và kiểm soát qua quy ước tương thích.
5. OLTP vs OLAP
Hai chữ này định hình toàn bộ kiến trúc dữ liệu.
5.1 OLTP — Online Transaction Processing
Hệ thống xử lý giao dịch trực tuyến: đặt hàng, chuyển tiền, cập nhật hồ sơ. Đặc trưng:
- Nhiều thao tác đọc/ghi nhỏ, chạm vào ít bản ghi.
- Yêu cầu độ trễ thấp và tính nhất quán (ACID).
- Lưu theo hàng, chuẩn hóa (normalized) để tránh trùng lặp.
- Hệ quản trị: PostgreSQL, MySQL, Oracle, SQL Server.
5.2 OLAP — Online Analytical Processing
Hệ thống phục vụ phân tích: báo cáo, dashboard, tổng hợp theo tháng/khu vực. Đặc trưng:
- Truy vấn quét lượng lớn dữ liệu, tổng hợp (SUM, AVG, GROUP BY) trên vài cột.
- Ít ghi lẻ, thường nạp theo lô (batch).
- Lưu theo cột, thường phi chuẩn hóa (denormalized) để truy vấn nhanh.
- Hệ thống: BigQuery, Snowflake, Redshift, ClickHouse, Trino, và các data lake dùng Parquet.
5.3 Sơ đồ tổng quan
Mô hình điển hình: dữ liệu sinh ra ở OLTP, được sao chép định kỳ sang object storage dưới dạng Parquet, rồi nạp vào kho OLAP để phân tích — tách biệt tải giao dịch khỏi tải phân tích.
6. Object Storage vs File System
6.1 File system truyền thống
Hệ thống file (POSIX) tổ chức dữ liệu theo cây thư mục, hỗ trợ ghi đè, thêm vào (append), khóa file. Phù hợp ứng dụng cục bộ, nhưng khó scale ngang và quản lý hàng tỷ file qua nhiều máy.
6.2 Object Storage (S3, GCS, Azure Blob)
Object storage lưu dữ liệu thành các object trong "bucket", truy cập qua API/HTTP bằng key (giống một đường dẫn phẳng).
- Scale gần như vô hạn, độ bền (durability) rất cao, chi phí thấp.
- Là nền tảng của data lake: chứa hàng triệu file Parquet.
- Đánh đổi: không phải file system thật — không sửa tại chỗ (object thường là immutable, ghi lại cả object), có eventual consistency ở một số thao tác liệt kê (dù các nhà cung cấp lớn nay đã mạnh consistency hơn), độ trễ mỗi request cao hơn đĩa cục bộ.
Vì vậy các engine phân tích được thiết kế để đọc theo khối lớn từ object storage thay vì nhiều thao tác nhỏ — và đây cũng là lý do định dạng cột nén tốt (đọc ít, đọc khối lớn) rất hợp với object storage.
7. Phân vùng (Partitioning) dữ liệu
Khi data lake có hàng nghìn file Parquet, làm sao tránh quét toàn bộ? Câu trả lời là partitioning: tổ chức dữ liệu thành các thư mục con theo một cột (thường là ngày).
s3://datalake/events/
dt=2026-06-28/part-0001.parquet
dt=2026-06-29/part-0002.parquet
dt=2026-06-30/part-0003.parquet
Khi truy vấn WHERE dt = '2026-06-30', engine chỉ cần đọc thư mục tương ứng — gọi là partition pruning, có thể loại bỏ 99% dữ liệu trước khi đọc.
Vài nguyên tắc thực dụng:
- Chọn partition key theo cột hay dùng để lọc nhất (thường là ngày/tháng, đôi khi region).
- Đừng phân vùng quá mịn: hàng triệu thư mục với mỗi file vài KB sẽ tạo ra small files problem (nhiều file nhỏ làm chậm liệt kê và lập kế hoạch truy vấn).
- Kết hợp partitioning (loại bỏ ở mức thư mục) + metadata min/max của Parquet (loại bỏ ở mức row group) để tối ưu hai tầng.
Tóm tắt
- Định dạng văn bản (CSV, JSON, XML) dễ đọc, dễ debug, nhưng cồng kềnh, chậm parse và không có schema chặt — hợp cho trao đổi đơn giản, không hợp phân tích lớn.
- Định dạng nhị phân: Avro (theo hàng, mạnh schema evolution, hợp streaming/ingest) và Parquet/ORC (theo cột, nén tốt, hợp phân tích).
- Row storage tối ưu cho OLTP (đọc/ghi từng bản ghi); columnar storage tối ưu cho OLAP (quét vài cột trên nhiều dòng) nhờ giảm I/O, nén tốt và predicate pushdown.
- Nén (Snappy nhanh, gzip/zstd nén mạnh) và schema evolution (Avro/Iceberg/Delta) giúp dữ liệu gọn và tiến hóa an toàn.
- OLTP (PostgreSQL/MySQL) tách biệt khỏi OLAP (BigQuery/Snowflake/ClickHouse) trong kiến trúc thực tế.
- Object storage (S3/GCS) là nền của data lake; partitioning giúp loại bỏ phần lớn dữ liệu không cần đọc.
Tự kiểm tra
- Vì sao truy vấn tổng hợp
AVG(salary)trên một bảng 200 cột chạy nhanh hơn nhiều với Parquet so với CSV? - Khi nào bạn chọn định dạng theo hàng thay vì theo cột? Cho một ví dụ workload cụ thể.
- Hãy giải thích sự khác nhau giữa OLTP và OLAP về kiểu truy vấn, cách lưu trữ và ví dụ hệ quản trị.
- Snappy và gzip khác nhau ở điểm gì, và khi nào bạn ưu tiên cái nào?
- "Small files problem" là gì và phân vùng quá mịn gây ra nó như thế nào?
- Vì sao object storage hợp với định dạng cột nén, trong khi không hợp với nhiều thao tác ghi/sửa nhỏ tại chỗ?
Đọc tiếp
Bài viết liên quan
Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.
Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.
Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.
Đơn vị cốt lõi của dbt: model là một câu SELECT, cách ref()/source() dựng DAG, và bốn kiểu materialization (view, table, incremental, ephemeral) — chọn đúng để cân bằng chi phí, độ tươi và tốc độ.