理解DAX有三个基本概念:语法(syntax)、函数(function)和上下文(context)。学习DAX函数和表达式的语法容易,知道内置函数(built-in function)的分类是基础,难点在于因地制宜地理解计算对应的数据表和问题环境,即上下文(context)。
- 语法syntax:是系统预设的书写规则,只有符合语法规范的计算才会被送往计算引擎执行;
- 函数function:是预先内置的公式、是常用计算的“封装”,每个函数都有最佳的使用场景;
- 上下文context:是函数有效性的背景,所有计算都相对于特定数据表而有效,相对于特定问题而有业务意义。
本文的重点是介绍DAX的两类context,即row context和filter context,它们背后反映的是不同的计算阶段、差异化的计算目的。因此,喜乐君先介绍适用于任何分析工具的“分析过程”(process of analysis),并据此区分两类计算阶段。
一、分析的聚合过程和两个阶段的计算
广义的角度看,数据库和计算机(computer)中所有的操作都是计算(calculation,computation)。在数据分析中,“计算”相对于数据表和问题而有意义。
1、分析的本质过程是聚合
数据分析的过程,是从数据表查询数据并根据问题处理、转换、归纳展现的过程,分析的重点是对数据的抽象归纳。
比如,用“100万”描述公司昨日的“销售额总和”,它代表了公司的营业规模,而不关心背后是多少笔订单、多少位客户。再比如,用“25.32%”描述公司开业以来的“合计利润率”,代表了公司每增加1元销售带来的边际利润贡献。这个数据值,是对数据库中记录的几百万数据行的抽象概括。
在数据分析中,最基本的数据抽象方式是聚合函数及其计算,比如SUM、AVG、MAX,及其比值计算。对应常见的指标:销售额总和、利润率(SUM利润/SUM销售额)。
从技术的角度看,分析的过程是从“数据明细表”(detail table)到“问题聚合表”(aggregate table)的转化过程。这个过程必然伴随分组、聚合,可能夹杂着筛选、排序、可视化,它们都是对数据的抽象方式(abstraction)。

举例而言,假设数据明细表是“每位作者的图书信息”,而聚合后的问题是“每位作者的图书数量”。分析过程是从左侧“数据明细表”,到右侧“分组聚合表”的过程。对应Excel的明细表和透视表,SQL的底表(from)和分组聚合表(select *from … group by…),也可以对应Tableau/Power BI的数据源和可视化图形——可视化只是聚合表的图表形式。

假设数据库的数据表为books,那么“各作者的图书数”可以借助于如下的查询实现:
SELECT author, COUNT( bookname ) -- 分组和聚合
FROM books -- 查询明细表
GROUP BY author; -- 分组依据
在大数据面前,聚合是最基本、最重要的抽象方式,只有包含聚合的查询才能称之为分析(analysis),否则仅能称之为查询(query)。因此,我们可以把“聚合”(aggregation)视为分析的本质过程。
“你叫什么名字”只是查询,“你去过多少地方”才叫分析,后者实现了数据的抽象转换。
Data aggregation is a process where data is collected and expressed briefly in a summarised format.
Aggregation is the core process of analysis , which is high level abstraction of data to answer specified questions.
这里推荐使用如下的可视化方式理解这个过程。可以有多种表达方式:
- 分析即抽象,抽象主要以聚合实现;不同问题的抽象程度不同,即“聚合度”不同。
- 分析是从“数据明细表”到“问题聚合表”的、由多变少的聚合过程。

2、从分析过程看计算的阶段、环境
基于上述的数据分析过程,对应聚合的起点和终点,分析计算可以分为两类:
- 数据明细表中的计算:在分组聚合之前完成的所有准备性计算
- 生成问题聚合表的计算:伴随聚合过程,以及聚合之后而来的计算
比如,为了回答“各日期、各产品分类的销售额总和”,我们需要准备一个包含日期、产品分类、销售额等多个字段的数据明细表。比如事先准备的明细表“每天、每个产品、产品分类的销售明细表”。但是,业务分析中通常拿到的数据表要么是分离的、要么是缺少字段的——实际上,这才是数据分析的常态。

