第一章
DataModel的概念
通过各种关系连接起来的表群。其中的关键点在于关系和表群。
-
为什么要用表群
如果可以只适用一个表就不行么?这样的话就不用考虑各种表之间的关系,也更利于数据的计算和分析。
把所有的字段放在一个表,肯定是可以的,但是显而易见的是这样会增加表的体积。假设原来的主数据只有10行,交易数据有1000行,如果强行合并,肯定是需要把主数据的各个字段扩展到1000行。另外,如果是两个交易的表的情况下呢,比如销售类的数据和出库类的数据,这样强行整合为一个大表的话,肯定会有很多无用的数据重复多次。现今,内存虽然便宜,但是还没有便宜到可以随意浪费的地步。
所以不严谨的说数据仓库发展到现在,为了跟快的反馈查询结果,一直在重复的就是选择时间换空间还是选择空间换时间。 -
关系
当决定通过关系把所有的表链接起来的话,各个表之间的关系就会变得微妙起来。表的连接方式无非就是1:1或1:N或M:N。1:1的情况下表的联动是最简单的是对等的,如果是1:N或者M:N的情况下就需要考虑一个问题就是两个表之间是如何联动的,即如何追溯和回溯?如从一个表如何过滤另外一张表的数据,如果使用两个表的字段计算一个新的字段。这是最需要关注的一点。
单向关系和双向关系
单向和双向的关系,单向双向影响的是维度表之间的跨表的计算,所有的维度表是与事实表进行关联,那么维度表就通过事实表进行间接的关联,这个时候单向和双向在统计这种间接关联的维度表中的数据的时候,会发生差异。
下图中,参与计算的除了事实表中的数量,还有维度表中的产品(进行了count计算)。Calendar Year来源于Date维度表,Product来源于Product维度表,分别与Sales表进行1:N的链接。
对于1:N的关系,书中说到过滤总是从1到N的方向进行过滤,这里的过滤可以理解为切片,比如表中添加了Year这个维度,相当于这个表使用Year对数据进行了切片,那么N方向的事实表的数据,就会按照这个切片进行分类汇总。如图中的数量字段按照年进行汇总。
跨表过滤(cross filter)- 间接过滤,单双向的差别在这里会出现差异,如下图中Product Name的计数是不一样的。原因在于单向的情况下,过滤(切片)总是从1到N的,这种情况下,年的切片其实是没有办法传递到Product维度表中的,即年的切片是对Product中字段的计算是不起作用的,因此Product Name的计数是对整个Product表中的Priduct Name进行计数汇总,然后显示在每一年下边。因此看到的是年由原来的三个变成了七个,2517是Product为维度表的product name的计数总和,重复的显示在了每一个年份中。
现在去掉这个Product Name的计数,同时添加Product中一个属性到这个表中,结果就相当于Date维度表和Product维度表同时对Sales事实表进行过滤(切片)。这种情况下其实单双向没任何区别,因为不涉及到过滤的传递,而只是同时过滤。
Other Point
- In DAX, relationships are part of the model, and they are all LEFT OUTER JOINs.
对于这句话,有两个疑问,第一以什么为主表的左连?怎么确定这个主表呢?第二,如果两个以上的事实表的话,是如何处理的?比如库存和销售。 - DAX is a function language. In DAX, every expression is a function call. Function parameters can be other function calls.
对应于函数式编程?
Tips
DAX中没有loop / Do循环
SUM(表达式) 对行计算
SUMX(表,表达式) 按行循环
第二章
注释:单行 -- // 多行 /* ... */
计算列和指标
计算列:依附于某个表,作为表的一个列,因此于此表有着同样的行,也就是说计算列是按照每一行的数据进行计算并存储在内存中的,是占用了空间的,可以认为是表的一部分。当数据抽取的时候发生的,而非在查询的时候才计算出来,因此占用的是抽取时间而非查询时间。
当创建计算列的时候,这个列的维度或者说颗粒度已经确定了,这里的每行就是最细的颗粒度。
指标:首先是不是依附于某一个具体的表,可以认为是基于整个模型的,因此他是没有具体的行的概念,也不是提前计算出来的。而是当你使用的时候根据当前可视化区域的切片/维度进行临时计算出来的结果。
指标创建的时候,并没有制定维度或者说颗粒度,他的结果或者他的颗粒度是根据报表中显示的维度而确定的,从而基于此颗粒度进行最终结果的计算。
-- 这样的理解貌似有点问题。
首先指标的创建也是选中某一个表然后进行创建的,所以应该还是依附于某一个表的。在创建指标的时候,首选是要选择汇总的类型,比如选择SUM然后才能跳出来字段以供选择,否则是没有办法创建指标的。
即先定义指标的汇总方式,然后决定按照什么样的颗粒度进行汇总,而计算列的话是先定义颗粒度(或者说默认颗粒度就是所在表的行级别),然后再使用的时候再定义汇总类型。
计算列和指标的图标是完全不一样的。
也可以简单的从以下角度理解两者的区别,计算列就是相当于从数据源选择数据的时候,新增的一列。即select calculate* as B from ***。进入到PBI的之后,就完全可以看作是一个表的列,这个列与其他的列没有什么本质的区别。而指标,就相当于一个管道/漏斗,他只会把你给的东西汇总起来,当你把某个字段拉到报表中显示的时候,这个漏斗就根据这个字段把所有的数据分别汇总并输送到不同的目的地。
计算列就相当于一个括号,
当计算只涉及到加减的时候,这个括号其实是没有什么作用的;
当计算开始涉及到乘除的时候,这个括号的作用就开始显现出来,因为加减乘除会根据这个括号调整计算的优先级。
比如:毛利率
SUM(Margin/Sales)
SUM(Margin)/SUM(Sales)
简而言之:
计算列:无需定义汇总类型,颗粒度为行级别,表更新的时候直接创建,占用内存空间,不受用户过滤的影响,相当于固定计算出一列值并存储在表中;
指标:先定义汇总类型,颗粒度按照使用图表的颗粒度,使用的时候临时计算,不存储,受用户过滤器的影响,相当于一个动态的函数计算值,随选择变动;
应该跟tableau中的LOD比较一下。
如何选择,两个应该基本都可以实现相同的功能,因此由时间换空间还是空间换时间决定;官方推荐用指标。 :-)
变量
用Var定义,用Return返回计算的值。
变量全部都是本地变量,没有全局变量,即变量值在定义的那个地方(定义时候的那个公示栏中)被使用,不能跨指标调用。 -- 那就把本地变量变成指标,不就成为全局变量了么。
同时变量在被用到的时候才会被检验同时被计算,只需要计算一次可以在多个地方使用。
BLANK () = 0 -- Always returns TRUE
BLANK () = "" -- Always returns TRUE
BLANK () = 0 = FALSE
BLANK () + BLANK () = BLANK ()
BLANK () / BLANK () = BLANK ()
10 * BLANK () = BLANK ()
BLANK () / 3 = BLANK ()
4 / BLANK () = Infinity
0 / BLANK () = NaN
When using variables, errors must be checked at the time of variable definition rather than where we use them. DAX engine cannot use optimized paths in its code when errors happen.
SUM, AVERAGE, MIN, MAX... -- 一步操作,列行为,对某列进行操作,只接受值,不接受表达式;所以复杂操作必须引入计算列;
需要验证,逻辑上来讲汇总细化到操作层面也是行的行为,最后在进行汇总操作,只是这个行行为是直接由函数定死的,无法进行双重操作,因为必须传入的是值而非表达式。 Aggregators are just a syntax-sugared version of iterators。
SUMX, AVERAGEX, MINX, MAXX...--两步操作,行行为,之后按照汇总类型进行汇总,接受值和表达式;复杂计算可以直接使用多重表达式嵌套。
COUNTA, AVERAGEA, MINA, MAXA...these functions are useful only for columns containing Boolean values,Text columns are always considered 0。
the functions ISNUMBER, ISTEXT, and ISNONTEXT always return TRUE or FALSE depending on the data type of the column and on the empty condition of each cell.根据定义的列的类型,所以这几个函数没什么用。除非先用Value进行转换,然后再做判定。
Related返回一个值,RelatedTable返回多个值组成一个表。
第三章
如果定义了多个表变量,其数据分别来源于不同的表,那么这个时候使用的连接还是原先定义的链接?
两个表的字段相乘?
如果新建一个calculatedTable,那么会复制源表的所有内容,如果原表有计算列,那么复制过来的就变成了值列了,即看不到原来的计算公式。
所有带All的函数,无视所有的切片/过滤器
All可以得到输入字段或者表的最细维度的值清单;
Value可以得到输入列或者表的值清单,但是多了一个空值(当事实表中的维度值不存在于维度表的时候,才会多一个空值去存储所有不存在于维度表的值,如果都存在的话,不会多一个空值);
Distinct可以得到输入列或者表的值清单,不包含空值;
SELECTEDVALUE ('Product'[Brand], "Multiple brands" )
CONCATENATEX (VALUES ( 'Product'[Brand] ),'Product'[Brand],", ")
第四章
The filter context filters; the row context iterates.
This means that the row context does not filter, and the filter context does not iterate.
第四章最重要的两句话。
第五章
理解CALCULATE和CALCULATETABLE
CALCULATE中引用的过滤条件,可以直接使用相关联表中的字段,不必要使用related去获取相对应的关系,也就是说,他只是起了一个过滤的作用,并且这个过滤相当于报表层面的过滤,其中间的连接关系使用已经定义好的对应关系。同时可以理解calculate不参与行计算,因此不需要根据每一行的内容去获取相关维度表中的内容以进行判断,只是起了一个过滤器的作用。
Before evaluating the expression, CALCULATE computes the filter arguments and uses them to manipulate the filter context。
即先过滤在计算,相当于在后边条件下的表中进行前边的计算。
同时,calculate中的过滤器会覆盖其他地方的过滤器,并且有一种ALL的感觉在里边。
关键点是,calcualte只覆盖前一层的过滤,只追溯一层。
或者应该理解为,calculate中用到的过滤取的维度都会被覆盖。但不会影响到未在Calculate中使用到的维度的过滤器。
看完这些,然后就轮到KEEPFILTERS出场拯救世界了。
context transition
Sum Num Of Sales := SUMX ( Sales, COUNTROWS (Sales ) ),需要好好的理解下这个指标最终给出的结果是什么。
这里说的重点是如果Calcualte函数第二个参数如果是空的,那么会默认在计算所依存的那个表中添加一个默认的过滤器,然后再遍历每行进行计算。如下图中M_Qty与Qty的不同之处。如果依存表中不存在两行一模一样的数据的话,那么结果如上图,与平常的计算是一致的,因为过滤器每次过滤都是一行数据。
Sales Amount := SUMX( Sales, CALCULATE( SUM( Sales[Quantity] ) ) )
简单理解的话可以理解为loop中套select;
loop sales condition X
select prod sum(qty)
from sales
where Condition X
endloop
在什么地方做什么事情
开始一层一层分析,假设这里的Sales表就是上图中5行的那个Prod,Qty表;
- SUMX的逻辑是遍历第一个参数的每一行,总共遍历5次;在什么地方,
遍历Sales的每一行,首先取第一行;Prod=A,Qty=10;
- 第二个参数开始做计算;做什么事情
Calculate(SUM(Quantity))
2.0 Calculate的计算顺序是反过来的,从后向前,所以第一步是过滤数据,这里没有传入参数,那么就相当于先把外层的表复制过来,然后每一列都添加上过滤器,也可以理解为把所有列Combine起来作为一个过滤字段。
2.1 开始取第一个合并的key,然后执行过滤,过滤条件就是外侧的那个Prod=A,Qty=5,这里可以过滤出来两行;在什么地方;
2.2 回到第一个参数进行计算,这里就是SUM计算了,第一步获得了两行Prod=A,Qty=10,汇总起来就是20,即Prod=A,Qty=20;做什么事情。
结束,跳出
执行第二行的,同样得到的也是Prod=A,Qty=20;
这两行的结果汇总起来就变成了Prod=A,Qty=40。
相当于下边这样
总结
- Context transition is expensive.
这里理解的expense不是loop套select的那种每次开关数据库连接的expense,而是两个循环互相嵌套的问题,即loop中套loop的那种内存开销,相当于要做两个表的行数相乘次的操作,所以会比较耗费内存和时间。 - Context transition does not only filter one row.
- Context transition is invoked whenever there is a row context.
意思就是loop中套的是一个select,这个select的结果可能是一条记录,但绝对不会一直是一条记录; - Context transition uses columns that are not present in the formula.
如果默认过滤参数,那么所有的列合并起来会作为一个过滤的条件; - Context transition creates a filter context out of a row context.
- Context transition transforms all the row contexts.
- Context transition invalidates the row contexts.
这里说的意思应该是select是from那个表,而不是loop传进来的那一条记录;
是不是可以说:
calculate(var) 等于 measure, measure是天然带着calculate的var,var是天然去掉calculate的measure,带不带calculate意味着是否进行语境传递?
计算列的话就是就是一个语境,但是是否使用这个语境要看使用什么样的汇总方式,是两列相乘,还是sum(),或是sumx();
第六章
Var...Return;
变量一经计算,结果便不能够在更改,哪怕对其重新计算重新调整过滤范围;这句话说的有点问题,变量什么时候计算呢?变量是包含在计算指标中的,那么计算指标在什么时候用呢?用的时候是什么样的context呢?如果计算指标被用在两个不同context的地方,那么这个变量的值是一样的么?
变量是定义的时候做校验;
表变量代表的是原表的一个子集,所以使用字段的时候还是需要引用原表的字段,而非引用表变量的字段。
第七章
DAX逻辑基础
row context / filter context / context transaction
报表层面的汇总,同一context下的内容直接计算就结束了,不同context下的内容的交叉计算是重点所在。
循环套循环一般会存在性能问题,所以开发过程中一般不使用循环套循环。
PBI中如果出现循环套循环的情况,只有里边的循环会被优化,外边的循环无法被优化。同时会把内循环的结果作为一个临时表存在内存中。因此会降低性能并占用过高的内存。
Rankx
RANKX ( <Table>, <Expression> [, <Value>] [, <Order>] [, <Ties>] )
- 第一个参数和第二个参数构成一个lookup表,然后使用第三个参数在lookup表中寻找,最后返回查找到的顺序(index?)。如果第三个参数省略,那么就使用第二个参数作为lookup的输入参数。比如SaleQty按照品牌排序,第一个参数就是所有的或者所有选择的品牌表,然后在这个表中添加SalesQty进行累计汇总,以便构建lookup表,之后使用context的SalesQty在lookup表中寻找,最后返回排位。
Rankx(All(Product[Brand]),SaleQty,SalesQty)简化为Rankx(All(Product[Brand]),SaleQty) - 如果排位不是按照汇总的SalesQty进行累计,比如销售总体分为5档,然后计算每个品牌的销售分别属于五档中的哪一档。这个时候第一个参数使用的不再是按照品牌进行累计的汇总表,而是使用的分档的销售额表,第二个参数就是这个分档的销售额表中的销售额,第三个参数即lookup的参数就是SalesQty,即需要排序的参数。
Rankx(档位,档位[销售额],SaleQty)
最后一个参数的意思是,如果两个品牌的销售额一样,那么怎么排序呢?这个时候这两个品牌的排位顺序肯定是一致的,比如都是第5位。最后一个参数的区别就在一这个第5位之后应该是第6位还是第7位。最后一个参数是Skip或者Dense,如果选择Skip的话,那么下一位就是第7位,如果使用Dense的话,下一位就是第6位。
即:
第八章
DAX Date table template https://github.com/sql-bi/DaxDateTemplate
Part 1
Sales Amount YTD =
VAR LastVisibleDate = MAX ( 'Date'[Date] )
VAR CurrentYear = YEAR ( LastVisibleDate )
VAR SetOfDateesYtd =
FILTER (
ALL ( 'Date' ),
AND (
'Date'[Date] <= LastVisibleDate,
YEAR ( 'Date'[Date] ) = CurrentYear
)
)
VAR Result =
CALCULATE (
SUMX ( Sales, Sales[Net Price] * Sales[Quantity] ),
SetOfDateesYtd
)
RETURN
Result
Part 2
Sales Amount YTD2 =
VAR LastVisibleDate = MAX ( 'Date'[Date] )
VAR CurrentYear = YEAR ( LastVisibleDate )
VAR SetOfDateesYTD =
FILTER (
ALL ( 'Date'[Date]),
AND (
'Date'[Date] <= LastVisibleDate,
YEAR ( 'Date'[Date] ) = CurrentYear
)
)
VAR Result =
CALCULATE (
SUMX ( Sales, Sales[Net Price] * Sales[Quantity] ),
SetOfDateesYTD
)
RETURN
Result
唯一的差别在于All('Date')和All('Date'[Date]),当使用的key是datekey连接的时候确实结果不一样。
原因在于All(Date[Date])只是去掉了Date这个字段的过滤,并没有去掉Year和Month上边的过滤器,而All(Date)是去掉了Date这个表上的所有过滤器;也就是说对于4月份那一行来说,默认的过滤器为年=2017,月=4,All(Date[Date])是没有办法去掉这个过滤器的,也就导致了Sales Amount YTD2计算的仍然是4月份的数据,而Sales Amount YTD是把Date上的所有过滤去给去掉之后计算的,因此能够得到YTD的数据。
count1 = CALCULATE(COUNT('Date'[Date]),ALL('Date'[Date]))
count2 = CALCULATE(COUNT('Date'[Date]),ALL('Date'))
count3 = CALCULATE(COUNT('Date'[Date]))
按照逻辑来讲,如果这个时候把Date表变成日期表,那么上边这个count的结果应该一样的,但事实并非如此,等等在研究吧。
第九章 Calculation Group
不好用,等待集成到PBI中再说吧。
第十章 Working with filter context
PRODUCTX ( Years, 1 + Inflation )
这个挺有意思的。
VALUES vs FILTERS
对于只有一个filter的情况下,两个返回的值是一样的,但是当有多个filter存在,并且是对于其他字段进行过滤的条件下,VALUES返回的是这多个过滤条件联合作用下,当前字段还拥有的值。而FILTERS返回的是对于当前字段所添加的过滤选择。或者可以说VALUES是对事实的统计,而FILTERS是对动作的统计.
比如字段A和B,现在对A进行过滤,但是对B进行Values和Filters检验,这种情况下,Values是返回在对A进行过滤之后B还剩下的值,而Filters只是检验B上边是否添加了filter,所以这个时候不管A做了任何的过滤,Filters显示的是B所有的值。
HASONEVALUE VS HASONEFILTER同样的道理.
ALLEXPECT VS ALL/VALUE
从这里开始会比较容易的发现,其实PBI的复杂之处都是在一些细枝末节,大的方向其实学习起来会很简单,但是对于细枝末节的东西或者说隐藏于其中的东西有太多了.如果稍微不注意,真的是很容易导致结果的错误,所以相对的后期的学习曲线还是比较陡的,对于IT可能需要很长时间的学习和分辨,但是对于用户,还真是一个比较难得问题.
第十一章 Handling Hierarchy
ISINSCOPE (),返回值为Ture或者False,用来判断给定的列是否是层次结构中的当前层。
该章节其他内容主要讲的就是如何从一个平面表重构出一个层次结构,主要逻辑就是利用父节点和子节点编号的对应关系,从而构建出具体层级节点的名称。但是有一点无奈的就是需要指定最深的层数,也就是说需要提前计算好层数,然后根据层数添加对应的层级列到主数据表中。
剩下的主要是在讨论,当层次结构不满的时候,下层的信息要如何正确的隐藏掉。
主要用到的函数有,
PATH(<ID_columnName>, <parent_columnName>) ,用户构建从根节点到叶节点的全路径;
LOOKUPVALUE(<result_columnName>, <search_columnName>,<search_value>),根据指定的值获取对应列的相对应内容,相当于VLOOPUP;
PATHITEM(<path>, <position>[, <type>]) ,获取指定层数的节点名称;
PATHLENGTH(<path>) ,获取层级的长度,用于计算某层级是否是满层级,从而隐藏掉多余的重复信息;
其中比较绕的一点就是如果不只是叶节点带数据,层次节点也带数据的时候,需要注意不能把这些数据给丢掉。判断的逻辑是用最大节点+1的节点是否有数据,同时该节点是否是最终叶节点来最终获得这个层次节点的数据。
第十二章 Working with Tables
Filter和Calculate的区别
FILTER(<table>,<filter>)
CALCULATE(<expression>[, <filter1> [, <filter2> [, …]]])
CALCULATETABLE(<expression>[, <filter1> [, <filter2> [, …]]])
Filter的逻辑是返回指定表的子集,如果表部分的参数是一个表达式的话,那肯定是先计算出这个表达式的结果,之后在这个结果进行过滤以获取部分数据。
Calculate以及Calculate Table的逻辑是在后边的过滤逻辑之上,Evaluate前边的表达式以计算对应的结果。
不从是否传递Context Filter的层面,但从计算逻辑层面来讲,两者的计算逻辑就是不一致的,所以结果大部分情况下应该也是不一致的。
这个逻辑理解起来是没有问题,但是比较有意思地方在于如果就是想用Filter的情况下,如何得到与Calculate Table一样的结果呢。
书中提供的解决方式是引用Calculate,下边的公式可以直接返回一个表,这个表中包含了所有的颜色,以及对应的count数。
ADDCOLUMNS (VALUES ( 'Product'[Color] ),"Num of Products", CALCULATE (COUNTROWS ('Product' ) ))
但是对于这个公式,就出现了一个新问题,有没有其他办法获取颜色列以及对应的行数呢?
https://www.sqlbi.com/articles/best-practices-using-summarize-and-addcolumns/
这个链接中给出了一定的比较和解释,其中关键的一点应该是两个表没有关联关系。
但是对于下边的公式,是否还有其他解决方案?目前水平不够,还没想出其他方法。
ADDCOLUMNS (VALUES ( 'Product'[Color] ),"Num of Products",COUNTROWS ('Product' ))
这里的Running Total还有点想不通。。。
VAR TotalSales = [Sales Amount]
VAR ProdsWithSales =
ADDCOLUMNS (
VALUES ( 'Product'[Product Name] ),
"ProductSales", [Sales Amount]
)
VAR ProdsWithRT =
ADDCOLUMNS (
ProdsWithSales,
"RunningTotal",
VAR SalesOfCurrentProduct = [ProductSales]
RETURN
SUMX (
FILTER (
ProdsWithSales,
[ProductSales] >= SalesOfCurrentProduct
--关键之处,如过换成<=的话就是从小到大的汇总,但如果是空白的话=>的情况下会有值,<=的情况下为空,
--因为小于等于空白的还是空白,而大于等于空白的就相当于所有的汇总
--高明。。。
),
[ProductSales]
)
)
VAR Top15Percent =
FILTER (
ProdsWithRT,
[RunningTotal] / TotalSales <= 0.15
--这里的TotalSales是总的Sales,不是Measure自动引入Calcualte么?为什么会变成总sales?
--同样是[Sales Amount],只不过一个是定义在外边的,一个是在里边直接引用的
--难道是因为定义的变量是在最外边的,所以的得到的就是汇总的?而变量内部引用的虽然也是[Sales Amount],但是会传递context?
)
RETURN Top15Percent
FIND is case-sensitive, while SEARCH is not.x