小思考
最近一直在思考,对于Power Bi的使用来说,如果日常项目中用不到的函数,如何更好的去理解。
简单地说,如果日常项目用加法就可以完成,那么可能不会发现乘法可以更好地优化计算。
对于这个问题,我觉得可能更好的方式有几点:
- 自己给自己出题。
- 没事就看看别人的代码。
- 不停的总结整理,从更方便自己理解的角度去做整理。
正题
最近想要好好理解一下Filter函数,在此过程中,看到一个别人写的代码。觉得很有意思,虽然代码本身没什么难度,但是代码里有一个我觉得我如果不遇到特定的问题,是无法想到的计算方式。
所以这里整理出来。
计算的目的 & 结果
计算当月产生购买的顾客中,有多少是新客户(第一笔购买发生在当月)
Raw data 如下,总行数大概59万行:

用了两种计算方式 (加入了注释,方便理解,嗯,说的就是你,写给你看的。)
CC_A =
VAR AllCustomer = VALUES( ZSMnet[CustomerKey] )
VAR OldCustomer =
FILTER(
AllCustomer,
// 注意Filter是迭代函数,这一点非常重要。意味着下面的表达式均需要对‘AllCustomer’表进行逐行计算。
// 先算出每个Customer的全局第一次订单日期,再与外部视觉对象中的年月中的第一次订单日期进行比较。
CALCULATE(
MIN( ZSMnet[BillingDate] ),
ALLEXCEPT( ZSMnet, ZSMnet[CustomerKey] )
// ALLEXCEPT ( <表名>, <列名>, [ <列名>, … ] ) : ALLEXCEPT 从第一个参数指定的表中删除筛选器,只保留后续参数指定的列中的直接筛选器。
)
< MIN( ZSMnet[BillingDate] )
// 这里可能有人会觉得如果当前行客户当前月没有下单岂不是BillingDate为空么,需要注意AllCustomer其实是当前上下文中的YearMonth下的已下单客户。
)
RETURN
COUNTROWS( EXCEPT( AllCustomer, OldCustomer ) )
// EXCEPT ( <左表>, <右表> ) : 返回一个表,其中包含左表的行减去右表的所有行而得到的行。
CC_B =
COUNTROWS(
FILTER(
ADDCOLUMNS(
VALUES( ZSMnet[CustomerKey] ),
"DateOfFirstBuy",
CALCULATE(
MIN( ZSMnet[BillingDate] ),
ALLEXCEPT( ZSMnet, ZSMnet[CustomerKey] )
)
),
// 注意Filter是迭代函数,这一点非常重要。意味着下面的表达式均需要对上面虚拟表进行逐行计算。
CONTAINS(
// CONTAINS ( <表>, <列名>, <值>, [ <列名>, <值> ], [ … ] )
// 如果每个指定的 <值>可以在对应的<列名>列中找到,或包含在这些列中,则结果为 TRUE;否则函数返回 FALSE 。
CALCULATETABLE(
VALUES( ZSMnet[BillingDate] ),
ALL( ZSMnet[CustomerKey] )
),
ZSMnet[BillingDate],
[DateOfFirstBuy]
)
)
)
计算结果 & 性能分析器
计算结果没什么好说的,肯定都是相同的。

性能分析器,这个就有点意思了:可以看到CC_B的DAX查询性能基本上都是CC_A的2-3倍。

思考
首先是DAX代码,CC_A的写法,基本上是最常见的,下意识的写法。
简单说就是,客户全局首次下单日期,和当月首次下单日期,比大小,就知道当前月下单是否是全局第一次下单。
CC_B的写法,就有点意思了。
简单地说就是,List 当前月所有客户的Billing Date,然后看当前行的Customer的全局首次下单日期,是否在这个 List 中。如果在则表示当前行客户在当前行年月是首次下单,属于新客户。
对于代码,能说的其实就这些。
但是我对这段代码最大的感触倒不完全是代码作者用到了CONTAINS函数来解决这个问题,而是我觉得,Filter 函数的第二参数 ( 布尔表达式 ),这是一个非常重要,但是又常常被我忽略的知识。
FILTER ( <表>, <布尔表达式> )
我相信大部分用Power BI的人都知道Filter的第二参数是布尔表达式,同样,我也知道。
但是我的Point是,如果把DAX所有返回值为布尔值的函数整理出来,也许在解决一些需求的时候,可以拓展很多算法思路。
因为从我的感觉来说,Power BI其实就是 筛选 + 计算。
第一步是如何把你需要计算的 Line 筛选出来,第二步计算反而不是很困难的。
附表
| DAX函数 | 语法 | 返回值 |
|---|---|---|
| TRUE | TRUE() | 标量 一个布尔值,始终返回 TRUE |
| PATHCONTAINS | PATHCONTAINS ( <路径>, <项> ) | 标量 一个布尔值 |
| OR | OR ( <逻辑表达式 1>, <逻辑表达式 2> ) | 标量 一个布尔值(True 或 False) |
| ISTEXT | ISTEXT ( <值> ) | 标量 一个布尔值 |
| ISSUBTOTAL | ISSUBTOTAL ( <列名> ) | 标量 一个布尔值 |
| ISSELECTEDMEASURE | ISSELECTEDMEASURE ( <度量值 1>, [<度量值 2> … ] ) | 标量 一个布尔值 |
| ISONORAFTER | ISONORAFTER ( <值 1>, <值 2>, [<排序>], [<值 1>,<值 2>,<排序>] … ) | 标量 一个布尔值 |
| ISODD | ISODD ( <数字> ) | 标量 一个布尔值 |
| ISNUMBER | ISNUMBER ( <值> ) | 标量 一个布尔值 |
| ISNONTEXT | ISNONTEXT ( <值> ) | 标量 一个布尔值 |
| ISLOGICAL | ISLOGICAL ( <值> ) | 标量 一个布尔值 |
| ISINSCOPE | ISINSCOPE ( <列名> ) | 标量 一个布尔值 |
| ISFILTERED | ISFILTERED ( |
标量 一个布尔值 |
| ISEVEN | ISEVEN ( <数字> ) | 标量 一个布尔值 |
| ISERROR | ISERROR ( <值> ) | 标量 一个布尔值 |
| ISEMPTY | ISEMPTY ( <表> ) | 标量 一个布尔值 |
| ISCROSSFILTERED | ISCROSSFILTERED ( |
标量 一个布尔值 |
| ISBLANK | ISBLANK ( <值> ) | 标量 一个布尔值,如果值为空,则返回 TRUE;反之返回 FALSE |
| HASONEVALUE | HASONEVALUE ( <列名> ) | 标量 一个布尔值 |
| HASONEFILTER | HASONEFILTER ( <列名> ) | 标量 一个布尔值 |
| FALSE | FALSE() | 标量 一个布尔值,始终是 FALSE |
| EXACT | EXACT ( <文本 1>, <文本 2> ) | 标量 一个布尔值 |
| CONTAINSSTRINGEXACT | CONTAINSSTRINGEXACT ( <查找范围>, <要找的文本> ) | 标量 布尔值 |
| CONTAINSSTRING | CONTAINSSTRING ( <查找范围>, <要找的文本> ) | 标量 布尔值 |
| CONTAINSROW 和 IN | CONTAINSROW ( <表>, <值>, [ <值> … ] ) | 标量 布尔值,True 或 False |
| CONTAINS | CONTAINS ( <表>, <列名>, <值>, [ <列名>, <值> ], [ … ] ) | 标量 一个布尔值 |
| AND | AND ( <逻辑表达式 1>, <逻辑表达式 2> ) | 标量 一个布尔值(True 或 False) |