Modeling nâng cao 2 — Dimensional nâng cao

22 thg 7, 2026 2 lượt xem
#data-engineering
#kimball
#data-modeling
#bus-matrix
#conformed-dimension
#bridge-table

Từ "một star" đến "một kiến trúc"

Bài Dimensional KimballFact tables đã dựng nền: fact, dimension, grain, star vs snowflake, ba loại fact. Bài này không lặp lại cơ bản — nó là hộp đồ nghề nâng cao mà Kimball dùng để ghép nhiều star rời rạc thành một kiến trúc kho dữ liệu doanh nghiệp nhất quán, và để mô hình hoá những tình huống "khó chịu" mà thực tế ngân hàng luôn ném ra: một khách hàng nhiều tài khoản, một khoản vay nhiều người bảo lãnh, một cờ trạng thái đổi mỗi ngày.

Mô hình tinh thần: một cái star đơn lẻ ai cũng vẽ được. Cái khó là khi bạn có hai chục star (giao dịch thẻ, giải ngân, thu nợ, sao kê, khiếu nại...) mà giám đốc hỏi "khách hàng phân khúc VIP đóng góp bao nhiêu doanh thu trên tất cả sản phẩm?". Nếu mỗi star tự định nghĩa "khách hàng" theo cách riêng, câu hỏi đó không trả lời được. Đó là lý do tồn tại của bus matrixconformed dimension.

Xem thêm tổng quan chuỗi tại Data Modeling nâng cao — tổng quan; phần lịch sử thay đổi chiều nâng cao ở SCD nâng cao; áp dụng trọn bộ vào nghiệp vụ ngân hàng ở Mô hình hoá ngân hàng.

Enterprise Bus Matrix & Conformed Dimensions

Enterprise bus matrix là công cụ hoạch định trung tâm của kiến trúc Kimball. Nó là một ma trận:

  • Hàng = mỗi quy trình nghiệp vụ (business process) — tương ứng một fact table dự kiến. Không phải phòng ban, không phải báo cáo, mà là sự kiện đo lường được: "giải ngân khoản vay", "giao dịch thẻ", "phát sinh phí".
  • Cột = các dimension dùng chung (conformed dimension).
  • Ô đánh dấu = quy trình đó dùng dimension đó.

"Bus" ở đây là ẩn dụ backplane bus trong máy tính: một xương sống chuẩn hoá mà mọi module cắm vào. Dimension chính là bus.

Sơ đồ trên chính là bus matrix vẽ dạng đồ thị: bốn quy trình (fact) khác nhau, nhưng cùng cắm vào cùng một Dim Khách hàng và Dim Ngày. Đó là điều làm cho việc "drill across" trở nên khả thi.

Conformed dimension là một dimension giống hệt nhau (hoặc là tập con nhất quán về mặt toán học) khi dùng ở nhiều fact. "Conformed" đòi hỏi:

  1. Cùng khoá surrogate và cùng ý nghĩa nội dung — khach_hang_sk = 5001 phải là cùng một khách hàng ở fact giải ngân và fact giao dịch thẻ.
  2. Cùng tên thuộc tính, cùng giá trị miền (domain) — cột phan_khuc có cùng tập giá trị ('VIP', 'Priority', 'Mass') ở mọi nơi.

Khi hai fact dùng chung dimension conformed, ta làm được drill-across: truy vấn riêng từng fact ở cùng mức dimension rồi hợp kết quả theo khoá chung — chứ không join hai fact với nhau (join hai fact khác grain gây double-count).

-- Drill-across: doanh thu vay + phí thẻ theo phân khúc khách hàng, cùng tháng.
-- Mỗi fact được tổng hợp RIÊNG ở cùng mức conformed dimension, rồi FULL JOIN.
WITH vay AS (
    SELECT k.phan_khuc, SUM(f.so_tien_giai_ngan) AS tong_giai_ngan
    FROM fact_giai_ngan f
    JOIN dim_khach_hang k ON k.khach_hang_sk = f.khach_hang_sk
    JOIN dim_ngay d       ON d.ngay_sk = f.ngay_giai_ngan_sk
    WHERE d.thang = '2026-06'
    GROUP BY k.phan_khuc
),
the AS (
    SELECT k.phan_khuc, SUM(f.so_tien_phi) AS tong_phi_the
    FROM fact_phi_the f
    JOIN dim_khach_hang k ON k.khach_hang_sk = f.khach_hang_sk
    JOIN dim_ngay d       ON d.ngay_sk = f.ngay_phat_sinh_sk
    WHERE d.thang = '2026-06'
    GROUP BY k.phan_khuc
)
SELECT COALESCE(v.phan_khuc, t.phan_khuc) AS phan_khuc,
       v.tong_giai_ngan, t.tong_phi_the
