BI 9 — Ngôn ngữ tính toán: DAX & Measures

13 thg 7, 2026 3 lượt xem
#bi
#power-bi
#dax
#measure

Vì sao BI cần một "ngôn ngữ tính toán"?

Một mô hình dữ liệu sạch sẽ (các bảng fact, dimension, quan hệ rõ ràng) mới chỉ trả lời được những câu hỏi đơn giản: "tổng doanh số là bao nhiêu?". Nhưng câu hỏi kinh doanh thực tế hiếm khi đơn giản như vậy. Lãnh đạo muốn biết: "doanh số tháng này so với cùng kỳ năm ngoái tăng bao nhiêu phần trăm?", "tỷ lệ CASA của từng chi nhánh là bao nhiêu, và nó chiếm bao nhiêu phần trăm trên toàn hệ thống?", "lũy kế huy động từ đầu năm đến nay?".

Những con số này không nằm sẵn trong dữ liệu. Chúng phải được tính — và tính lại mỗi khi người dùng lọc, khoan sâu (drill-down), hay đổi lát cắt (slice). Đây chính là lý do mọi công cụ BI hiện đại đều đi kèm một ngôn ngữ biểu thức:

  • Power BI / Analysis Services dùng DAX (Data Analysis Expressions).
  • Tableau dùng Calculated Fields với LOD expressions (FIXED/INCLUDE/EXCLUDE).
  • Looker dùng LookML với measures và dimensions.
  • Qlik dùng cú pháp set analysis.

Bài này dùng DAX làm ví dụ điển hình vì nó là một trong những ngôn ngữ mạnh và phổ biến nhất, nhưng các khái niệm cốt lõi — ngữ cảnh tính toán, tổng hợp theo bối cảnh, time intelligence — áp dụng cho tất cả các công cụ. Khi bạn hiểu DAX, bạn hiểu cách các công cụ kia "suy nghĩ".

Điểm mấu chốt khiến ngôn ngữ tính toán BI khác với SQL hay Excel: kết quả phụ thuộc vào bối cảnh nơi nó được hiển thị. Cùng một công thức Tổng doanh số sẽ ra một con số ở dòng "Hà Nội", một con số khác ở dòng "TP.HCM", và một con số tổng ở dòng cuối — mà bạn chỉ viết công thức một lần. Hiểu được cơ chế "bối cảnh" này là chìa khóa của toàn bộ DAX.

Calculated Column vs Measure

Đây là phân biệt đầu tiên và quan trọng nhất khi viết DAX. Cả hai đều dùng cú pháp DAX, nhưng hành vi hoàn toàn khác nhau.

Calculated column (cột tính toán)

Một cột tính toán được tính một lần khi nạp/làm mới dữ liệulưu vật lý trong mô hình (chiếm bộ nhớ/dung lượng). Nó được tính theo từng dòng của bảng.

-- Cột tính toán trong bảng SoDu
SoDu[LoaiTaiKhoan] =
    IF (
        SoDu[KyHan] = 0,
        "CASA",
        "Co ky han"
    )

Mỗi dòng trong bảng SoDu sẽ có một giá trị "CASA" hoặc "Co ky han". Vì giá trị đã được tính sẵn và lưu lại, bạn có thể dùng cột này để lọc, nhóm, làm trục (axis), làm slicer — giống như một cột nguồn bình thường.

Measure (đại lượng đo)

Một measure không lưu giá trị. Nó là một công thức được tính tại thời điểm truy vấn (query time), theo bối cảnh của ô đang hiển thị. Measure không tốn dung lượng lưu trữ nhưng tốn CPU mỗi lần hiển thị.

-- Measure
Tong So Du = SUM ( SoDu[SoTien] )

Tong So Du không có giá trị cố định. Nếu đặt nó vào một bảng phân theo chi nhánh, mỗi dòng sẽ tính tổng riêng cho chi nhánh đó.

Bảng so sánh

Tiêu chíCalculated ColumnMeasure
Thời điểm tínhKhi nạp/làm mới dữ liệuTại thời điểm truy vấn (mỗi lần hiển thị)
Lưu trữLưu vật lý trong mô hình (tốn bộ nhớ)Không lưu (chỉ lưu công thức)
Bối cảnh tínhRow context (theo từng dòng)Filter context (theo ô hiển thị)
Dùng làmTrục, slicer, nhóm, lọcGiá trị trong vùng values, KPI
Hiệu năngTốn dung lượng + thời gian refreshTốn CPU lúc truy vấn
Ví dụ phù hợpPhân loại, gắn cờ, ghép khóaTổng, trung bình, tỷ lệ, %