如下所示,Sales数据表和Product数据表分开显示,有几个地方需要实现准备:
- 销售表中没有产品分类(category),要么从产品字段中拆分计算而来,要么从其他数据表合并而来——这里显然是后者;
- 没有“销售额”,需要从销售数量quantity、销售单价Unit price字段计算而来。
所有的操作都是计算,不管是表与表之间的“表操作”(table manipulation),还是字段与字段之间的“字段操作”(fields manipulation)。在这里,为了完成“各日期、各产品分类的销售额总和”,分析师必须在聚合开始前,完成如下的两类计算:
- 表合并:把product数据表的产品类别(category)合并到销售表中——至于是物理的合并,还是逻辑上的合并,暂且忽略。在Excel中,跨表合并用Vlookup完成,对应SQL中的join子句,以及DAX中的RELATED函数。
- 行级别字段计算:在数据表的每一行中计算“销售数量”乘以“销售单价”,从而为聚合各个类别的“销售额总和”做准备——至于这里是物理上的实现计算,还是逻辑上的名称,暂且忽略。在Excel中可以两列直接相乘(可能是E2:100 * F2:100),在SQL中也是如此( ‘Quantity’*’Unit Price’),在DAX则需要指明字段所在的数据表。
分组聚合前的数据准备,都是相对于数据表的明细行(row)而言的,因此可以说明细行的级别(row level)就是此类准备性计算的环境(context),DAX中称之为Row Context。
在完成上述的字段准备之后,就可以根据问题需要执行SUM求和聚合了——注意,所有的聚合都需要分组依据(group),部分问题还选项性的需要过滤条件(filter conditions)。
由于 DAX不仅仅是Power BI中的函数语言,更是可以独立于Power BI的查询语言,因此可以使用如下的DAX表达式,完成“各日期、各产品分类的销售额总和”。
Evaluate
SUMMARIZECOLUMNS (
'Sales'[Order Date],
'Product'[Category], -- 从产品表中合并字段,表关系在模型中实现预设
"total Sales",SUMX(Sales, Sales[Quantity] * Sales[Unit Price]) -- 每一行中两个字段相乘,最后根据问题聚合
)
为了让上述计算更加清晰,还可以预先DEFINE定义包含行级别计算的聚合,如下所示:
DEFINE
MEASURE Sales[Sales Amount] = SUMX ( Sales, Sales[Quantity] * Sales[Unit Price] ) -- 两个字段相乘,从而计算每一行的销售金额
Evaluate
SUMMARIZECOLUMNS (
'Sales'[Order Date],
'Product'[Category], -- 相当于vlookup从产品表中合并字段
"total Sales",Sales[Sales Amount]
)
如果我们把问题略作调整,可以增加筛选条件的部分,如“东北地区中,各日期、各产品分类的销售额总和”,聚合是分析的本质,如同指标是业务的向导,问题中的所有其他部分都围绕聚合而起作用,都影响聚合值的多少、大小,都是聚合的背景,DAX中称之为FILTER Context(这个词具有误导性,后续补充)。
当然,不熟悉DAX的读者也可以先用使用SQL来完成,对应的查询如下所示:
SELECT a.'Order Date', b.Category,
SUM(a.Quantity * a.'Unit Price ) as Total_sales
FROM Sales a
left join Product b
on a.Productkey=b.Productkey
在上述查询中,from及JOIN是在SELECT之前完成的,而SUM聚合的对象(a.Quantity * a.’Unit Price’)也可以视为是聚合前的准备,只是它只存在于一瞬,不像数据表中的字段那么实在、具体。它们都可以统称之为“分析前的数据准备”,在数据表明细行上计算。
相对应的,SUM聚合,以及影响聚合的分组条件(group by),过滤条件(where,或者having)都是分析过程,它们随着问题变化而变化,依赖于问题、相对于聚合而有价值。其中分组决定聚合值返回的数量、过滤条件决定聚合值返回的大小,这一点正是Filter context中需要区分的部分,接下来详细阐述。
为了避免和filter context中的filter冲突,这里用过滤(restrict)来代替筛选过程。
3、聚合计算的环境:分组和过滤筛选
“数据准备”(data prepare)有两个基本类型:数据表合并、行级别字段计算,从而弥补问题中需要用到的字段的不足。这之后,问题的聚合就要开始了。
在问题中增加一个筛选条件,“2022年,各产品分类的销售额总和”,分析的关键是用聚合回答问题答案——指标就是聚合度量的业务表达。聚合的关键是分组依据和数据范围,比如问题中的“产品分类”和“2022年”。分组依据决定返回多个少聚合值,而数据范围则决定返回的聚合值的数字大小,它们对聚合值的作用略有不同。
聚合是分析的本质,指标是业务的向导
分析中,可以把影响聚合值的所有要素,统称之为聚合计算的环境(context)。推荐从问题的标准结构中理解,如下图所示,问题必然由分析范围、分析对象和问题答案构成,问题的答案必然是聚合,聚合值的多少、大小受分析对象和分析范围的影响,故说它们是聚合的环境(context)。

可见,聚合的环境可以分为两大类:
- 聚合的分组依据:即最终聚合表中返回多少个聚合值的依据,称之为问题的维度
- 聚合的筛选条件:条件可以是明细上的条件(比如2022年),也可以说聚合后的条件(比如计算“产品类别的销售额总和”之后,再筛选“销售额总和大于10万”的产品类别),二者计算类型不同,但都是对最终问题表的过滤
也就是说,聚合之前的所有计算,都是相对于数据表明细行而言的,明细行(row)是此类计算的环境(context);而影响聚合值多少、大小的分组依据、过滤条件则是相对于问题、最终聚合表而言的,问题就是此类计算的环境(context)。
总结一下,
- 分析是从数据明细表到问题聚合表的过程;分析中的任何操作都可以称之为计算。
- 以聚合为分界,计算可以分为聚合前的数据准备计算,和之后的分析计算。
- 数据准备计算的主要类型是数据表合并(join)和行级别字段计算,相对于行row而有意义;
- 影响聚合的要素可以分为分组依据、过滤条件两大类,它们构成了聚合的context。
接下来,我们可以把上述的逻辑体系,推广到DAX之中,就构成了D AX的两类context(习惯翻译为“上下文”)。
二、DAX中两类计算及其Context
借助于“各日期、各产品分类的销售额总和”案例,前面总结了分析的过程、计算分类,适用于任何一种分析工具,不管是Excel、SQL,还是Tableau、Power BI。不过,这也是最简单的问题实例——数据准备过程中没有聚合,问题答案中没有筛选。
分析中,要明确的区分问题前的数据准备计算,和与问题结合的回答问题计算,前者是相对于数据表而言的,后者是相对于问题而言的,数据表和问题就是计算的背景、环境(context)。
可以用如下的图示来表述两个阶段,及其中的计算。左侧阴影区是聚合之前的准备,包括数据表准备和计算准备,它们都是相对于数据表明细行的;后者是相对于聚合的。

1、使用简单案例理解两种Context
前面讲解的简单案例“各产品类别的销售额总和”,算是最为简洁的一个模型。其中的行级别计算(Quantity * Unit Price)和数据表关系合并在左侧,它们可以通往非常多、截然不同的问题,因此说它们仅仅是相对于数据表而言的,而和具体的问题无关。
而该案例中的SUM计算,则必然要相对于问题而言,问题中的维度是聚合的分组依据、问题中的过滤条件影响聚合值的大小,它们就是聚合计算的环境(context)。
在DAX中,我们把准备计算所依赖的数据表明细行,称之为Row context,对应的计算是在数据表的每一行中完成,且有业务意义;而把聚合计算所依赖的问题环境,称之为filter context,对应的计算需要在问题中寻找依据,包括筛选器、切片、分组依据。
- Row context: 对应的是数据表中每一行(every row),即计算在每一行上完成,而且都有意义。
- Filter context:对应的是问题中影响聚合值大小的所有要素,包括filters筛选器、slice切片器、dimension视图维度。Power BI中的筛选器、切片器都可以理解为筛选条件,它们影响聚合值的大小;而视图维度主要影响聚合值的数量。
第一部分,本文从计算的阶段理解计算分类(以聚合为分界),这里以计算所依赖的环境理解计算,角度不同,在不考虑嵌套组合前提下,二者可以一一对应。
但要注意,和context对应的是不同阶段的计算,而非不同类型的计算。
2、区分两类context和两类计算类型
特别注意的是,计算所依赖的环境(context),和计算的类型无关。换句话说,行级别计算并非总是在左边,聚合计算也并非总是在右边。最常见的情况是下图中的第1和第3象限,已经在第一部分介绍;其次是第4象限和第2象限,这里分别距离。

第四象限的典型案例是影响聚合值大小的行级别筛选条件。
比如,问题中可能出现两类过滤条件,其一是“东北地区”,其二是“销售额总和大于10万”,前者对应的计算是行级别的(region= “东北”),后者对应的计算是相对于问题的聚合(sum(sales)>10^5)。从计算类型角度看,二者分别属于非聚合计算(行级别计算)和聚合计算,但是都属于影响聚合值的FILTER CONTEXT。
第二象限的典型案例,是在数据表中预先构建的聚合。
比如,为了完成一些高级分析,比如“不同购买频率的客户数量”,我们需要引入事先聚合好的数据表,或者使用预先聚合,在明细行中增加“每个客户的订单计数”,此时聚合发生在问题的聚合之前,属于问题分析的准备,计算依然是在行级别有效的(row context)。这个问题,对应Tableau的LOD表达式,或者DAX中使用聚合的计算列。
Tableau_Frequence_LOD = {fixed [customerID]: COUNTD(orderID)}
DAX_Frequence_calculated_column=
Calculate (DISTINCTCOUNT(sales.[orderID]),
Values([customerID])
)
此类明细表中预先聚合,比聚合过程中引用行级别筛选条件,往往更难以理解。
在实践中,计算的复杂性通常来自于函数及context的组合。不考虑函数嵌套的情况下,两类context对应两类计算,就有四种组合(小节开始的四个象限);如果考虑到嵌套(比如聚合计算中嵌套行级别计算),则会更多。
3、理解CONTEXT的禁忌:避免用单元格的角度审视计算
大数据分析的基本对象是数据表,数据表是元组的集合,计算的对象是字段列,筛选的常见对象是明细行。不管从哪个角度看,都避免用单元格思维来审视计算。
目前主流的看待filter context的方式,是站在聚合表中单个数据值的立场来看影响它的所有条件,统称之为filter context;这种方式下,一个问题的聚合表中,每个值对应的filter context都会不同。这种理解进一步增加了复杂性。
比如,在 the definitive guide to DAX一书中,作者举例如下,在“各个brand的total quantity”视图中,Contoso对应的3000数据值,包含三个filter contexts,分别来自于筛选器、交互筛选、brand字段值。Contoso和Litware对应的filter contexts当然是不同的。

而我认为,最好的方式是从字段的角度去看,不管是明细表中的物理字段,还是问题聚合表中的逻辑字段。
对于上面的题目而言,聚合的quantity对应的filters context还是有三个,其中两个的作用是筛选(C Y2007的交互筛选,和High school & Partial college的筛选器),另一个的作用是分组(brand)。
我们要站在整个聚合表的视图看待filter contexts,同时把筛选和分组分开——它们对聚合值的影响不同。
至此,本文介绍了分析的本质过程(聚合),并站在聚合角度审视左右——左侧是为了分析而预先完成的准备计算,右侧是为了聚合而来的分组和过滤条件;左侧的计算依赖于数据表明细行,右侧的计算依赖于问题。在DAX中,左侧的计算环境称之为row context,右侧的计算环境称之为filter context。
不过,本文中建议详细区分filter context中的两类条件:分组依据和过滤条件。这是理解分析中的层次概念,通往高级分析、结构性分析的关键。
参考文献:
- Microsoft:DAX function reference
- SQLBI:Row Context and Filter Context in DAX
喜乐君
Feb 26, 2023
Feb 28, 2023 修改第一部分