FROM vay v
FULL OUTER JOIN the t ON v.phan_khuc = t.phan_khuc;

Đối lập với conformed dimension là stovepipe (ống khói): mỗi bộ phận tự xây kho riêng với dimension riêng, không nói chuyện được với nhau — đúng thứ bus matrix sinh ra để chống.

Ba loại fact & bài toán additivity (góc nâng cao)

Fact tables đã mô tả ba loại; ở đây nhấn vào hệ quả additivity — thứ quyết định bạn có được phép SUM hay không.

Loại factGrainGhi/UpdateAdditivity điển hình
Transaction1 dòng / 1 sự kiệnInsert-onlyAdditive theo mọi chiều
Periodic snapshot1 dòng / thực thể / kỳInsert theo kỳSemi-additive: số dư cộng được theo khách/sản phẩm nhưng không cộng theo thời gian
Accumulating snapshot1 dòng / thực thể theo suốt vòng đờiUpdate khi qua mốcNhiều cột ngày (role-playing), lag giữa các mốc

Additive (cộng dồn được theo mọi dimension): số tiền giao dịch, số lượng. SUM tự do.

Semi-additive (cộng được theo một số dimension, không theo thời gian): điển hình là số dư tài khoản. Số dư cuối ngày của 3 khách = cộng được (ra tổng số dư). Nhưng số dư của 1 khách qua 30 ngày không được SUM — vô nghĩa. Với thời gian, dùng last (số dư cuối kỳ) hoặc average (số dư bình quân) thay vì SUM. Đây là cạm bẫy số một trong báo cáo ngân hàng.

Non-additive (không cộng được theo bất kỳ chiều nào): tỷ lệ, phần trăm, đơn giá, tỷ giá. Không bao giờ lưu sẵn "lãi suất trung bình" để SUM. Lưu tử số và mẫu số riêng dưới dạng additive, rồi tính tỷ lệ ở lớp truy vấn: SUM(lai_thu) / SUM(du_no_binh_quan).

-- SAI: SUM số dư qua thời gian (semi-additive → vô nghĩa)
SELECT SUM(so_du_cuoi_ngay) FROM fact_so_du_ngay
WHERE khach_hang_sk = 5001 AND thang = '2026-06';   -- ❌ cộng 30 ngày số dư

-- ĐÚNG: số dư bình quân tháng
SELECT AVG(so_du_cuoi_ngay) AS so_du_binh_quan FROM fact_so_du_ngay
WHERE khach_hang_sk = 5001 AND thang = '2026-06';   -- ✅

Factless fact — đo cái không có số đo

Factless fact là fact không có measure (hoặc chỉ có một cột đếm hằng = 1). Nó ghi lại sự kiện đã xảy ra hoặc một quan hệ tồn tại. Hai kiểu:

  1. Event tracking: ghi một sự kiện không sinh số tiền — ví dụ "khách đăng nhập app", "khách nhận một chiến dịch tiếp thị". Đếm số dòng chính là số lần xảy ra.
  2. Coverage / điều kiện: ghi lại những gì đáng lẽ có thể xảy ra để trả lời câu hỏi phủ định. Ví dụ kinh điển: "sản phẩm nào được phép bán ở chi nhánh nào" — cần để trả lời "sản phẩm nào không phát sinh giao dịch tháng này" (bạn không thể thấy cái vắng mặt nếu chỉ có transaction fact).

Đếm sự kiện trong factless fact dùng COUNT(*) chứ không SUM(measure).

Degenerate dimension — mã nghiệp vụ nằm ngay trong fact