Nguyên tắc thực hành: mặc định ưu tiên measure. Chỉ dùng calculated column khi bạn cần kết quả đó để lọc/nhóm/làm trục, hoặc khi nó là thuộc tính cố định của từng dòng (ví dụ phân loại tài khoản). Lạm dụng calculated column làm phình mô hình và chậm refresh; lạm dụng tính toán trong column những thứ vốn nên là measure (như tổng) gần như luôn sai về mặt logic.

Ngữ cảnh tính toán: Row Context vs Filter Context

Đây là khái niệm khó nhất nhưng quan trọng nhất trong DAX. Nếu chỉ nhớ một điều từ bài này, hãy nhớ phần này.

Row context (ngữ cảnh dòng)

Row context là cảm giác "đang đứng ở một dòng cụ thể". Khi DAX tính một calculated column, nó duyệt từng dòng và tại mỗi dòng nó "biết" giá trị của các cột trên chính dòng đó.

-- Trong row context, [SoTien] và [KyHan] tham chiếu tới
-- giá trị của ĐÚNG dòng hiện tại
SoDu[ThueUocTinh] = SoDu[LaiSuat] * SoDu[SoTien]

Row context tự động xuất hiện trong calculated column và bên trong các hàm iterator (SUMX, AVERAGEX...). Điều quan trọng: row context không tự động lọc các bảng khác — nó chỉ cho biết "tôi đang ở dòng nào".

Filter context (ngữ cảnh lọc)

Filter context là tập hợp tất cả các bộ lọc đang áp lên mô hình tại ô đang được tính. Nó đến từ: dòng/cột của bảng pivot, slicer, bộ lọc trang, bộ lọc trực quan, và các filter do CALCULATE thêm vào.

Khi bạn viết Tong So Du = SUM(SoDu[SoTien]) và đặt nó vào ô giao giữa "Chi nhánh = Hà Nội" và "Tháng = 6/2026", filter context là {Chi nhánh = Hà Nội, Tháng = 6/2026}. SUM chỉ cộng những dòng thỏa cả hai điều kiện đó.

Phân biệt bằng ví dụ

Đây là cái bẫy kinh điển. Tưởng tượng bảng SoDu có cột SoTien:

-- Calculated COLUMN: chỉ thấy dòng hiện tại (row context)
-- => kết quả = chính SoTien của dòng đó, KHÔNG phải tổng
SoDu[ThuColumn] = SUM ( SoDu[SoTien] )
-- Lưu ý: SUM trong column vẫn cộng toàn bảng vì SUM tự tạo
-- filter context rỗng; đây là điểm gây nhầm lẫn — xem giải thích dưới.

Để thấy rõ sự khác biệt, hãy so SUM với SUMX:

-- MEASURE dùng SUM: hoạt động theo filter context
Tong = SUM ( SoDu[SoTien] )

-- MEASURE dùng SUMX: tạo row context bên trong, duyệt từng dòng
Tong Co Trong So =
    SUMX (
        SoDu,                       -- duyệt từng dòng của SoDu
        SoDu[SoTien] * SoDu[HeSo]   -- biểu thức tính theo TỪNG dòng
    )

SUMX mở ra row context (đứng ở từng dòng để nhân SoTien * HeSo), rồi cộng tất cả lại — tất cả vẫn nằm trong filter context của ô hiển thị. Đây là lý do iterator (chữ X) tồn tại: chúng tính biểu thức dòng-theo-dòng rồi tổng hợp.

Chuyển ngữ cảnh (context transition)

Khi bạn dùng CALCULATE (kể cả gián tiếp qua measure được gọi trong iterator), DAX thực hiện context transition: nó biến row context hiện tại thành filter context. Đây là cơ chế mạnh nhưng dễ gây bất ngờ — chúng ta sẽ thấy ngay ở phần CALCULATE.

CALCULATE — hàm "vua" của DAX

CALCULATE là hàm quan trọng nhất vì nó là cách duy nhất để thay đổi filter context một cách tường minh. Cú pháp:

CALCULATE ( <biểu thức>, <bộ lọc 1>, <bộ lọc 2>, ... )

CALCULATE tính <biểu thức> sau khi áp/đè các bộ lọc vào filter context hiện tại.

-- Tổng số dư chỉ của tài khoản CASA, BẤT KỂ bối cảnh đang lọc gì
So Du CASA =
    CALCULATE (
        SUM ( SoDu[SoTien] ),
        SoDu[LoaiTaiKhoan] = "CASA"
    )

Khi measure này nằm trong dòng "Hà Nội", filter context đã có {Chi nhánh = Hà Nội}; CALCULATE thêm điều kiện LoaiTaiKhoan = "CASA". Kết quả: số dư CASA của Hà Nội.

CALCULATE có thể gỡ bỏ bộ lọc bằng ALL (rất hữu ích cho phép tính % trên tổng):

