Power BI 3 — Mô hình dữ liệu & quan hệ (star schema)
Vì sao mô hình quan trọng hơn visual
Người mới học Power BI thường nghĩ báo cáo đẹp hay dở nằm ở visual: chọn loại biểu đồ, phối màu, bố cục. Nhưng khi số liệu bắt đầu sai, DAX viết mãi không ra, và báo cáo giật lag mỗi lần lọc — thì nguyên nhân gần như luôn nằm ở mô hình dữ liệu (data model), không phải ở visual.
Lý do rất đơn giản: trong Power BI, mọi con số bạn thấy trên màn hình đều là kết quả của một công thức DAX được đánh giá trong một ngữ cảnh lọc. Ngữ cảnh đó được tạo ra bởi các bộ lọc (slicer, hàng/cột của bảng, chọn điểm trên biểu đồ) rồi lan truyền qua các quan hệ giữa các bảng. Nếu quan hệ đặt sai, hướng lọc đặt sai, hay cột có cardinality quá cao, thì:
- DAX trở nên khó viết, phải "vá" bằng những công thức phức tạp không cần thiết;
- con số ra sai hoặc double-count mà không ai để ý;
- engine phải quét nhiều hơn, mô hình phình to, báo cáo chậm.
Nói cách khác: visual chỉ trình bày lại thứ mô hình cho phép. Đầu tư vào mô hình đúng là đầu tư có lãi kép — nó làm DAX ngắn đi, số liệu đáng tin, và hiệu năng tốt lên cùng một lúc. Bài này nhìn mô hình chiều (đã học ở BI — Mô hình hoá chiều) từ góc riêng của Power BI.
Star schema: cấu trúc mà Power BI được sinh ra để chạy
Star schema (lược đồ sao) là cách bố trí: một (hoặc vài) bảng fact ở trung tâm, chứa các sự kiện đo lường được (số tiền, số lượng, số giao dịch), bao quanh là các bảng dimension (chiều) mô tả bối cảnh của các sự kiện đó (khách hàng nào, sản phẩm gì, ngày nào, chi nhánh nào).
- Fact table: dài (nhiều dòng), hẹp về mặt "ý nghĩa" — chủ yếu là các khoá ngoại trỏ tới dimension và vài cột số để tính toán (measure). Ví dụ:
Salesvới các cộtDateKey,ProductKey,CustomerKey,Quantity,Amount. - Dimension table: ngắn hơn (ít dòng hơn nhiều), rộng về mô tả — chứa thuộc tính dùng để nhóm, lọc, cắt lát. Ví dụ:
DimProductvớiProductKey,ProductName,Category,Brand.
Power BI ưu tiên star schema không phải vì thẩm mỹ mà vì ba lý do kỹ thuật rất cụ thể:
- DAX đơn giản hơn. Các hàm như
CALCULATE,FILTERgiả định filter đi từ dimension xuống fact. Star schema khớp chính xác với giả định này, nên phần lớn measure chỉ cần vài dòng. Xem sâu về ngữ cảnh trong DAX — Row/Filter context. - Filter propagation rõ ràng. Khi bạn chọn
Category = "Loan"trên slicer, filter chạy từ dimension → fact theo một đường duy nhất, dễ suy luận. Mô hình càng nhiều "vòng" hay nhiều đường, càng khó đoán con số ra từ đâu. - Engine VertiPaq tối ưu cho cấu trúc này. Fact hẹp + khoá số nguyên nén rất tốt; join giữa fact và dimension qua khoá nguyên là đường đi mà engine được thiết kế để chạy nhanh.
Nguyên tắc thực dụng: khi phân vân, hãy làm mọi thứ về star. Đây là khuyến nghị chính thức của Microsoft cho gần như mọi mô hình Power BI.
Mũi tên chỉ hướng lọc: dimension lọc fact, không ngược lại. Đây là hình dạng "sao" kinh điển — fact ở giữa, dimension toả ra, mọi cạnh là quan hệ 1-tới-nhiều lọc một chiều.
Quan hệ (relationship): xương sống của mô hình
Quan hệ nối một cột ở bảng này với một cột ở bảng kia. Ba yếu tố quyết định hành vi của quan hệ: cardinality, cross-filter direction, và trạng thái active/inactive.
Cardinality (lực lượng quan hệ)
- One-to-many (1:*) — chuẩn mực. Một dòng dimension ứng với nhiều dòng fact.
DimCustomer[CustomerKey](mỗi khách một dòng, giá trị duy nhất) nối tớiFactSales[CustomerKey](mỗi khách xuất hiện nhiều lần). Đây là quan hệ bạn nên dùng trong ~95% trường hợp. Bên "một" phải chứa giá trị duy nhất, không null. - Many-to-many (*:*) — cẩn thận. Cả hai phía đều có giá trị lặp. Power BI cho phép, nhưng nó dễ gây ra kết quả gây bối rối (đặc biệt với con số tổng), khó suy luận, và thường là dấu hiệu bạn đang thiếu một bảng cầu (bridge table). Hãy coi many-to-many là ngoại lệ có chủ đích, không phải mặc định.
- One-to-one (1:1). Hiếm. Nếu hai bảng có quan hệ 1:1, thường chúng nên được gộp thành một bảng ngay trong Power Query. Xem Power Query & ngôn ngữ M.
Cross-filter direction (hướng lọc chéo)
Đây là phần bị hiểu sai nhiều nhất.
- Single (một chiều) — mặc định và nên dùng: filter chỉ chạy theo một hướng, thường từ dimension → fact. Một đầu mũi tên trên đường nối biểu thị hướng này. Chọn
Categorysẽ lọcFactSales, đúng như mong đợi. - Both / bi-directional (hai chiều): filter chạy cả hai hướng — dimension lọc fact và fact lọc ngược lại dimension. Hai đầu mũi tên biểu thị điều này. Nghe tiện, nhưng Microsoft khuyến nghị hạn chế tối đa vì nó làm chậm truy vấn, tạo trải nghiệm gây bối rối, và với mô hình có nhiều fact/dimension có thể tạo đường lọc mơ hồ khiến engine không biết đi đường nào.
Thay vì bật hai chiều cho cả quan hệ, thực hành tốt là để một chiều và chỉ mở lọc hai chiều cục bộ trong một measure cụ thể bằng hàm CROSSFILTER khi thật sự cần.
-- (minh hoạ) chỉ bật lọc hai chiều cho riêng measure này
Sản phẩm được mua =
CALCULATE(
DISTINCTCOUNT( DimProduct[ProductKey] ),
CROSSFILTER( FactSales[ProductKey], DimProduct[ProductKey], BOTH )
)
Active vs inactive relationship
Giữa hai bảng chỉ có một đường lọc active tại một thời điểm (đường liền nét). Nếu cần thêm đường thứ hai (ví dụ fact có cả OrderDateKey lẫn ShipDateKey cùng trỏ về DimDate), Power BI buộc bạn đặt quan hệ thứ hai là inactive (đường đứt nét). Quan hệ inactive không tự lan truyền filter — nó chỉ được "kích hoạt" tạm thời trong một phép tính bằng USERELATIONSHIP.
-- (minh hoạ) doanh số tính theo ngày giao hàng (quan hệ inactive)
Doanh số theo ngày giao =
CALCULATE(
[Total Sales],
USERELATIONSHIP( FactSales[ShipDateKey], DimDate[DateKey] )
)
Lưu ý phân biệt: USERELATIONSHIP kích hoạt quan hệ inactive; CROSSFILTER đổi hướng lọc của quan hệ đang có nhưng không làm active một quan hệ inactive.
Filter propagation: hiểu để không đặt sai hướng
Hãy hình dung filter như dòng nước chảy theo mũi tên trên các quan hệ. Quy tắc vàng: dimension lọc fact.
Khi người dùng chọn DimDate[Year] = 2025 và DimProduct[Category] = "Loan":
- Filter từ
DimDatechảy xuốngFactSales, giữ lại các dòng cóDateKeythuộc 2025. - Filter từ
DimProductcũng chảy xuốngFactSales, giữ lại các dòng cóCategory = "Loan". - Measure (ví dụ
SUM(FactSales[Amount])) được tính trên tập dòng còn lại — giao của các bộ lọc.
Điều gì xảy ra khi đặt sai hướng? Giả sử ai đó bật lọc hai chiều giữa DimCustomer và FactSales. Filter giờ có thể chảy DimProduct → FactSales → DimCustomer → …, tạo ra hiệu ứng lọc chéo ngoài chủ đích. Con số vẫn "ra một giá trị nào đó" nhưng không đúng nghĩa nghiệp vụ — loại lỗi cực khó gỡ vì báo cáo không báo lỗi, chỉ trả sai âm thầm.
Star vs Snowflake: nên làm phẳng dimension
Snowflake schema là khi dimension được chuẩn hoá thành nhiều bảng con nối chuỗi: thay vì một DimProduct chứa cả Category và Brand, ta tách thành DimProduct → DimCategory → DimDepartment. Đúng chuẩn hoá cơ sở dữ liệu quan hệ, nhưng không lý tưởng cho Power BI:
- Mỗi bảng con thêm một "hop" (bước nhảy) mà filter phải đi qua → chậm hơn và khó suy luận.
- DAX phức tạp hơn khi cần thuộc tính ở bảng xa.
- Lợi ích tiết kiệm dung lượng của chuẩn hoá gần như biến mất vì VertiPaq nén cột rất tốt (xem phần dưới).
Khuyến nghị: làm phẳng (denormalize / flatten) các bảng dimension chuẩn hoá thành một dimension rộng duy nhất, thực hiện việc gộp này ngay trong Power Query. Đưa Category, Brand, Department vào chung một DimProduct.
Role-playing dimension
Một dimension có thể đóng nhiều vai trong cùng mô hình. Điển hình là DimDate: fact bán hàng thường có cả ngày đặt hàng và ngày giao hàng, cả hai đều là ngày. Có hai cách xử lý:
- Một
DimDatedùng chung + hai quan hệ (một active cho order date, một inactive cho ship date), rồi dùngUSERELATIONSHIPkhi cần đổi vai. Gọn về mô hình. - Nhân bản
DimDatethànhDimOrderDatevàDimShipDate(dùng calculated table hoặc reference query). Rõ ràng cho người dùng cuối vì mỗi vai có slicer riêng, nhưng tốn thêm một bảng.
Cả hai đều hợp lệ; chọn tuỳ vào việc người dùng có cần lọc đồng thời theo cả hai loại ngày trong một trực quan hay không.
Bảng Date chuyên dụng
Time intelligence trong DAX (TOTALYTD, SAMEPERIODLASTYEAR, DATEADD…) yêu cầu một bảng ngày liên tục, đầy đủ — không có ngày trống, phủ toàn bộ khoảng thời gian của dữ liệu, và có một cột kiểu Date/DateTime duy nhất làm khoá.
Sau khi tạo bảng này, cần Mark as date table (đánh dấu là bảng ngày) và chỉ định cột ngày. Bước này báo cho engine rằng đây là chiều thời gian "chính chủ", giúp các hàm time intelligence hoạt động đúng và cho phép ẩn hệ thống phân cấp ngày tự sinh (auto date/time) vốn làm phình mô hình. Chi tiết áp dụng time intelligence nằm ở DAX — Row/Filter context.
Đừng dùng cột ngày nằm rải rác trong fact table làm chiều thời gian. Luôn dùng một DimDate riêng, nối tới fact qua khoá ngày.
VertiPaq: vì sao giảm cardinality làm mô hình nhỏ và nhanh
Ở chế độ Import, Power BI dùng VertiPaq — engine lưu trữ theo cột (columnar), nén, in-memory. Hiểu vài nguyên tắc của nó sẽ thay đổi cách bạn thiết kế mô hình. (So sánh Import vs DirectQuery ở Chế độ lưu trữ.)
- Lưu theo cột, không theo dòng. Mỗi cột được nén độc lập. Cột có ít giá trị khác biệt (low cardinality) nén cực tốt; cột có nhiều giá trị khác biệt (high cardinality) nén kém.
- Từ điển (dictionary) + chỉ mục. Với dictionary encoding, engine liệt kê các giá trị duy nhất vào một "từ điển", rồi lưu ở mỗi dòng chỉ một số nguyên chỉ vào từ điển. Cột 10 triệu dòng nhưng chỉ 500 giá trị khác nhau → từ điển 500 mục, phần dữ liệu chỉ là các số nguyên nhỏ. Nén khổng lồ.
- Cardinality quyết định tất cả. Cột càng nhiều giá trị duy nhất → từ điển càng lớn → nén càng kém → mô hình càng to, quét càng chậm. Kích thước từ điển phụ thuộc cả số giá trị duy nhất lẫn kích thước mỗi giá trị (chuỗi dài, GUID, URL rất đắt).
- RLE (run-length encoding) phụ thuộc thứ tự sắp xếp. Nếu dữ liệu sắp theo một cột, các giá trị giống nhau nằm liền kề và nén thành vài "run"; cùng một cột có thể chênh nhau tới ~100 lần bộ nhớ chỉ vì thứ tự sắp xếp khác nhau.
Hệ quả thực hành cho thiết kế mô hình:
- Tránh cột high-cardinality vô ích. Cột đắt nhất thường là các ID kỹ thuật, GUID, timestamp đầy đủ tới giây, hay khoá dài dạng chuỗi. Nếu không dùng để lọc/nhóm/nối, bỏ đi.
- Tách datetime thành Date + Time riêng. Một cột
DateTimetới giây có cardinality khổng lồ. Tách thành cộtDate(cardinality thấp) và, nếu cần, cộtTimeriêng — tổng bộ nhớ giảm mạnh. - Ưu tiên khoá số nguyên thay vì khoá chuỗi cho quan hệ.
- Đừng ném cả một bảng rộng phẳng khổng lồ vào mô hình. Một "big flat table" gộp tất cả (fact + mọi thuộc tính dimension lặp đi lặp lại) làm bùng nổ cardinality trên các cột mô tả và phá vỡ lợi thế nén — trái ngược hẳn với star schema, nơi thuộc tính mô tả nằm gọn trong dimension nhỏ.
Bạn có thể dùng DAX Studio (View Metrics) hoặc VertiPaq Analyzer để soi cột nào chiếm nhiều bộ nhớ nhất và bắt đầu tối ưu từ đó.
Use case thực tế
Mô hình giao dịch ngân hàng theo star
Bài toán: dựng báo cáo phân tích giao dịch cho một ngân hàng — theo dõi doanh số cho vay, số dư, số giao dịch theo chi nhánh, sản phẩm, khách hàng và thời gian.
Thiết kế star:
FactTransaction(fact trung tâm): mỗi dòng là một giao dịch. Cột:DateKey,CustomerKey,ProductKey(loại sản phẩm: vay tín chấp, thế chấp, thẻ…),BranchKey, và các cột sốAmount,Fee,Balance. Chỉ giữ khoá số nguyên + số đo; không nhét tên khách hàng, tên chi nhánh vào đây.DimCustomer:CustomerKey,Segment(cá nhân/doanh nghiệp),City,AgeBand.DimProduct:ProductKey,ProductName,ProductType,RiskCategory— đã làm phẳng, không tách thêm bảng con.DimBranch:BranchKey,BranchName,Region.DimDate: bảng ngày liên tục, đã Mark as date table; nếu giao dịch có ngày phát sinh và ngày hạch toán thì dùng role-playing (quan hệ active cho ngày phát sinh, inactive cho ngày hạch toán).
Mọi quan hệ là 1:* single direction, dimension lọc fact. Measure điển hình Tổng dư nợ = SUM(FactTransaction[Amount]) chỉ một dòng, và mọi slicer (Region, Segment, ProductType, Year) tự động lọc đúng nhờ filter propagation. Đây là mô hình nhỏ, nhanh, dễ mở rộng.
Sửa một mô hình snowflake
Bạn nhận một file có sẵn với dimension sản phẩm bị chuẩn hoá: FactSales → DimProduct → DimSubCategory → DimCategory. Báo cáo chậm, và mỗi lần cần lọc theo Category phải "nhảy" qua ba bảng.
Cách sửa:
- Trong Power Query, merge
DimProduct,DimSubCategory,DimCategorythành một bảngDimProductrộng chứa đủProductName,SubCategory,Category. - Xoá hai bảng con và các quan hệ chuỗi.
- Nối
DimProduct(đã phẳng) trực tiếp tớiFactSalesbằng quan hệ 1:* single direction. - Kiểm tra measure vẫn đúng, và dùng VertiPaq Analyzer xác nhận kích thước không tăng (thường còn giảm nhờ bớt bảng và bớt cột khoá thừa).
Kết quả: mô hình trở về star chuẩn, filter đi một hop, DAX gọn, báo cáo nhanh hơn.
Bẫy thường gặp
- Bi-directional lan tràn. Bật lọc hai chiều "cho chắc" khắp nơi → đường lọc mơ hồ, số sai âm thầm, truy vấn chậm. Mặc định để single; mở hai chiều cục bộ bằng
CROSSFILTERkhi thật cần. - Many-to-many tuỳ tiện. Thường là dấu hiệu thiếu bridge table. Nếu dùng, phải hiểu rõ hệ quả với các phép tổng.
- Dùng calculated column thay vì measure. Calculated column được vật chất hoá và lưu trong mô hình (tốn bộ nhớ, tăng cardinality, không phản ứng theo ngữ cảnh lọc động). Đại lượng tổng hợp gần như luôn nên là measure — tính lúc truy vấn, không chiếm dung lượng. (Xem DAX context.)
- Mô hình quá chuẩn hoá (snowflake). Đẹp theo lý thuyết CSDL, tệ cho Power BI. Làm phẳng dimension.
- Cột high-cardinality vô dụng. GUID, timestamp tới giây, mã kỹ thuật không dùng để phân tích — bỏ hoặc tách nhỏ.
- Big flat table. Gộp tất cả vào một bảng khổng lồ phá vỡ nén và filter propagation. Star gần như luôn tốt hơn.
- Không có DimDate chuyên dụng. Time intelligence sẽ không hoạt động đúng nếu thiếu bảng ngày liên tục được Mark as date table.
Ghi nhớ
- Star schema là mặc định: fact ở giữa (khoá + số đo), dimension bao quanh (thuộc tính mô tả). Khi phân vân, làm về star.
- Quan hệ chuẩn = 1-tới-nhiều, một chiều, dimension lọc fact. Many-to-many và bi-directional là ngoại lệ có chủ đích, không phải mặc định.
- Filter propagation là cơ chế cốt lõi: hiểu dòng filter chảy theo mũi tên để không đặt sai hướng.
- Làm phẳng dimension (bỏ snowflake); dùng role-playing cho các dimension đóng nhiều vai (ví dụ Date cho order/ship).
- DimDate chuyên dụng + Mark as date table là điều kiện cho time intelligence.
- VertiPaq thưởng cho cardinality thấp: giảm giá trị duy nhất, tránh cột đắt vô ích, tránh big flat table → mô hình nhỏ và nhanh.
- Dùng measure, không calculated column cho đại lượng tổng hợp.
Đọc tiếp
- Power Query & ngôn ngữ M — nơi làm sạch và làm phẳng dữ liệu trước khi vào mô hình.
- DAX — Row & Filter context — hiểu ngữ cảnh để đọc đúng con số mô hình sinh ra.
- Chế độ lưu trữ: Import & DirectQuery — VertiPaq (Import) so với truy vấn trực tiếp nguồn.
- BI — Mô hình hoá chiều: Fact, Dimension, Star & SCD — nền tảng mô hình chiều theo Kimball.
Bài viết liên quan
Phân biệt ước lượng điểm và ước lượng khoảng, cách xây khoảng tin cậy (CI) bằng margin of error z*·SE / t*·SE, đánh đổi mức 90/95/99%, và cách DIỄN GIẢI ĐÚNG khoảng tin cậy (95% CI không phải xác suất tham số nằm trong khoảng). Có CI cho trung bình và cho tỷ lệ, ảnh hưởng của cỡ mẫu, cùng ví dụ ngân hàng về tỷ lệ nợ xấu và số dư trung bình.
Vì sao phải lấy mẫu thay vì đo toàn bộ dân số, các phương pháp lấy mẫu (ngẫu nhiên đơn giản, phân tầng, cụm, hệ thống) và những thiên lệch cần tránh. Giải thích sampling error vs bias, phân phối của trung bình mẫu, standard error = σ/√n, Định lý giới hạn trung tâm (CLT) và bootstrap — nền móng của mọi suy diễn thống kê.
Chọn biểu đồ đúng cho từng loại dữ liệu, nguyên tắc thiết kế dashboard, và kể chuyện bằng dữ liệu.
Bài mở đầu series thống kê: đo xu hướng trung tâm và độ phân tán, phân vị và five-number summary, hình dạng phân phối, và cách xử lý dữ liệu lệch phải điển hình trong ngân hàng. Kèm SQL chạy được trên PostgreSQL sandbox.