Modeling nâng cao 5 — One Big Table & Wide Tables
Mô hình tinh thần: đảo ngược một quy tắc cũ
Suốt ba thập kỷ, kim chỉ nam của kho dữ liệu là chuẩn hoá vừa đủ: tách dữ liệu thành fact và dimension (xem star & snowflake của Kimball) để tránh trùng lặp và giữ tính nhất quán. Quy tắc đó sinh ra trong thời đại join rẻ, storage đắt: dữ liệu nằm trên đĩa quay, mỗi GB tốn tiền, và các engine hàng (row-store) join hai bảng nhỏ nhanh hơn quét một bảng khổng lồ.
One Big Table (OBT) — còn gọi là wide table hay fully denormalized table — đảo ngược quy tắc đó: gộp fact và tất cả thuộc tính dimension liên quan vào một bảng phẳng duy nhất, mỗi dòng đã chứa sẵn mọi thứ cần để phân tích, không phải join gì thêm.
Vì sao dám làm điều "phản Kimball" này? Vì trên kho cột MPP hiện đại (BigQuery, Snowflake, ClickHouse, Redshift) hai giả định gốc đã đảo ngược:
- Storage rẻ gần như miễn phí. Lưu trữ cột nén rất mạnh; trùng lặp một chuỗi
"Chi nhánh Hà Nội"hàng triệu lần gần như không tốn thêm byte sau nén dictionary/RLE. - Join đắt và khó song song. Join phân tán cần shuffle — di chuyển dữ liệu qua mạng giữa các node để gom cùng khoá về một chỗ. Shuffle là thao tác tốn kém nhất trên MPP. Một OBT loại bỏ hoàn toàn shuffle cho truy vấn phân tích.
Nói ngắn gọn: OBT đánh đổi dung lượng lưu trữ (rẻ) để mua lấy việc không phải join (đắt).
Vì sao denormalize lại thắng trên kho cột
Điểm mấu chốt nằm ở cách kho cột đọc dữ liệu và chi phí join phân tán.
Columnar: chỉ đọc cột cần, nén theo cột
Kho cột lưu mỗi cột thành một khối riêng. Truy vấn SUM(so_tien) GROUP BY ma_chi_nhanh chỉ chạm đúng hai cột, bỏ qua hàng trăm cột khác — column pruning. Bảng có 300 cột không làm truy vấn 2 cột chậm đi. Đây là lý do một bảng wide không bị phạt như trong row-store (nơi đọc một dòng là kéo cả dòng lên).
Thêm nữa, mỗi cột nén độc lập bằng thuật toán hợp với kiểu dữ liệu của nó (dictionary cho chuỗi lặp, run-length cho giá trị liên tục, delta cho số tăng dần). Cột ten_chi_nhanh lặp lại triệu lần nén cực nhỏ — nên trùng lặp phi chuẩn gần như miễn phí về storage. Xem thêm lưu trữ cột trong BigQuery.
Join phân tán = shuffle qua mạng
Trên engine MPP, join fact ⋈ dim theo khoá đòi hỏi hai bảng phải được co-partition theo khoá join. Nếu chưa, engine phải shuffle: băm khoá, đẩy dữ liệu qua mạng để mọi dòng cùng khoá về cùng một node. Với fact hàng tỷ dòng, shuffle tốn băng thông mạng, RAM (spill ra đĩa khi tràn) và thời gian.
Broadcast join (nhân bản dim nhỏ tới mọi node) giúp tránh shuffle khi dim đủ nhỏ — nhưng với dim lớn hoặc join nhiều tầng snowflake thì không cứu được. OBT xoá bỏ vấn đề tận gốc: không còn khoá ngoại, không còn join, không còn shuffle ở thời điểm truy vấn. Chi phí join được trả một lần lúc ETL dựng bảng, thay vì trả mỗi lần người dùng chạy query.
| Yếu tố | Star schema | One Big Table |
|---|---|---|
| Storage | Ít trùng lặp | Trùng lặp nhiều (nhưng nén tốt) |
| Join lúc truy vấn | Có (shuffle/broadcast) | Không |
| Độ phức tạp câu SQL của BI | Cao (nhiều JOIN) | Thấp (SELECT một bảng) |
| Cập nhật một thuộc tính dim | Sửa 1 dòng trong dim | Rewrite hàng triệu dòng fact |
| Nhất quán dữ liệu | Một nguồn sự thật cho dim | Dễ lệch, khó đồng bộ |
| Grain / linh hoạt phân tích | Cao (join lại tuỳ ý) | Cố định theo grain đã flatten |
| Chi phí ETL dựng bảng | Thấp–trung bình | Cao (join sẵn, refresh lớn) |
Đánh đổi: cái giá của sự phẳng
OBT không phải "bữa trưa miễn phí". Nó dịch chuyển độ phức tạp từ thời điểm đọc sang thời điểm ghi, và đánh đổi tính linh hoạt.
Ưu điểm
- Truy vấn & BI cực đơn giản: người phân tích chỉ
SELECT ... FROM obt— không cần hiểu khoá ngoại, không join sai grain gây fan-out nhân đôi số liệu. - Hiệu năng ổn định, dự đoán được: không phụ thuộc optimizer chọn đúng thứ tự join hay đúng broadcast.
- Hợp với công cụ self-service / no-code vốn kém trong việc mô hình hoá quan hệ.
Nhược điểm
- Trùng lặp và khó bảo trì: cùng một thuộc tính lặp ở mọi dòng. Đổi định nghĩa một cột phải chạm cả bảng.
- Cập nhật dimension đắt: trong star, đổi tên một chi nhánh là
UPDATEmột dòngdim_chi_nhanh. Trong OBT, phải rewrite mọi dòng fact mang chi nhánh đó — với bảng cột bất biến (immutable file) đây là rewrite partition tốn kém. - SCD khó hơn: lịch sử thay đổi thuộc tính (SCD type 2) khi đã phẳng thì mất ngữ cảnh "bản ghi dim nào đang hiệu lực"; phải cẩn thận snapshot đúng giá trị tại thời điểm giao dịch.
- Bùng nổ mô hình: mỗi nhu cầu phân tích mới với grain khác lại dễ đẻ ra một OBT mới → nhiều bảng wide chồng chéo, logic trùng lặp, khó giữ nhất quán định nghĩa metric. Đây chính là lý do cần semantic/metrics layer ở tầng trên.
- Grain bị đóng băng: OBT dựng ở grain "một dòng = một giao dịch" khó tái dùng cho câu hỏi ở grain khác (ví dụ "một dòng = một khách hàng-tháng").
Nguyên tắc: OBT tối ưu cho đọc lặp lại một mẫu truy vấn đã biết; star schema tối ưu cho khám phá linh hoạt nhiều mẫu chưa biết trước.
Nested & repeated: STRUCT/ARRAY thay cho bridge
Một hiểu lầm phổ biến: "denormalize là phải làm phẳng tuyệt đối". Kho cột hiện đại hỗ trợ kiểu lồng nhau — STRUCT (bản ghi con) và ARRAY (danh sách) — cho phép giữ quan hệ một–nhiều ngay bên trong một dòng, thay vì tách bảng bridge hoặc nhân dòng.
Ví dụ kinh điển: một đơn hàng có nhiều dòng hàng (line items). Cách quan hệ cần bảng orders và order_items rồi join. Trên BigQuery/Snowflake, ta nhúng line items thành một ARRAY<STRUCT<...>> ngay trong dòng đơn hàng. Truy vấn dùng UNNEST (BigQuery) / FLATTEN (Snowflake) để bung mảng ra khi cần, mà không hề tốn một join qua mạng — dữ liệu con nằm cùng chỗ với dòng cha (data locality).
Điều này giữ được lợi ích của denormalize (không shuffle) mà không trùng lặp thô thuộc tính cha ở mọi dòng con. Xem sâu về nested & repeated trong BigQuery và dữ liệu bán cấu trúc trong Snowflake.
-- BigQuery: OBT giao dịch với thuộc tính đã flatten + mảng lồng thay bridge
CREATE OR REPLACE TABLE mart.obt_giao_dich_wide AS
SELECT
t.ma_giao_dich,
t.thoi_diem,
t.so_tien,
-- thuộc tính dimension đã denormalize thẳng vào dòng:
kh.ma_khach_hang,
kh.ten_khach_hang,
kh.phan_khuc AS khach_hang_phan_khuc,
cn.ma_chi_nhanh,
cn.ten_chi_nhanh,
cn.tinh_thanh AS chi_nhanh_tinh_thanh,
-- quan hệ một-nhiều giữ trong ARRAY<STRUCT> thay vì bảng bridge:
ARRAY_AGG(
STRUCT(p.ma_phi, p.loai_phi, p.so_tien_phi)
) AS cac_khoan_phi
FROM fact.giao_dich t
JOIN dim.khach_hang kh ON kh.sk_khach_hang = t.sk_khach_hang
JOIN dim.chi_nhanh cn ON cn.sk_chi_nhanh = t.sk_chi_nhanh
LEFT JOIN fact.phi_giao_dich p ON p.ma_giao_dich = t.ma_giao_dich
GROUP BY 1,2,3,4,5,6,7,8,9;
-- Truy vấn BI: KHÔNG join, chỉ UNNEST khi cần tới mảng con
SELECT chi_nhanh_tinh_thanh,
SUM(so_tien) AS tong_giao_dich,
SUM(phi.so_tien_phi) AS tong_phi
FROM mart.obt_giao_dich_wide
LEFT JOIN UNNEST(cac_khoan_phi) AS phi
GROUP BY chi_nhanh_tinh_thanh;
Khi nào OBT hợp — và khi nào không
OBT là công cụ chuyên dụng cho tầng phục vụ (serving/mart), không phải kiến trúc cho toàn kho.
OBT hợp khi:
- Mart phục vụ đúng một dashboard / một bộ báo cáo với mẫu truy vấn ổn định. Trả tiền join một lần lúc build, đọc nhanh vô số lần.
- Feature table cho ML: mô hình cần một dòng = một thực thể (khách hàng, giao dịch) kèm hàng trăm feature. Phẳng hoá tránh join lúc train và đảm bảo train/serve consistency.
- Người dùng self-service kém về mô hình quan hệ; OBT giấu khoá ngoại, tránh join sai gây nhân đôi số liệu.
Nên tránh OBT (dùng star) khi:
- Dữ liệu cần khám phá linh hoạt ở nhiều grain khác nhau.
- Dimension biến động và cần theo dõi lịch sử (SCD2) — sửa dim trong star rẻ hơn rewrite fact.
- Nhiều nhóm cần định nghĩa dim/metric nhất quán (conformed dimension) dùng chung.
Kiến trúc kết hợp: star ở lõi, OBT ở rìa
Thực hành trưởng thành không chọn một–bỏ một. Mẫu phổ biến, khớp với medallion (bronze/silver/gold):
- Lõi (silver/gold-core): giữ star/snowflake chuẩn làm một nguồn sự thật. Fact + conformed dimension, dễ bảo trì, dễ sửa dim, dễ quản trị SCD. Đây là nơi định nghĩa dữ liệu sống.
- Rìa (mart/serving): vật chất hoá (materialize) các OBT wide phái sinh từ lõi, mỗi cái tối ưu cho một dashboard hoặc một feature store. Khi lõi đổi, ta rebuild OBT — chứ không sửa tay OBT.
- Tầng trên OBT: semantic/metrics layer chuẩn hoá định nghĩa metric để nhiều OBT/mart không định nghĩa lệch nhau.
Cách này lấy được cả hai: khả năng bảo trì & nhất quán của star ở lõi, và tốc độ đọc & đơn giản của OBT ở nơi phục vụ. Việc dựng OBT từ star thường là một model dbt chạy incremental — build một lần, xài nhiều lần.
Use case thực tế
(Số liệu minh hoạ) Tại NCB, đội BI có một dashboard giao dịch chi nhánh mà lãnh đạo mở mỗi sáng. Bản gốc chạy trên star schema: mỗi lần load, BigQuery join fact_giao_dich (~1,2 tỷ dòng/năm) với 5 dimension (khách hàng, chi nhánh, sản phẩm, kênh, thời gian). Mỗi lượt render ~9 truy vấn, mỗi truy vấn quét và shuffle vài trăm GB, độ trễ p95 ~14 giây, chi phí on-demand đáng kể vì mỗi query bung fan-out qua nhiều dim.
Đội chuyển sang mẫu star-lõi + OBT-rìa: một dbt model incremental dựng obt_giao_dich_wide mỗi đêm sau EOD, flatten toàn bộ thuộc tính dim vào fact và gom phí giao dịch vào ARRAY<STRUCT>. Dashboard giờ SELECT ... FROM obt không join. Kết quả (minh hoạ): p95 xuống ~2 giây, byte quét mỗi lượt giảm mạnh nhờ column pruning trên bảng đã denormalize, và câu SQL của lớp BI ngắn đi rõ rệt — người phân tích tự thêm chiều lọc mà không sợ join sai grain. Đổi lại: bảng lõi star vẫn là nguồn sự thật; khi dim_chi_nhanh đổi tên, chỉ cần rebuild OBT đêm hôm đó thay vì sửa tay.
Ghi nhớ
- OBT đánh đổi storage rẻ để mua không-join: hợp lý vì trên kho cột MPP, storage nén gần miễn phí còn join cần shuffle qua mạng — thao tác đắt nhất.
- Columnar khiến bảng wide không bị phạt: column pruning chỉ đọc cột cần; cột trùng lặp nén rất mạnh.
- OBT giúp BI đơn giản & đọc nhanh, ổn định, nhưng trùng lặp, khó bảo trì, cập nhật dim đắt (rewrite fact) và grain bị đóng băng.
- STRUCT/ARRAY (nested/repeated) giữ quan hệ một–nhiều trong dòng, thay bảng bridge mà không cần join — denormalize mà không nhân dòng thô.
- OBT hợp cho mart phục vụ 1 dashboard cố định và feature table cho ML; star hợp cho khám phá linh hoạt, dim biến động, cần SCD & conformed dimension.
- Kiến trúc trưởng thành: star ở lõi (nguồn sự thật) + OBT vật chất hoá ở rìa, rebuild từ lõi thay vì sửa tay; phủ semantic layer để metric nhất quán.
- Đừng để "denormalize" thành cái cớ đẻ vô số OBT chồng chéo — mỗi OBT phải có chủ đích phục vụ rõ ràng.
Nguồn tham khảo
- Ralph Kimball & Margy Ross, The Data Warehouse Toolkit (3rd ed., Wiley) — dimensional modeling, denormalization và grain.
- Joe Reis & Matt Housley, Fundamentals of Data Engineering (O'Reilly) — mô hình hoá cho kho cột, OBT vs dimensional.
- Martin Kleppmann, Designing Data-Intensive Applications (O'Reilly) — lưu trữ cột, nén, và chi phí join phân tán.
- BigQuery documentation — Nested & repeated fields,
UNNEST, columnar storage: https://cloud.google.com/bigquery/docs/nested-repeated - Snowflake documentation — Semi-structured data,
FLATTEN, VARIANT: https://docs.snowflake.com/en/user-guide/semistructured-concepts - ClickHouse documentation — Denormalization & data modeling: https://clickhouse.com/docs/en/data-modeling/denormalization
- dbt documentation — Materializations & incremental models: https://docs.getdbt.com/docs/build/materializations
Bài viết liên quan
So sánh các định dạng dữ liệu (CSV, JSON, XML, Avro, Parquet, ORC) và lý do lưu theo cột nhanh hơn cho phân tích. Bài đi sâu vào row vs columnar storage, nén (Snappy/gzip/zstd), schema evolution, OLTP vs OLAP, object storage và partitioning để tối ưu chi phí lẫn tốc độ truy vấn.
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.
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.
Vì sao một máy không đủ và cần xử lý phân tán: từ MapReduce, Hadoop/HDFS đến Apache Spark in-memory. Kiến trúc driver–executor–cluster manager, các mức trừu tượng RDD/DataFrame/Dataset, cơ chế lazy evaluation với DAG, và vì sao shuffle (wide dependency) là phần tốn kém nhất. Kèm PySpark, Spark SQL và các kỹ thuật tối ưu (partition, broadcast join, cache, chống skew) cùng khi nào KHÔNG nên dùng Spark.
Cảm nhận của bạn
Bình luận
Chưa có bình luận. Hãy là người đầu tiên chia sẻ!