-- Tổng toàn hệ thống, bỏ qua mọi bộ lọc trên bảng SoDu
Tong Toan He Thong =
    CALCULATE (
        SUM ( SoDu[SoTien] ),
        ALL ( SoDu )
    )

Quy tắc ghi nhớ: CALCULATE = "tính biểu thức này, nhưng với một filter context mà tôi vừa chỉnh sửa". Mọi phép tính phức tạp trong DAX hầu như đều xoay quanh việc dùng CALCULATE để định hình lại filter context.

Hàm tổng hợp & Iterator: SUM vs SUMX

DAX có hai họ hàm tổng hợp:

  • Hàm tổng hợp đơn giản: SUM, AVERAGE, MIN, MAX, COUNT — nhận một cột và tổng hợp theo filter context.
  • Iterator (đuôi X): SUMX, AVERAGEX, MINX, MAXX, COUNTX — nhận một bảng + một biểu thức, duyệt từng dòng (tạo row context), tính biểu thức rồi tổng hợp.
-- KHÔNG dùng được SUM cho biểu thức nhân hai cột:
-- Doanh thu = SUM ( Ban[SoLuong] * Ban[DonGia] )   -- SAI cú pháp

-- Phải dùng iterator để có row context cho phép nhân từng dòng:
Doanh Thu =
    SUMX (
        Ban,
        Ban[SoLuong] * Ban[DonGia]
    )

Nguyên tắc: cần biểu thức trên nhiều cột tính theo từng dòng → dùng iterator. Chỉ tổng hợp một cột có sẵn → dùng hàm đơn giản (nhanh hơn, dễ đọc hơn).

Quan hệ giữa bảng & cách filter lan truyền

Sức mạnh của mô hình BI nằm ở chỗ một bộ lọc trên bảng này tự động lan sang bảng khác qua quan hệ. Trong sơ đồ sao điển hình, các bảng dimension lọc bảng fact theo một chiều (từ "phía một" sang "phía nhiều").

Khi người dùng chọn "Chi nhánh = Hà Nội" trên slicer, bộ lọc đó truyền từ Dim Chi nhanh xuống Fact So du, thu hẹp các dòng được measure tính. Đây là lý do mô hình hóa quan hệ đúng (hướng lọc, một chiều/hai chiều) quan trọng ngang với chính công thức DAX: nếu quan hệ sai hướng, measure ra số đúng cú pháp nhưng sai nghĩa.

Time Intelligence — tính toán theo thời gian

Đây là nhóm hàm trả lời các câu hỏi "lũy kế", "so kỳ trước", "tăng trưởng YoY". Điều kiện tiên quyết: phải có một bảng thời gian (Date table) liên tục, đầy đủ ngày, và đánh dấu là bảng ngày.

-- Lũy kế từ đầu năm (Year-To-Date)
So Du YTD =
    TOTALYTD (
        SUM ( SoDu[SoTien] ),
        DimThoiGian[Ngay]
    )

-- Lũy kế từ đầu tháng (Month-To-Date)
So Du MTD =
    TOTALMTD (
        SUM ( SoDu[SoTien] ),
        DimThoiGian[Ngay]
    )

-- Giá trị cùng kỳ năm trước
So Du Cung Ky Nam Truoc =
    CALCULATE (
        SUM ( SoDu[SoTien] ),
        SAMEPERIODLASTYEAR ( DimThoiGian[Ngay] )
    )

-- Tăng trưởng YoY (%)
Tang Truong YoY % =
    DIVIDE (
        SUM ( SoDu[SoTien] ) - [So Du Cung Ky Nam Truoc],
        [So Du Cung Ky Nam Truoc]
    )

Lưu ý DIVIDE thay vì toán tử /: DIVIDE xử lý an toàn trường hợp chia cho 0 (trả về trống thay vì lỗi).

Các mẫu công thức thường gặp

Tỷ lệ phần trăm trên tổng (% of total)

% Tren Tong He Thong =
    DIVIDE (
        SUM ( SoDu[SoTien] ),
        CALCULATE (
            SUM ( SoDu[SoTien] ),
            ALL ( DimChiNhanh )      -- bỏ lọc chi nhánh => mẫu số là tổng
        )
    )

Ý tưởng: tử số dùng filter context hiện tại (một chi nhánh); mẫu số dùng ALL để bỏ lọc chi nhánh, lấy tổng toàn bộ. Tỷ lệ chính là phần đóng góp của chi nhánh đó.

Running total (số lũy tiến theo thời gian)

Luy Ke So Du =
    CALCULATE (
        SUM ( SoDu[SoTien] ),
        FILTER (
            ALL ( DimThoiGian[Ngay] ),
            DimThoiGian[Ngay] <= MAX ( DimThoiGian[Ngay] )
        )
    )