Degenerate dimension (DD) là một khoá/mã nghiệp vụ nằm trực tiếp trong fact table, không có bảng dimension riêng vì nó không còn thuộc tính nào khác. Ví dụ điển hình: số hoá đơn, mã giao dịch, số hồ sơ vay. Số hoá đơn thì có ích để group các dòng chi tiết cùng một hoá đơn, nhưng bản thân nó chẳng có "màu sắc", "danh mục" gì để tách ra bảng riêng — tách ra chỉ tạo một dimension một-cột vô nghĩa. Vì thế nó "thoái hoá" (degenerate) và ở lại trong fact như một cột text/mã.

DD rất hay dùng làm "sợi chỉ" nối các dòng transaction về cùng một chứng từ gốc, phục vụ đối soát (audit trail).

Junk dimension — gom cờ và trạng thái vụn

Một fact thường bị bủa vây bởi hàng loạt cờ nhị phân và mã trạng thái low-cardinality: da_xac_thuc_otp (Y/N), kenh (ATM/POS/Online), loai_giao_dich (rut/chuyen/nap), co_khuyen_mai (Y/N). Nếu mỗi cờ thành một dimension riêng → hàng chục khoá ngoại, star biến thành "centipede" (con rết). Nếu để nguyên trong fact → phình fact bằng text.

Junk dimension gom các cờ/trạng thái vụn này vào một dimension duy nhất, mỗi dòng là một tổ hợp giá trị đã xuất hiện. Fact chỉ giữ một khoá ngoại junk_sk.

CREATE TABLE dim_junk_giao_dich (
    junk_sk        INT PRIMARY KEY,
    kenh           VARCHAR(10),   -- ATM / POS / Online
    loai_gd        VARCHAR(10),   -- rut / chuyen / nap
    da_xac_thuc    CHAR(1),       -- Y / N
    co_khuyen_mai  CHAR(1)        -- Y / N
);
-- Chỉ nạp các tổ hợp thực sự xuất hiện. Nếu miền tích Descartes nhỏ
-- (3×3×2×2 = 36) có thể nạp trọn; nếu lớn thì nạp theo nhu cầu.

Quy tắc: nếu tích Descartes của các miền là nhỏ và ổn định, nạp trước toàn bộ; nếu lớn/thưa, chỉ chèn tổ hợp mới khi gặp trong ETL.

Role-playing dimension — một dimension, nhiều vai

Một dimension vật lý duy nhất có thể xuất hiện nhiều lần trong cùng một fact với các vai (role) khác nhau. Kinh điển nhất là Dim Ngày: một accumulating snapshot hồ sơ vay có ngay_nop_sk, ngay_duyet_sk, ngay_giai_ngan_sk, ngay_tat_toan_sktất cả trỏ về cùng bảng dim_ngay.

Triển khai: giữ một bảng vật lý, tạo view/alias cho từng vai để BI hiểu rõ ngữ nghĩa.

CREATE VIEW dim_ngay_nop      AS SELECT * FROM dim_ngay;
CREATE VIEW dim_ngay_giai_ngan AS SELECT * FROM dim_ngay;

SELECT nop.ngay AS ngay_nop, gn.ngay AS ngay_giai_ngan,
       f.so_tien_giai_ngan
FROM fact_ho_so_vay f
JOIN dim_ngay_nop       nop ON nop.ngay_sk = f.ngay_nop_sk
JOIN dim_ngay_giai_ngan gn  ON gn.ngay_sk  = f.ngay_giai_ngan_sk;

Bridge table — quan hệ nhiều-nhiều & thuộc tính đa trị

Fact–dimension mặc định là quan hệ nhiều-một (nhiều fact, một khách). Nhưng thực tế đầy quan hệ nhiều-nhiều: một khoản vay có nhiều người vay/bảo lãnh; một tài khoản có nhiều chủ sở hữu; một khách thuộc nhiều phân khúc cùng lúc. Không thể nhét vào một khoá ngoại đơn.

Giải pháp Kimball là bridge table (bảng cầu): một bảng trung gian giữa fact và dimension (hoặc giữa hai dimension) mang khoá nhóm và, khi cần, cột trọng số phân bổ (allocation factor).

