Data Engineering 2 — Định dạng & Lưu trữ dữ liệu

13 thg 7, 2026 11 lượt xem
#data-engineering
#storage
#oltp-olap
#parquet

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: 28 là 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:

idnameagesalary
1An281000
2Binh341500
3Cuong412000

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:

  1. Nén tốt hơn nhiều: một cột city có 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.
  2. Column pruning + predicate pushdown: truy vấn SELECT salary WHERE age > 30 chỉ cần đọc cột agesalary, 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 choOLTP, ghi/cập nhật từng bản ghiOLAP, đọc & quét vài cột
Lấy 1 bản ghi đầy đủRất nhanhChậm (phải ghép nhiều cột)
Quét/tổng hợp 1-vài cộtChậm (đọc thừa)Rất nhanh
Khả năng nénTrung bìnhRất cao
Ghi/insert lẻHiệu quảKém (thường ghi theo lô)
Ví dụPostgreSQL, MySQL, AvroParquet, 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. zstd ngà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.

Use case thực tế

Bài toán tại NCB (minh hoạ): Khối Phân tích rủi ro cần chạy báo cáo tổng hợp dư nợ và phân loại nhóm nợ hàng ngày trên toàn bộ lịch sử giao dịch thẻ và khoản vay. Ban đầu, dữ liệu được xuất hằng đêm từ core banking (OLTP, row-store) ra các file CSV khổng lồ đặt trên object storage, mỗi ngày vài trăm GB. Truy vấn SUM(du_no)/GROUP BY nhom_no phải đọc toàn bộ file dù chỉ cần 4–5 cột trong số hơn 120 cột — cực kỳ lãng phí I/O, mỗi báo cáo chạy hàng chục phút.

Giải pháp: Đội Data Engineering chuyển tầng phân tích sang Parquet nén Snappy, đồng thời partition theo ngay_bao_cao (dt=YYYY-MM-DD). Kết quả (ước lượng, minh hoạ):

  • Dung lượng lưu trữ giảm mạnh nhờ nén theo cột (giá trị nhom_no, chi_nhanh lặp nhiều → dictionary/run-length encoding rất hiệu quả).
  • Truy vấn báo cáo ngày chỉ đọc đúng partition dt cần thiết (partition pruning) và chỉ vài cột (column pruning), thời gian chạy rút từ hàng chục phút xuống dưới một phút.
  • Metadata min/max của mỗi row group cho phép engine bỏ qua các khối không thoả điều kiện lọc số dư.

Dữ liệu gốc trong core vẫn giữ row-store để phục vụ giao dịch (OLTP), còn bản sao Parquet trên data lake phục vụ phân tích (OLAP) — tách biệt hai loại tải, đúng mô hình đã trình bày ở mục 5.

Ghi nhớ

  • Định dạng văn bản (CSV, JSON, XML) dễ đọc và debug nhưng cồng kềnh, parse chậm, thiếu schema chặt — chỉ hợp trao đổi đơn giản, không hợp phân tích quy mô lớn.
  • Avro là nhị phân theo hàng, lưu schema kèm file, mạnh về schema evolution → hợp streaming/ingest (Kafka). Parquet/ORC là nhị phân theo cột, nén tốt → hợp phân tích.
  • Row-oriented tối ưu cho OLTP (lấy/ghi trọn một bản ghi); columnar tối ưu cho OLAP (quét vài cột trên rất nhiều dòng) nhờ giảm I/O, nén cao và predicate pushdown.
  • Columnar nén tốt vì giá trị cùng cột đồng nhất → dùng được dictionary/run-length encoding; kèm metadata min/max theo row group để skip cả khối.
  • Nén: Snappy nhanh (tỉ lệ vừa phải), gzip/zstd nén mạnh hơn (tốn CPU hơn); zstd ngày càng được ưa chuộng vì cân bằng tốt.
  • Schema evolution: thêm cột có default thường an toàn; đổi tên/đổi kiểu cần kiểm soát, thường qua table format như Iceberg/Delta/Hudi.
  • Object storage (S3/GCS/Azure Blob) là nền của data lake: scale lớn, bền, rẻ, nhưng đọc theo khối lớn và object gần như immutable — rất hợp định dạng cột nén.
  • Partitioning (thường theo ngày) cho partition pruning loại bỏ phần lớn dữ liệu; tránh phân vùng quá mịn để không dính small files problem.

Nguồn tham khảo

Tự kiểm tra

  1. 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?
  2. 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ể.
  3. 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ị.
  4. Snappy và gzip khác nhau ở điểm gì, và khi nào bạn ưu tiên cái nào?
  5. "Small files problem" là gì và phân vùng quá mịn gây ra nó như thế nào?
  6. 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

Data Engineering 3 — Pipeline & Orchestration

Bài viết liên quan

Data Engineering là ngành xây dựng và vận hành hệ thống biến dữ liệu thô thành dữ liệu sạch, tin cậy, sẵn sàng cho phân tích và AI. Bài giới thiệu vai trò Data Engineer trong vòng đời dữ liệu (nguồn → ingestion → storage → transformation → serving), phân biệt với Analyst/Scientist/ML Engineer, bức tranh hệ sinh thái công cụ và bài toán đưa dữ liệu core banking sang kho phân tích.

13 thg 7, 2026 9

Stream processing là gì, khác biệt batch vs stream (bounded/unbounded), micro-batch (Spark) vs true streaming, Apache Flink là gì và định vị so với Spark Structured Streaming và Kafka Streams. Kiến trúc runtime JobManager/TaskManager, các tầng API, triết lý streaming-first và bối cảnh phát hiện gian lận ngân hàng.

13 thg 7, 2026 8

Đả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.

13 thg 7, 2026 7

So sánh ba trường phái Kimball, Inmon và Data Vault khi xây kho dữ liệu ngân hàng. Đào sâu các loại fact table, grain, surrogate key vs natural key và toàn bộ SCD (đặc biệt Type 2). Kèm dimension đặc biệt, bridge table và cách xử lý dữ liệu đến trễ.

13 thg 7, 2026 7

Cảm nhận của bạn

Bình luận

Bạn cần để viết bình luận.

Chưa có bình luận. Hãy là người đầu tiên chia sẻ!