Power BI 4 — DAX nền tảng: row context & filter context
Vì sao phải bắt đầu từ "ngữ cảnh"?
Rất nhiều người học DAX theo kiểu học thuộc hàm: nhớ SUM, CALCULATE, SUMX, RELATED... rồi ghép lại theo cảm tính. Cách này đi được một đoạn ngắn rồi tắc — vì DAX không phải một ngôn ngữ tính toán thông thường. Cùng một công thức, đặt ở hai chỗ khác nhau trên báo cáo, cho ra hai con số khác nhau. Không phải vì công thức sai, mà vì ngữ cảnh (context) xung quanh nó đã đổi.
Toàn bộ DAX xoay quanh hai loại ngữ cảnh: row context (ngữ cảnh dòng) và filter context (ngữ cảnh lọc). Hiểu đúng hai khái niệm này — và cách chúng chuyển hóa lẫn nhau qua CALCULATE — là ranh giới giữa người "viết được vài measure đơn giản" và người thật sự làm chủ DAX. Đây là bài khó và quan trọng nhất trong loạt bài, nên hãy đọc chậm.
Bài này giả định bạn đã nắm Mô hình dữ liệu star schema và quan hệ. Sau bài này, DAX nâng cao & time intelligence sẽ dùng lại toàn bộ nền tảng ở đây để tính YTD, YoY, MTD. Nếu bạn mới đến, hãy xem Tổng quan Power BI trước.
Calculated Column vs Measure — phân biệt sống còn
Cả hai đều viết bằng DAX, nhưng bản chất tính toán hoàn toàn khác. Nhầm hai thứ này là nguyên nhân số một của các lỗi "số không khớp".
Calculated column (cột tính toán)
Một calculated column được tính một lần khi refresh dữ liệu, kết quả lưu vật lý trong model (tốn RAM và dung lượng file), và được tính theo từng dòng của bảng.
-- Cột tính toán trong bảng Sales (minh hoạ)
Sales[ThanhTien] = Sales[SoLuong] * Sales[DonGia]
Ở đây DAX biết Sales[SoLuong] và Sales[DonGia] là giá trị của dòng đang xét. Đó chính là row context: khi tính cột, DAX duyệt qua từng dòng và tại mỗi dòng nó "nhìn thấy" giá trị của dòng đó.
Measure (thước đo)
Một measure không lưu giá trị. Nó chỉ lưu công thức, và được tính lại mỗi khi truy vấn — tức mỗi ô của bảng, mỗi cột của biểu đồ, mỗi lần bạn kéo slicer. Measure tính theo filter context của nơi nó xuất hiện, không phải theo dòng.
-- Measure (minh hoạ)
Doanh thu = SUM ( Sales[ThanhTien] )
Measure Doanh thu này khi đặt ở dòng "Hà Nội" ra con số của Hà Nội, ở dòng "TP.HCM" ra con số của TP.HCM, ở dòng tổng ra tổng toàn quốc — bạn chỉ viết một lần. Sự khác biệt đến từ filter context của từng ô.
Khi nào dùng cái nào?
| Tiêu chí | Calculated column | Measure |
|---|---|---|
| Thời điểm tính | Khi refresh | Khi truy vấn (theo filter context) |
| Lưu trữ | Có, tốn RAM/dung lượng | Không, chỉ lưu công thức |
| Ngữ cảnh | Row context | Filter context |
| Dùng để | Slice/filter/nhóm/quan hệ, giá trị theo dòng | Tổng hợp: tổng, trung bình, %, tỷ lệ |
Quy tắc ưu tiên: mặc định dùng measure. Chỉ tạo calculated column khi bạn cần một cột để lọc, nhóm, làm slicer, hoặc làm khóa quan hệ — những việc measure không làm được. Lý do:
- Measure không tốn dung lượng lưu trữ và không làm phình model.
- Measure phản ứng đúng theo bối cảnh người dùng chọn; column thì "đóng băng" tại thời điểm refresh.
- Một calculated column phân loại (ví dụ nhóm khách hàng theo doanh số) trông tiện nhưng sẽ sai khi người dùng lọc, vì nó không tính lại theo filter.
Một sai lầm kinh điển: viết TyLe = Sales[ThanhTien] / SUM(Sales[ThanhTien]) làm calculated column và mong ra "tỷ trọng". Nó sẽ cho kết quả vô nghĩa, vì SUM trong một cột lại chạy trong ngữ cảnh không có bộ lọc nào (xem phần context transition). Tỷ lệ đóng góp phải là measure.
Row context — "dòng hiện tại"
Row context là khái niệm "dòng đang được xét là dòng nào". Nó tồn tại trong hai trường hợp:
- Trong calculated column — DAX tự động duyệt từng dòng.
- Trong iterator — các hàm kết thúc bằng
XnhưSUMX,AVERAGEX,FILTER,MAXX... duyệt qua một bảng, mỗi lần một dòng.
-- Iterator SUMX (minh hoạ)
Doanh thu = SUMX ( Sales, Sales[SoLuong] * Sales[DonGia] )
SUMX duyệt từng dòng của Sales, tại mỗi dòng biểu thức bên trong có row context nên đọc được Sales[SoLuong] và Sales[DonGia] của đúng dòng đó, rồi cộng dồn tất cả. Cách này còn tránh phải tạo cột ThanhTien vật lý.
Điểm cực kỳ quan trọng — và hay bị hiểu sai: row context KHÔNG tự động lọc bảng. Nó chỉ cho biết "dòng hiện tại là dòng nào", chứ không biến toàn bộ engine chỉ thấy dòng đó. Nếu bên trong iterator bạn gọi SUM(Sales[ThanhTien]), hàm SUM này không bị giới hạn ở dòng hiện tại — nó vẫn cộng theo filter context bên ngoài. Row context và filter context là hai thế giới tách biệt; muốn bắc cầu giữa chúng phải dùng context transition (nói ở dưới).
Filter context — bộ lọc đang áp
Filter context là tập hợp tất cả các bộ lọc đang áp lên model tại thời điểm tính. Nó quyết định measure "nhìn thấy" tập dữ liệu nào. Filter context đến từ nhiều nguồn cùng lúc:
- Toạ độ của ô trong visual: dòng "Hà Nội" trong bảng tự áp filter
Branch[City] = "Hà Nội". - Slicer và filter pane: chọn năm 2026 áp filter
Date[Year] = 2026. - Quan hệ (relationship): filter trên dimension lan sang fact (xem phần propagation).
CALCULATE: cho phép bạn chủ động thêm/bớt/sửa filter.
Tất cả các filter này giao nhau (AND). Measure Doanh thu = SUM(Sales[ThanhTien]) ở ô giao của dòng "Hà Nội" và cột "2026" sẽ cộng doanh thu của các dòng Sales thoả đồng thời cả hai điều kiện.
Điểm mấu chốt: measure không có filter context riêng — nó thừa hưởng filter context của nơi được gọi. Chính vì vậy cùng một measure cho ra vô số kết quả khác nhau trên cùng một báo cáo.
CALCULATE — hàm quan trọng nhất của DAX
Nếu chỉ được nhớ một hàm, hãy nhớ CALCULATE. Đây là hàm duy nhất có khả năng thay đổi filter context một cách trực tiếp.
Cú pháp:
CALCULATE ( <biểu thức>, <filter1>, <filter2>, ... )
<biểu thức>thường là một phép tổng hợp hoặc một measure.- Các
<filter>sửa đổi filter context trước khi biểu thức được tính.
Ví dụ điển hình:
-- Doanh thu chỉ tính cho sản phẩm nhóm "VIP" (minh hoạ)
Doanh thu VIP =
CALCULATE (
SUM ( Sales[ThanhTien] ),
Product[Segment] = "VIP"
)
CALCULATE lấy filter context hiện tại, thêm điều kiện Product[Segment] = "VIP", rồi mới tính SUM. Dù người dùng đang ở dòng nào, kết quả luôn bị ép về phân khúc VIP.
Filter là boolean hay table
Đối số filter của CALCULATE có hai dạng, và hiểu điều này giúp bạn đọc được mọi công thức nâng cao:
- Boolean filter như
Product[Segment] = "VIP"thực chất là đường tắt (syntax sugar). DAX ngầm dịch nó thành một bảng:FILTER(ALL(Product[Segment]), Product[Segment] = "VIP"). Chú ý chữALL: boolean filter thay thế filter cũ trên cột đó, chứ không cộng dồn. - Table filter là một biểu thức trả về bảng, thường dùng
FILTER(...)khi điều kiện phức tạp:
-- Filter dạng table cho điều kiện phức tạp (minh hoạ)
Doanh thu đơn lớn =
CALCULATE (
SUM ( Sales[ThanhTien] ),
FILTER ( Sales, Sales[ThanhTien] > 10000000 )
)
Các CALCULATE modifier: quản trị filter context
Đây là những công cụ tinh chỉnh cách CALCULATE đối xử với filter hiện có. Nội dung này dễ sai nên mô tả kỹ:
ALL(table hoặc cột)— bỏ mọi filter trên bảng/cột chỉ định. Dùng nhiều nhất để tính "tổng bất kể lát cắt" cho phép chia % of total.REMOVEFILTERS(...)— là bí danh (alias) củaALLkhi dùng trongCALCULATE; không có khác biệt về kết quả. Điểm khác:REMOVEFILTERSchỉ dùng được như modifier củaCALCULATE, không dùng như một biểu thức bảng độc lập. Tên gọi rõ nghĩa hơn khi ý định chỉ là "xoá filter".ALLEXCEPT(table, cột giữ lại, ...)— bỏ filter trên toàn bộ bảng trừ các cột được liệt kê. Dùng khi muốn giữ đúng một cấp độ nhóm và xoá phần còn lại.ALLSELECTED(...)— bỏ filter đến từ chính visual hiện tại, nhưng giữ filter từ slicer/filter pane bên ngoài. Đây là hàm để tính "tổng theo lựa chọn người dùng" (visual total) — mẫu số của % of total tôn trọng slicer.KEEPFILTERS(...)— không xoá filter cũ xung đột mà cộng thêm filter mới (giao nhau thay vì thay thế). Nhớ rằng boolean filter mặc định là thay thế; bọcKEEPFILTERSđể chuyển sang cộng dồn.
Context transition — nguồn gốc mọi nhầm lẫn
Đây là phần khó nhất. Hãy đọc thật chậm.
Định nghĩa: khi CALCULATE chạy mà đang có row context, nó biến row context thành filter context tương đương trước khi áp các filter khác. Đó gọi là context transition. Filter context mới sinh ra chứa tất cả các cột đang được iterate ở dòng hiện tại.
Có một sự thật ẩn khiến điều này xảy ra ở khắp nơi: mọi lời gọi measure đều được bọc ngầm bởi một CALCULATE. Nghĩa là chỉ cần bạn gọi tên một measure bên trong iterator hay calculated column, context transition tự động xảy ra.
Ví dụ đối chiếu để thấy rõ. Trong một calculated column của bảng Sales:
-- Cột A: KHÔNG có CALCULATE (minh hoạ)
Sales[TongToanBang] = SUM ( Sales[ThanhTien] )
-- Cột B: CÓ CALCULATE (minh hoạ)
Sales[TongDongNay] = CALCULATE ( SUM ( Sales[ThanhTien] ) )
- Cột A cho ra grand total ở mọi dòng. Vì calculated column không có filter context (filter context rỗng),
SUMcộng tất cả những gì nó nhìn thấy — tức toàn bảng. Row context chỉ tồn tại nhưng không lọcSUM. - Cột B cho ra đúng giá trị của dòng hiện tại.
CALCULATEthực hiện context transition: biến "dòng hiện tại" thành filter context, nênSUMchỉ còn thấy đúng dòng đó.
Đây chính là bằng chứng "row context không tự lọc". Chỉ khi có CALCULATE (dù ngầm hay hiện) thì dòng hiện tại mới trở thành bộ lọc thật.
Một ví dụ nữa với iterator — mẫu tính rất hay gặp:
-- Trung bình doanh thu mỗi khách hàng (minh hoạ)
DT bình quân KH =
AVERAGEX (
VALUES ( Customer[CustomerID] ),
[Doanh thu]
)
AVERAGEX duyệt từng khách hàng (row context trên Customer[CustomerID]). Tại mỗi dòng, ta gọi measure [Doanh thu] — lời gọi measure kích hoạt CALCULATE ngầm → context transition biến "khách hàng hiện tại" thành filter → [Doanh thu] trả về doanh thu của riêng khách đó. Cuối cùng AVERAGEX lấy trung bình. Nếu [Doanh thu] là biểu thức trần SUM(...) không phải measure, không có context transition và mọi dòng sẽ ra cùng một tổng — kết quả sai.
Lưu ý quan trọng: context transition xảy ra trước khi các filter khác trong CALCULATE được áp. Do đó filter viết trong CALCULATE có thể ghi đè filter sinh ra từ context transition.
Filter propagation qua quan hệ
Filter context không đứng yên tại bảng bị lọc — nó lan truyền qua quan hệ. Trong star schema, quan hệ mặc định là một chiều: từ dimension → fact. Khi filter context áp lên một dimension (ví dụ chọn Date[Year] = 2026), nó tự động chảy xuống lọc bảng fact Sales qua khóa quan hệ. Đây là lý do bạn kéo Date[Year] vào slicer thì measure trên Sales tự thu hẹp.
Chiều ngược lại (fact → dimension) không tự lan trừ khi bật cross-filter hai chiều — điều nên hạn chế (xem lại bài mô hình dữ liệu). Ghi nhớ: filter đi từ phía "một" sang phía "nhiều", không ngược lại.
Các hàm nền tảng cần thuộc
RELATED— trong row context của bảng fact, lấy giá trị từ bảng dimension ở phía "một" (đi theo quan hệ, dạng lookup).RELATEDTABLEđi chiều ngược lại, trả về bảng các dòng liên quan ở phía "nhiều".FILTER(table, điều kiện)— trả về bảng con thoả điều kiện; là iterator, dùng khi điều kiện phức tạp hoặc phải so với một measure.VALUES(cột)— trả về danh sách giá trị phân biệt của cột trong filter context hiện tại (rất hay dùng làm bảng để iterate).DISTINCT(cột)— giốngVALUESnhưng không kèm dòng trống phát sinh do quan hệ; trong đa số trường hợp hằng ngày kết quả tương đương.HASONEVALUE(cột)— trả về TRUE nếu filter context hiện tại chỉ còn đúng một giá trị của cột. Dùng để kiểm tra "đang ở dòng chi tiết hay dòng tổng".SELECTEDVALUE(cột, [mặc định])— nếu cột chỉ còn một giá trị thì trả về giá trị đó, ngược lại trả về mặc định. Gọn hơn cặpIF(HASONEVALUE...).DIVIDE(tử, mẫu, [thay thế])— phép chia an toàn: nếu mẫu bằng 0 hoặc rỗng thì trả về giá trị thay thế (mặc định trống) thay vì lỗi. Luôn dùngDIVIDEthay cho/khi tính tỷ lệ.
Ví dụ đối chiếu: % of total
Bài toán "tỷ trọng đóng góp" gói gọn mọi thứ ở trên. Ý tưởng: tử số là doanh thu theo filter context hiện tại (mỗi dòng khác nhau); mẫu số là tổng bất kể filter dòng — tạo bằng cách xoá filter với ALL.
-- Tỷ trọng doanh thu từng chi nhánh trên tổng toàn quốc (minh hoạ)
Doanh thu = SUM ( Sales[ThanhTien] )
% đóng góp =
DIVIDE (
[Doanh thu],
CALCULATE ( [Doanh thu], REMOVEFILTERS ( Branch ) )
)
- Ở dòng "Hà Nội", tử số là doanh thu Hà Nội; mẫu số dùng
REMOVEFILTERS(Branch)để bỏ filter chi nhánh nên ra tổng tất cả chi nhánh. Kết quả là tỷ trọng của Hà Nội. - Cùng một measure ở dòng tổng cho ra 100%.
Nếu muốn mẫu số tôn trọng slicer (ví dụ người dùng đã lọc còn 5 chi nhánh, và bạn muốn % tính trên tổng của 5 chi nhánh đó chứ không phải toàn bộ), đổi REMOVEFILTERS thành ALLSELECTED:
-- % trên tổng của những chi nhánh người dùng đang chọn (minh hoạ)
% đóng góp (theo lựa chọn) =
DIVIDE (
[Doanh thu],
CALCULATE ( [Doanh thu], ALLSELECTED ( Branch ) )
)
Đây là minh hoạ trực tiếp cho câu "cùng công thức, kết quả khác nhau do context": chỉ đổi modifier trong CALCULATE, ý nghĩa mẫu số đã đổi hoàn toàn.
Use case thực tế
1. Tính % đóng góp CASA của chi nhánh. Dùng đúng mẫu % of total ở trên: tử là số dư CASA của chi nhánh, mẫu là CALCULATE([CASA], REMOVEFILTERS(Branch)). Đây là chỉ số lãnh đạo hay hỏi vì CASA phản ánh chi phí vốn.
2. So với tổng / so với trung bình. Muốn biết một chi nhánh cao hay thấp hơn mặt bằng chung: mẫu số dùng AVERAGEX(ALL(Branch), [Doanh thu]) để lấy trung bình toàn hệ thống rồi so sánh. Context transition trong AVERAGEX (do gọi measure) là thứ làm mẫu này chạy đúng.
3. Trung bình theo thực thể, không theo dòng. "Doanh thu bình quân mỗi khách" phải iterate qua VALUES(Customer[CustomerID]) và gọi measure — không thể lấy AVERAGE trực tiếp trên cột giao dịch (sẽ ra trung bình mỗi giao dịch, khác hẳn).
4. Chuẩn bị cho YTD. Mọi công thức time intelligence ở bài kế tiếp đều là CALCULATE cộng với một filter thời gian đặc biệt: YTD là CALCULATE([Doanh thu], DATESYTD(Date[Date])). Nếu bạn hiểu CALCULATE sửa filter context như thế nào, YTD/YoY/MTD chỉ còn là thay filter thời gian — không có gì mới về bản chất.
Ghi nhớ
- Row context = "dòng hiện tại"; có trong calculated column và iterator (
...X); không tự lọc bảng. - Filter context = tập filter đang áp (visual + slicer + quan hệ +
CALCULATE); quyết định measure thấy dữ liệu nào; measure thừa hưởng context của nơi được gọi. - Measure là mặc định; chỉ tạo calculated column khi cần cột để lọc/nhóm/làm khóa quan hệ.
CALCULATElà hàm duy nhất sửa filter context; boolean filter là đường tắt và thay thế filter cũ;KEEPFILTERSđể cộng dồn.- Context transition =
CALCULATE(kể cả ngầm khi gọi measure trong iterator) biến row context thành filter context. Đây là chìa khóa đểAVERAGEX + measurechạy đúng. - Filter lan từ dimension (một) → fact (nhiều), không ngược lại.
- % of total: mẫu số dùng
ALL/REMOVEFILTERS(tổng tuyệt đối) hoặcALLSELECTED(tổng theo lựa chọn). Luôn chia bằngDIVIDE.
Nguồn tham khảo về context transition và các CALCULATE modifier:
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.
Kimball dimensional modeling: bảng fact/dimension, star vs snowflake, grain, và Slowly Changing Dimension.
Khối OLAP, các thao tác drill-down/roll-up/slice & dice/pivot, OLAP vs OLTP và ROLAP/MOLAP/HOLAP.
Từ nguồn dữ liệu qua ETL/ELT vào Data Warehouse, Data Mart đến dashboard; staging, ODS, batch vs streaming.