Cấu trúc: fact trỏ tới nhom_nguoi_vay_sk (một nhóm), bridge liệt kê từng thành viên của nhóm đó kèm trong_so.

CREATE TABLE bridge_nguoi_vay (
    nhom_sk    INT,        -- khoá nhóm (fact trỏ tới cái này)
    nguoi_sk   INT,        -- FK → dim_khach_hang (một thành viên)
    vai_tro    VARCHAR(20),-- 'chinh' / 'dong_vay' / 'bao_lanh'
    trong_so   NUMERIC(5,4)-- allocation factor; tổng mỗi nhóm = 1.0000
);

-- Truy vấn PHÂN BỔ (weighted): dư nợ chia theo trọng số → không double-count
SELECT k.nguoi_sk, SUM(f.so_tien_giai_ngan * b.trong_so) AS du_no_phan_bo
FROM fact_giai_ngan f
JOIN bridge_nguoi_vay b ON b.nhom_sk = f.nhom_nguoi_vay_sk
JOIN dim_khach_hang  k ON k.khach_hang_sk = b.nguoi_sk
GROUP BY k.nguoi_sk;

Điểm mấu chốt về double-counting: nếu join qua bridge rồi SUM(so_tien) không nhân trọng số, mỗi khoản vay bị đếm một lần cho mỗi thành viên → tổng phình lên. Hai kiểu truy vấn hợp lệ:

  • Impact (ảnh hưởng): bỏ trọng số, trả lời "khách này liên quan tới bao nhiêu dư nợ" — cố ý cho phép trùng, không được cộng ra tổng toàn hệ thống.
  • Allocated (phân bổ): nhân trong_so, tổng cộng lại đúng bằng tổng gốc — dùng cho báo cáo tài chính.

Bridge cũng dùng cho phân cấp biến thiên (variable-depth hierarchy) như sơ đồ tổ chức/cây sở hữu doanh nghiệp, nơi độ sâu không cố định.

Outrigger — dimension trỏ tới dimension (có chừng mực)

Outrigger là khi một dimension chứa khoá ngoại trỏ tới một dimension khác. Ví dụ: dim_khach_hangngay_mo_tk_sk trỏ tới dim_ngay, để lọc/nhóm khách theo thuộc tính lịch (quý mở tài khoản, ngày lễ...). Về hình thức nó giống snowflake, nhưng Kimball phân biệt: outrigger là ngoại lệ có chủ đích, hạn chế, cho một dimension phụ đã conformed (thường là Dim Ngày) — không phải chuẩn hoá tràn lan. Lạm dụng outrigger sẽ biến star đẹp thành snowflake khó dùng; dùng khi giá trị nghiệp vụ rõ ràng và dimension được trỏ tới đã ổn định.

Mini-dimension — tách thuộc tính đổi nhanh

Vấn đề: một dimension lớn (hàng triệu khách hàng) có vài thuộc tính đổi rất nhanh — điểm tín dụng, nhóm thu nhập, phân khúc hành vi. Nếu áp SCD Type 2 cho cả bảng, mỗi lần điểm tín dụng nhích một bậc lại sinh một dòng mới cho toàn bộ hồ sơ khách → bảng phình khủng khiếp ("rapidly changing monster dimension").

Mini-dimension tách nhóm thuộc tính đổi nhanh ra một dimension riêng, nhỏ, chứa các dải giá trị (banded) thay vì giá trị liên tục:

CREATE TABLE dim_ho_so_tin_dung (      -- mini-dimension
    ho_so_td_sk     INT PRIMARY KEY,
    dai_diem_td     VARCHAR(20),  -- '300-579','580-669','670-739',...  (banded)
    nhom_thu_nhap   VARCHAR(20),  -- 'thap','trung','cao'
    phan_khuc_hv    VARCHAR(20)   -- hành vi
);
-- Mỗi dòng = một TỔ HỢP dải giá trị (số dòng hữu hạn, nhỏ).
  • Fact mang cả hai khoá: khach_hang_sk (dimension chính, đổi chậm) ho_so_td_sk (mini-dimension, đổi nhanh) — chụp đúng "hồ sơ tín dụng của khách tại thời điểm giao dịch".
  • Banding: chuyển giá trị liên tục (điểm 300–850) thành dải rời rạc để số tổ hợp hữu hạn, tránh mini-dimension tự nó lại bùng nổ.
  • Muốn biết hồ sơ tín dụng hiện tại của khách mà không cần đọc fact → thêm khoá mini-dimension vào chính dimension khách (kỹ thuật outrigger ở trên), cập nhật kiểu Type 1.

