【DAX圣经】第一章:DAX是什么?
DAX是微软SQL Server分析服务(SSAS)和Microsoft Power Pivot for Excel的编程语言, 它是在2010年创建的,第一次发布了Excel 2010的PowerPivot (是的,在2010年PowerPivot拼写是没有空格的;这个空格在2013版介绍Power Pivot的名字时被引入)。 随着时间的推移,DAX在Excel社区中流行起来,它使用DAX在Excel中创建Power Pivot数据模型,以及在商业智能(BI)社区中使用DAX来构建带有SSAS的模型。
DAX是一种简单的语言。也就是说,DAX与大多数编程语言不同,它可能需要一些时间才能熟悉它。在我们的经验中,向成千上万的人教授了DAX,学习DAX的基本知识是较为简单的:你可以在几个小时内就开始使用它。但是,当涉及到理解高级概念,如评估上下文、迭代和上下文转换时,一切看起来都很复杂。不放弃! 要有耐心。一旦你的大脑开始消化这些概念,你就会发现DAX确实是一种简单的语言。只是需要时间来适应。
第一个章节主要以表格和关系对数据模型进行简单回顾。我们推荐所有经验水平的读者阅读这一节以了解我们在参考文献中使用的术语。
在下一节中,我们将向那些对其他编程语言有一定经验的读者提供建议,即Excel、SQL和MDX。每个部分都是为已经知道该语言的读者提供的,并且可能会发现阅读对DAX的快速介绍是很有用的,我们将它与各种语言进行比较。如果您是一个Excel用户,并且发现MDX部分几乎不可能理解,那是完全可以预料到的。跳过这一部分,因为它包含了对你来说毫无意义的信息,然后进入下一章,我们进入DAX语言的旅程真正开始了。
理解数据模型
DAX是一种专门用于计算数据模型的业务公式的语言。您可能已经知道什么是数据模型,但是如果您不熟悉它,那么就值得为数据模型和关系的描述提供一些页面,以便创建一个基础,您可以在此基础上构建DAX知识。
数据模型是一组表,由关系链接
我们都知道一个表是什么:一组包含数据的行,每一行都被分成几列。每一列都有一个数据类型,并且包含一个单独的信息。我们通常把表中的一行称为记录。表格是一种方便的组织数据的方法。就其本身而言,表已经是一个数据模型,尽管它是最简单的形式。因此,当您在Excel工作簿中写入姓名和数字时,您正在创建一个数据模型。
如果您的数据模型包含许多表,那么很有可能它们是通过关系连接起来的。两张表之间的关系。当两个表与一段关系联系在一起时,我们说它们是相关的。图形化的关系是由连接两个表的直线表示的。图1-1显示了一个数据模型的示例: 图1-1 这是一个由5个表组成的数据模型的简单示例关系的一些重要方面要好好学习:
- 在一段关系中两边的表没有相同的角色。他们被称为关系的一方和多方。在图1-1中,重点关注产品和产品子类之间的关系。单个子类别包含许多产品,而单个产品只有一个子类。因此,产品子类别是关系的一方(有一个子类别),而产品是多方(有许多产品)。
- 用来创建关系的列(通常在两个表中都有相同的名称)被称为关系的键。在关系的一方,此列需要在每一行有一个惟一的值。在多方上,相同的值可以(通常是)在许多不同的行中重复。当列对每一行有唯一的值时,它被称为表的键。通常,表格有一个作为键的列。
- 关系可以形成一个链条。每个产品都有一个子类别,每个子类别都有一个类别。因此,每个产品都有一个类别。为了检索产品的类别,您需要遍历两种关系的链。图1-1包含了一个由三个关系组成的链的例子,从销售开始,一直到产品类别。
- 在每一段关系中,都可以有一到两个小箭头。在图1-1中,您可以看到销售和产品之间的两个箭头,而所有其他的关系都有一个箭头。箭头指示自动过滤关系的方向。我们将在后面的章节中更详细地讨论这个问题,因为确定过滤器的正确方向是学习的最重要的技能之一。
- 在表格数据模型中,关系只能在单列上创建。多列关系不被引擎支持
理解一段关系的方向
正如我们在前一节中所说的,每一种关系都可以有一个或两个方向的过滤。过滤总是从关系的一方到多方。如果关系是双向的(也就是说,它有两个箭头),那么过滤也会从多方到单方。
一个例子可以帮助您更好地理解这种行为。如果您根据图1-1中所示的数据模型创建一个pivot表,那么在值区域中使用年数、销售额和ProductName数之和,您将看到如图1-2 所示的结果。 图1-2这个透视表显示了在操作中跨多个表进行过滤的效果行标签包含年份——也就是来自日期表的列。日期是与销售表的关系的一方,所以当你把销售金额的总和放在透视表上时,引擎会根据年份来过滤销售。销售和产品表之间的关系是双向的;当你在透视表中放入产品名称时,你就会得到每年卖出的产品数量。换一种说法,在日期上的过滤器使用一系列的关系传播到产品表。
如果您现在通过将颜色放在行上并在值区域中添加FullDateLabel的计数来修改透视表,那么结果就有点难以理解了,如图1-3所示。 图1-3 这个透视表显示,如果双向过滤不被激活,表就不会被过滤行的过滤器是产品表中的颜色列。因为产品是与销售关系的一方,销售金额的总和被正确的过滤了。产品名称的计数显然是经过过滤的,因为它是来自于行(Product)上的同一表的计算值。错误的数字是FullDateLabel的数量。事实上,它总是显示所有行的相同的值——顺便说一下,这个数字是日期表中行的总行数。
从颜色列中产生的过滤器不传播到日期的原因是,日期和销售之间的关系有一个箭头,从日期指向销售。因此,即使销售中有一个有效的过滤器,过滤器也不能传播到日期,因为这种关系的类型阻止了它。
如果您改变了日期和销售额之间的关系,以启用双向过滤,那么结果将是图1-4所示的结果: 图1-4 如果启用双向过滤,则使用颜色列过滤日期表正如你所看到的,这些数字现在是不同的,反映了至少一种特定颜色的产品被售出的天数。乍一看,似乎所有的关系都应该被定义为双向的,以便让过滤器在任何方向传播,并且总是返回有意义的结果。正如您将在本书中了解到的,这并不总是正确的设计数据模型的方法。事实上,根据您所使用的场景,您应该选择正确方向的关系传播。
DAX对于EXCEL用户
很有可能你已经知道了Excel公式语言,它有点像。毕竟,DAX的根基是用于Excel的Power Pivot,开发团队试图让这两种语言保持类似的功能。这使得向这种新语言的过渡变得更加容易。然而,两者之间有一些非常重要的区别。
单元格VS表格
在Excel中,您可以对单元执行计算。一个单元格使用它的坐标来引用。因此,你可以写出如下公式
= (A1 * 1.25) - B2
使用Excel,您可以使用@ColumnName格式来引用表格中的列,其中ColumnName是您想要使用的列的名称,而@符号的意思是“取当前行的值”。“虽然语法不是很直观,但通常你不会写这些表达式。它们只是简单地点击一个单元格,而Excel则负责为您插入正确的代码。
您可能认为Excel有两种不同的执行计算方法:您可以使用标准的单元引用(在这种情况下,单元F4的公式应该是E4*D4),或者您可以使用列引用,如果您在一个表中计算的话。使用列引用的优点是,您可以在列的所有单元格中使用相同的表达式,而Excel则用不同的公司计算每一行。
DAX在表格上工作,所以所有的公式都需要引用列。例如,在DAX中,你用这种方式写出之前的乘法
[AllSales] := SUM ( Sales[SalesAmount] )
使用一列来获取特定行的值或者将整个列作为一个整体,并没有语法上的区别。DAX认为您想要对列的所有值求和,因为您在聚合器中使用列名(在本例中是SUM函数),它要求将列名作为参数传递。因此,虽然Excel需要显式的语法来区分要检索的两种类型的数据,但DAX以一种自动的方式来消除歧义。至少在一开始,这可能会让人困惑。
Excel和DAX:两种函数式语言
这两种语言非常相似的一个方面是,Excel和DAX都是函数式语言。函数式语言是由基本的函数调用组成的。无论是在Excel中还是在DAX中,都没有语句、循环和跳跃的概念,这在许多编程语言中是很常见的。在DAX,一切都是一种表达。对于来自不同语言的程序员来说,这种语言的这一方面通常是一个挑战,但对于Excel用户来说,这一点也不奇怪。
使用迭代器
对您来说可能是新概念的一个概念是迭代器。在Excel工作时,您习惯于一次执行计算。在前面的例子中,您已经看到,为了计算总销售额,您已经创建了一个列,其中包含了价格乘以数量,然后,作为第二步,您将它总结为计算总销售额。这个数字将会很有用,例如,作为一个分母来计算每个产品的销售百分比
使用DAX,您可以通过使用迭代器在单个步骤中执行相同的操作。迭代器如它名字所述的:遍历一个表,并对表的每一行执行一个计算,聚合结果以产生所需的单个值。
在前面的例子中,你可以使用SUMX迭代器来计算所有销售的总和。
[AllSales] := SUMX ( Sales, Sales[ProductQuantity] * Sales[ProductPrice] )
在这种方法中,既有优点也有缺点。这样做的好处是,您可以作为一个步骤执行许多复杂的计算,而不必担心添加许多列,而这些列最终只对某些特定的公式有用。另一方面,缺点是,使用DAX编程的视觉效果不如Excel。事实上,你不会看到列计算价格乘以数量;它只存在于计算的生命周期中。
说实话,你仍然可以选择创建一个计算的列,计算价格和数量的乘积。然而,正如您稍后将学到的,这很少是一个好的实践,因为它使用了宝贵的内存,并且可能会减慢计算速度。
DAX需要一些理论
让我们明确一点:这不是编程语言之间的区别;这是思维模式的不同之处。就像这个星球上的任何一个人一样,你可能已经习惯了在web上搜索复杂的公式和解决方案模式,以解决你想要解决的问题。当使用Excel时,很有可能你会找到一个几乎满足你需要的公式。你可以复制这个公式,定制它来满足你的需要,然后使用它,不用太担心它是如何工作的。
例如,在我每天使用的一个工作表中,我有这个公式
{=SUM(IF(('Transactions'!$B$5:$B$991>=M30)* ('Transactions'!$B$5:$B$991<=N30),1,0))}
我不太明白花括号里的公式是怎么工作的以及如何计算IF语句。老实说,我只记得我需要用一个奇怪的键盘组合来确认它们。也就是说,它是有效的,它总是有效的,它计算的数字是感兴趣的,而不是它内部计算值的方式。因此,作为书籍的作者和DAX专家,我也属于这一类的用户
这种方法在Excel中工作,与DAX不兼容。您将需要学习一些理论,并在您能够编写良好的DAX代码之前彻底了解评估上下文是如何工作的。如果没有适当的理论基础,DAX将会计算像魔术这样的值,或者它会计算出一些毫无意义的奇怪数字。问题不是DAX,而是你还没有完全理解它是如何工作的
幸运的是,DAX的理论局限于几个重要的概念,在第四章“理解评估环境”中得到的解释。“当你读到那一章的时候,卷起你的袖子,准备好一段时间回到学校。”一旦你掌握了它的内容,DAX将不再有任何秘密,而学习它将主要是一个获得经验的问题。然而,除非这一理论得到了充分的理解,否则请不要试图更进一步。记住:理解是成功的一半。
DAX 对于SQL 开发人员
如果您已经习惯了SQL语言,那么您已经使用了许多表,并在列之间创建了连接,以便设置关系。从这个角度来看,在DAX的世界里,你会感到非常熟悉,因为在DAX的计算中,计算是查询一组由关系和聚合值组成的表。
理解关系处理
SQL和DAX之间的第一个区别在于模型中的关系工作方式。在SQL中,你可以在表格之间设置外键来声明关系,但是引擎在查询中从不使用这些外键,除非你对它们很明确。例如,如果你有一个客户表和一个销售表,其中CustomerKey是客户的主键和销售中的外键,那么你可以编写一个查询,如下所列:
SELECT
Customers.CustomerName,
SUM ( Sales.SalesAmount ) AS SumOfSales
FROM
Sales INNER JOIN Customers
ON Sales.CustomerKey = Customers.CustomerKey
GROUP BY
Customers.CustomerName
即使您使用外键声明了模型中的关系,您仍然需要显式的,并在查询中声明联接条件。尽管它使查询变得更加冗长,但这是很有用的,因为它允许您在不同的查询中使用不同的联接条件,从而使您在查询的表达性方面有很大的自由度。
在DAX中,关系是模型的一部分,它们都是外部连接。一旦在模型中定义了,您就不再需要在查询中指定联接类型:当您使用与主表相关的列时,DAX在查询中使用一个自动的左外连接。因此,您可以在DAX中编写以前的SQL查询:
EVALUATE
SUMMARIZE (
Sales,
Customers[CustomerName], "SumOfSales",
SUM ( Sales[SalesAmount] )
)
因为DAX知道销售和客户之间的现有关系,所以它会自动跟随模型。最后,SUMMARIZE函数需要通过客户Customers[CustomerName]来进行分组,但是您没有任何关键字:SUMMARIZE通过选定的列来自动分组数据。
DAX是一种函数式语言
SQL是一种声明性语言。您可以通过声明使用SELECT语句来检索所需的数据集来定义您所需要的东西,而不必担心引擎将如何实际检索信息。另一方面,DAX是一种函数式语言。
在DAX中,每个表达式都是一个函数调用,而函数参数可以依次是其他函数调用。对参数的计算可能会导致非常复杂的查询计划,而DAX则执行这些计划来计算结果。
例如,如果你只想检索居住在欧洲的客户,你可以用SQL来写:
SELECT
Customers.CustomerName,
SUM ( Sales.SalesAmount ) AS SumOfSales
FROM
Sales INNER JOIN Customers
ON Sales.CustomerKey = Customers.CustomerKey
WHERE Customers.Continent = 'Europe'
GROUP BY
Customers.CustomerName
使用DAX,您不会声明查询中的WHERE条件。相反,您使用一个特定的函数(FILTER)来过滤结果:
EVALUATE
SUMMARIZE (
FILTER ( Customers,
Customers[Continent] = "Europe"
),
Customers[CustomerName], "SumOfSales",
SUM ( Sales[SalesAmount] )
)
你可以看到这个 FILTER是一个函数:它只会返回居住在欧洲的客户,产生预期的结果。你嵌套函数的顺序和你使用的函数对最终结果和引擎的性能有很大的影响。这也发生在SQL中,即使在SQL中,您信任查询优化器来找到最优查询计划。在DAX中,尽管查询优化器做得很好,但是程序员在编写好的代码方面有更多的责任。
DAX 作为一种编程和查询语言
在SQL中,查询语言和编程语言之间有明显的区别;也就是说,用于在数据库中创建存储过程、视图和其他代码片段的指令集。每种SQL语法都有自己的语句,让程序员用代码丰富数据模型。另一方面,DAX在查询和编程之间几乎没有区别。一组丰富的函数可以操作表,反过来又可以返回表。您刚才在前面的查询中看到的过滤器函数就是一个很好的例子。
因此,在这方面,DAX比SQL更简单。一旦您将它作为一种编程语言(通常是它的第一个用法)来学习,您就会知道所有需要使用它作为查询语言的东西。
在DAX和SQL中的子查询和条件
作为查询语言,SQL最强大的特性之一是使用子查询的选项。DAX也有一些类似的概念即使在DAX的子查询中,它们自然地来自于语言的功能特性。
例如,在SQL中,仅为购买了100美元以上的客户检索客户和总销售额,您可以按照以下方式编写该查询:
SELECT
CustomerName,
SumOfSales
FROM
(
SELECT
Customers.
CustomerName,
SUM ( Sales.SalesAmount ) AS SumOfSales
FROM
Sales INNER JOIN Customers
ON Sales.CustomerKey = Customers.CustomerKey
GROUP BY
Customers.CustomerName
)
AS SubQuery
WHERE
SubQuery.SumOfSales > 100
通过简单地嵌套函数调用,您可以在DAX中获得相同的结果:
EVALUATE
FILTER (
SUMMARIZE (
Customers,
Customers[CustomerName], "SumOfSales",
SUM ( Sales[SalesAmount] )
),
[SumOfSales] > 100
)
在这段代码中,检索CustomerName和SumOfSales的子查询随后被输入到一个过滤器函数中,该函数只保留SumOfSales大于100的行。现在,这段代码可能看起来不可读,但是,一旦你开始学习DAX,你会发现子查询的用法要比SQL简单得多而且它自然流畅,因为DAX是一种函数式语言。
DAX 对于MDX开发人员
许多BI专业人员开始学习DAX,因为它是SSAS表格的新语言,在过去,他们使用MDX语言来构建和查询SSAS多维模型。如果你是其中之一,那就准备好学习一种全新的语言:DAX和MDX不怎么共享。更糟糕的是,DAX中的一些概念会让你想起MDX中类似的概念,即使它们是非常不同的。
事实上,根据我们的经验,在MDX之后学习DAX是最具挑战性的选择。为了学习DAX,你需要从MDX中解放你的思想;试着忘记你所知道的关于多维空间的一切,并准备好以清晰的头脑学习这门新语言。
多维模型VS表结构模型
MDX在您的模型定义的多维空间中工作。多维空间的形状是基于您在模型中定义的维度和层次结构的体系结构,而这又定义了多维空间的坐标集。不同维度的成员集合的交集定义了多维空间中的点。我们想你们需要花一些时间来理解任何属性层次结构中的所有成员实际上都是多维空间中的一个点。
DAX的工作方式简单得多。在多维空间中没有维度,没有成员,也没有点。换句话说,根本就没有多维空间。有一些层次结构,您可以在模型中定义它们,但是它们与MDX中的层次结构非常不同。DAX空间是建立在表、列和关系之上的。表格模型中的每一个表既不是度量组也不是维度:它只是一个表,用来计算值,您必须扫描它,过滤它,或者在它内部进行求和。一切都基于两个简单的表格和关系概念。
您很快就会发现,从建模的角度来看,表格提供的选项比多维度的要少。在这种情况下,拥有更少的选项并不意味着不那么强大,因为您有一种编程语言(即DAX),它可以让您丰富模型。表格的真正的建模能力是DAX的惊人速度。事实上,您可能已经习惯了在模型中避免使用太多的MDX,因为优化MDX速度通常是一个挑战。另一方面,DAX的速度惊人的快。因此,计算的大部分复杂性将不会出现在模型中,而是在DAX公式中。
DAX作为一种编程和查询语言
DAX和MDX都是编程语言和查询语言。在MDX中,MDX脚本的存在使差异变得清晰。您在MDX脚本中使用MDX,以及一些只能在脚本中使用的特殊语句(例如,范围语句),当您编写检索数据的SELECT语句时,您可以在查询中使用MDX。在DAX,这有点不同。您将使用DAX作为一种编程语言来定义计算出的列(MDX中不存在的新概念)和度量(类似于MDX中的计算成员)。您还可以使用DAX作为查询语言,例如,使用报表服务从表格模型中检索数据。然而,在DAX中并没有特殊的功能,只对这两种语言中的一种有用。此外,您还可以使用MDX查询表格模型。因此,MDX的查询部分使用表格模型,而DAX是您在编程表格模型时的唯一选择。
层次结构
使用MDX,您依赖于层次结构来执行大部分的计算。例如,如果您想要计算前一年的销售情况,您必须检索当前社员在年层次结构中的之前的社员,并使用它来重写MDX过滤器。例如,你用这种方法来定义MDX的前一年计算:
CREATE MEMBER CURRENTCUBE.[Measures].
[SamePeriodPreviousYearSales] AS
(
[Measures].[Sales Amount],
ParallelPeriod (
[Date].[Calendar].[Calendar Year],
1,
[Date].[Calendar].CurrentMember
)
)
该方法使用parallel周期函数,该函数返回日历层次结构中CurrentMember的表亲。因此,它基于模型中定义的层次结构。在DAX中使用过滤上下文和标准时间智能函数编写相同的计算:
[SamePeriodPreviousYearSales] :=
CALCULATE (
SUM ( Sales[Sales Amount] ),
SAMEPERIODLASTYEAR ( 'Date'[Date] )
)
您可以用许多其他方式编写相同的计算方法,使用FILTER和其他DAX函数,但思想仍然是一样的:您可以过滤表格,而不是使用层次结构。这个差别是巨大的,你可能会想念层级计算直到你习惯了DAX。
另一个重要的区别是,在MDX中,您用的是 [Measures].[Sales Amount]。在模型中已经定义了需要使用的销售额和聚合函数。在DAX中,没有预先定义的聚合。事实上,正如您可能已经注意到的,计算的表达式是SUM(Sales[Sales Amount])。预先定义的聚合函数不再在模型中;您需要在您想要使用它的时候定义它(您总是可以创建一个度量,它包含销售的总和,但是我们目前不想在这里言辞过多)。
DAX和MDX之间的另一个重要区别是,后者大量使用SCOPE语句来实现业务逻辑(同样,使用层次结构),而前者需要完全不同的方法,因为在语言中,层次处理是不存在的。
举个例子,如果你想在年度水平上定义一个度量值,在MDX中你会写下这个语句:
SCOPE (
[Measures].[SamePeriodPreviousYearSales],
[Date]. [Month].[All]
)
THIS = NULL;
END SCOPE;
在DAX中,没有SCOPE语句。为了获得相同的结果,您需要检查过滤器上下文中的过滤器的存在,并且场景要复杂得多
[SamePeriodPreviousYearSales] :=
IF (
ISFILTERED ( 'Date'[Month] ),
CALCULATE (
SUM ( Sales[Sales Amount] ),
SAMEPERIODLASTYEAR ( 'Date'[Date] ) ),
BLANK()
)
稍后您将了解这个公式的详细计算,但是,直观地,只有当用户在月份或下面浏览日历层次结构时,才会返回一个值,否则返回一个空白。这个公式比同等的MDX代码更容易出错。老实说,层次处理是DAX中真正缺失的一个特性。
叶级计算
最后,在使用MDX时,您可能已经习惯了避免叶级计算。在MDX中执行叶级计算是如此的缓慢,以至于您总是倾向于预先计算值和利用聚合来返回结果。在DAX中,叶级计算工作速度非常快,而且聚合根本不存在。这将需要在构建数据模型的时候改变您的想法。在大多数情况下,一个完全符合SSAS多维度的数据模型并不符合表格式模型,反之亦然。