Ở mỗi ô (mỗi ngày/tháng), MAX(DimThoiGian[Ngay]) cho biết mốc thời gian hiện tại; FILTER giữ lại mọi ngày nhỏ hơn hoặc bằng mốc đó, tạo hiệu ứng cộng dồn.

Ví dụ cho ngành ngân hàng

Tỷ lệ CASA

CASA (Current Account Savings Account) là tỷ lệ tiền gửi không kỳ hạn trên tổng huy động — chỉ số sống còn vì nguồn vốn giá rẻ.

So Du CASA =
    CALCULATE (
        SUM ( SoDu[SoTien] ),
        SoDu[LoaiTaiKhoan] = "CASA"
    )

Tong Huy Dong =
    SUM ( SoDu[SoTien] )

Ty Le CASA % =
    DIVIDE (
        [So Du CASA],
        [Tong Huy Dong]
    )

Cả ba measure này tự động phản ứng theo bối cảnh: đặt vào bảng phân theo chi nhánh sẽ ra tỷ lệ CASA từng chi nhánh; thêm slicer tháng sẽ ra tỷ lệ CASA theo tháng — không cần viết lại công thức.

Tăng trưởng huy động so kỳ trước

Huy Dong Thang Truoc =
    CALCULATE (
        [Tong Huy Dong],
        DATEADD ( DimThoiGian[Ngay], -1, MONTH )
    )

Tang Truong Huy Dong % =
    DIVIDE (
        [Tong Huy Dong] - [Huy Dong Thang Truoc],
        [Huy Dong Thang Truoc]
    )

DATEADD dịch toàn bộ filter context thời gian lùi một tháng, cho phép so sánh tháng hiện tại với tháng liền trước ở mọi mức khoan sâu.

Tóm tắt

Ngôn ngữ tính toán (DAX là đại diện) tồn tại vì các con số kinh doanh phải được tính lại theo từng bối cảnh người dùng đang xem. Phân biệt nền tảng nhất là calculated column (tính một lần, lưu vật lý, theo row context, dùng để lọc/nhóm) với measure (tính tại truy vấn, không lưu, theo filter context, dùng cho giá trị/KPI) — và mặc định nên ưu tiên measure. Hai loại ngữ cảnh điều khiển mọi thứ: row context ("tôi đang ở dòng nào") tự nhiên xuất hiện trong column và iterator; filter context (tập bộ lọc đang áp lên ô) đến từ slicer, trục và CALCULATE. CALCULATE là hàm trung tâm vì nó là cách tường minh để chỉnh sửa filter context (thêm điều kiện, hoặc dùng ALL để gỡ lọc). Iterator (SUMX...) cần khi tính biểu thức nhiều cột theo từng dòng. Bộ lọc lan truyền giữa các bảng qua quan hệ, nên mô hình hóa đúng hướng quan trọng ngang công thức. Cuối cùng, time intelligence (YTD, MTD, cùng kỳ năm trước, YoY) cùng các mẫu % tổng và running total là bộ công cụ tái sử dụng cho hầu hết báo cáo — như đã thấy với tỷ lệ CASA và tăng trưởng huy động ngân hàng.

Tự kiểm tra

  1. Sự khác biệt cốt lõi giữa calculated column và measure về thời điểm tính, nơi lưu trữ và loại ngữ cảnh là gì?
  2. Hãy giải thích bằng lời: vì sao cùng một measure SUM(SoDu[SoTien]) lại ra các con số khác nhau ở các dòng khác nhau của một bảng pivot?
  3. CALCULATE làm gì với filter context, và ALL(...) trong CALCULATE phục vụ mục đích gì trong phép tính "% trên tổng"?
  4. Khi nào bắt buộc phải dùng iterator như SUMX thay vì SUM? Cho một ví dụ.
  5. Để tính Tăng Trưởng YoY %, bạn cần những thành phần nào (bảng, hàm time intelligence), và vì sao nên dùng DIVIDE thay vì toán tử /?
  6. Hướng lan truyền filter qua quan hệ ảnh hưởng thế nào đến tính đúng của một measure, dù công thức đúng cú pháp?

Đọc tiếp

BI 10 — Data Storytelling

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.

13 thg 7, 2026 4

Kimball dimensional modeling: bảng fact/dimension, star vs snowflake, grain, và Slowly Changing Dimension.

13 thg 7, 2026 3

Khối OLAP, các thao tác drill-down/roll-up/slice & dice/pivot, OLAP vs OLTP và ROLAP/MOLAP/HOLAP.

13 thg 7, 2026 3

Từ nguồn dữ liệu qua ETL/ELT vào Data Warehouse, Data Mart đến dashboard; staging, ODS, batch vs streaming.

13 thg 7, 2026 3