Mini-dimension là "van giảm áp": giữ SCD2 cho phần đổi chậm (tên, địa chỉ), đẩy phần đổi nhanh sang bảng nhỏ theo dải — chi tiết phối hợp với SCD ở SCD nâng cao.

Use case thực tế

Bối cảnh (minh hoạ, NCB): kho phân tích tín dụng bán lẻ, ~4 triệu khách hàng, ~1,2 triệu khoản vay đang hoạt động, dữ liệu 5 năm. Các số dưới đây là minh hoạ.

  • Bus matrix: 9 quy trình (giải ngân, thu nợ, phát sinh phí, giao dịch thẻ, sao kê, khiếu nại, tiếp thị, đăng nhập app, phân loại nợ) cùng cắm vào 5 conformed dimension (Khách hàng, Ngày, Sản phẩm, Chi nhánh, Nhân viên QHKH). Nhờ conformed dimension, báo cáo "đóng góp doanh thu theo phân khúc trên mọi sản phẩm" chạy bằng drill-across thay vì 9 định nghĩa "khách hàng" mâu thuẫn.
  • Semi-additive: fact số dư cuối ngày ~1,2 tỷ dòng/năm. Báo cáo dư nợ bình quân dùng AVG theo thời gian — lần rà soát trước phát hiện một dashboard cũ SUM số dư 30 ngày khiến "tổng dư nợ" bị thổi ~30 lần.
  • Bridge người vay: ~11% khoản vay có đồng vay/bảo lãnh. Báo cáo tài chính dùng nhánh allocated (nhân trong_so, tổng khớp sổ cái); báo cáo rủi ro dùng nhánh impact (không trọng số) để thấy toàn bộ dư nợ một cá nhân dính líu.
  • Mini-dimension điểm tín dụng: tách điểm tín dụng (dải) khỏi dimension khách 4 triệu dòng, cắt việc sinh hàng chục triệu dòng SCD2 mỗi tháng xuống một mini-dimension vài nghìn tổ hợp.
  • Junk dimension: gom 6 cờ giao dịch thẻ (kênh, loại, OTP, khuyến mãi, quốc tế, hoàn tiền) vào một dim_junk, giảm 6 khoá ngoại trên fact ~2 tỷ dòng.

Ghi nhớ

  • Bus matrix (quy trình × conformed dimension) là bản thiết kế kiến trúc; conformed dimension cho phép drill-across — tổng hợp từng fact riêng ở cùng mức dimension rồi hợp, không join hai fact khác grain.
  • Additivity là hợp đồng chống tính sai: additive SUM thoải mái; semi-additive (số dư) không SUM theo thời gian — dùng last/avg; non-additive (tỷ lệ) lưu tử/mẫu riêng, chia ở truy vấn.
  • Factless fact đếm sự kiện/độ phủ bằng COUNT(*); degenerate dimension giữ mã chứng từ (số hoá đơn, mã GD) ngay trong fact.
  • Junk dimension gom cờ/trạng thái vụn thành một; role-playing tái dùng một dimension (nhất là Dim Ngày) qua nhiều vai bằng view/alias.
  • Bridge table xử lý quan hệ nhiều-nhiều/đa trị; nhớ tách impact (không trọng số, cho phép trùng) khỏi allocated (nhân trong_so, tổng khớp) để tránh double-count.
  • Outrigger = dimension trỏ dimension, dùng có chừng mực (thường tới Dim Ngày conformed).
  • Mini-dimension tách thuộc tính đổi nhanh (banded) khỏi dimension lớn, tránh bùng nổ SCD2 — phối hợp chi tiết với SCD nâng cao.

Áp dụng trọn bộ vào một kho ngân hàng end-to-end: xem Mô hình hoá ngân hàng.

Nguồn tham khảo

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.

13 thg 7, 2026 10

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

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.

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ẻ!