DAX 权威指南 | 01 DAX是什么?

DAX是Microsoft SQL Server Analysis Services(SSAS)和Microsoft Power Pivot for Excel的编程语言。它创建于2010年,首次发布于PowerPivot for Excel 2010(是的,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语言之旅。

1.1 理解数据模型

DAX是专门用于在数据模型上计算业务公式的语言。您可能已经知道数据模型是什么,但如果您不熟悉它,则有必要将一些页面专门用于描述数据模型和关系,以便创建一个基础,您将在其上构建DAX知识。

数据模型是一组表,通过关系连接。

我们都知道表是什么:一组包含数据的行,每行分成列。每列都有一个数据类型,并包含一条信息。我们通常将表中的一行称为记录。表是组织数据的便捷方式。实际上,表本身已经是一个数据模型,尽管它是最简单的形式。因此,当您在Excel工作簿中编写名称和数字时,您正在创建数据模型。

如果您的数据模型包含许多表,则很可能它们通过关系连接。两个表之间存在关系。当两个表通过关系联系在一起时,我们说它们是相关的。在图形上,关系由连接两个表的线表示。图1-1显示了数据模型的示例。

DAX 权威指南 | 01 DAX是什么?_第1张图片
图1-1 这是由五个表组成的数据模型的简单示例。

学好关系的几个重点:

  • 关系中的两个表有不同的角色。它们被称为关系的一端和多端。在图1-1中,重点关注Product和Product Subcategory之间的关系。单个子类别包含许多产品,而单个产品只有一个子类别。因此,Product Subcategory是关系的一端(具有一个子类别),而Product是多端(具有许多产品)。
  • 用于创建关系的列(通常在两个表中具有相同的名称)称为关系的键。在关系的一端,列需要为每一行提供唯一值。在关系的多端,可以(并且经常)有重复的值。当列具有每行的唯一值时,它将被称为表的键。通常,表格中有一列是键。
  • 关系可以形成一个链。每个产品都有一个子类别,每个子类别都有一个类别。因此,每个产品都有一个类别。为了检索产品的类别,您需要遍历由两个关系组成的链。图1-1包含一个由三个关系组成的链的示例,从Sales开始并继续到Product Category。
  • 在每个关系中,可以有一个或两个小箭头。在图1-1中,您可以看到Sales和Product之间关系中的两个箭头,而所有其他关系都有一个箭头。箭头表示自动筛选关系的方向。我们将在后面的章节中更详细地讨论这个问题,因为确定正确的筛选方向是最重要的技巧之一。
  • 在表格数据模型中,只能在单个列上创建关系。引擎不支持多列关系。

1.1.1 了解关系的方向

正如我们在上一节中所说,每个关系可以有一个或两个筛选方向。筛选总是从关系的一端到多端。如果关系是双向的(也就是说,它上面有两个箭头),那么筛选也会从多端到一端。

一个示例可能会帮助您更好地理解此行为。如果基于先前在图1-1中显示的数据模型创建1个数据透视表,并将年份放在行,将Sum of SalesAmount以及Count of ProductName放在值区域,您将看到如图1-2所示的结果。

DAX 权威指南 | 01 DAX是什么?_第2张图片
图1-2 此数据透视表显示了在多个表中进行筛选的效果。

行标签包含年份, 即Date中的列。Date位于与Sales的关系的一端。因此,当您将Sum of SalesAmount放在数据透视表中时,引擎会根据年份筛选Sales。Sales和Product之间的关系是双向的;当您将Count of ProductName放在数据透视表中时,您会得到每年销售的产品数量。换句话说,年份上的筛选器通过关系链传递到Product表。

如果现在修改数据透视表,将颜色放在行上并在值区域中添加Count of FullDateLabel,则结果有点难以理解,如图1-3所示。

DAX 权威指南 | 01 DAX是什么?_第3张图片
图1-3 此数据透视表显示,如果双向筛选未激活,则不会筛选表。

行上的筛选器是Product表中的Color列。由于Product是与Sales的关系的一端,因此正确筛选了Sum of SalesAmount。显然会筛选Count of ProductNames,因为它是计算相同表(Product)行上的值。错误的数字是Count of FullDateLabel。实际上,它总是为所有行显示相同的值 (顺便说一下,这个数字是Date表中的总行数)。

来自Color列的筛选器未传递到Date的原因是Date和Sales之间的关系具有单个箭头,从Date指向Sales。因此,即使Sales上具有筛选器,筛选器也不能传递到Date,因为关系类型会阻止它。

如果更改Date和Sales之间的关系以启用双向筛选,则结果将如图1-4所示。

DAX 权威指南 | 01 DAX是什么?_第4张图片
图1-4 如果启用双向筛选,Color列会筛选Date表。

如您所见,现在数字不同,反映出至少有一种特定颜色的产品销售的天数。乍一看,似乎应该将所有关系定义为双向关系,以便让筛选器在任何方向上传递并始终返回有意义的结果。正如您将在本书中学到的,这并不总是设计数据模型的正确方法。实际上,您需要根据您使用的方案,来选择正确的关系传递。

1.2 DAX针对Excel用户

有可能你已经知道了Excel公式语言,DAX有点类似。毕竟,DAX的根源在Power Pivot for Excel中,开发团队试图保持这两种语言的相似性。这使得转换到这种新语言变得更容易。但是,两者之间有一些非常重要的区别。

1.2.1 单元格与表

在Excel中,您对单元格执行计算。使用其坐标引用单元格。因此,您编写如下公式:

= (A1 * 1.25) - B2

而DAX不同。在DAX中,不存在单元格及其坐标的概念。 DAX适用于表和列,而不是单元格。因此,无论何时编写DAX表达式,它们都只会引用表和列。表和列的概念在Excel中不是新的。实际上,如果将Excel范围定义为表(通过使用创建表格功能),则可以在Excel中编写引用表和列的表达式。如果查看图1-5,您看到SalesAmount列计算引用同一表中的列的表达式,而不是工作簿中的单元格。

DAX 权威指南 | 01 DAX是什么?_第5张图片
图1-5 您也可以在Excel表中使用列名

使用Excel,您可以使用[@ColumnName]格式引用表中的列,其中ColumnName是要使用的列的名称,@符号表示“获取当前行的值。”显然语法不是很直观,通常你不会写这些表达式。只需单击一个单元格即可显示它们,Excel会为您插入正确的代码。

您可能认识到Excel有两种不同的执行计算的方式:您可以使用标准单元格引用(在这种情况下,单元格F4的公式可能是E4 * D4),或者您可以使用列引用,如果您是在表中工作。使用列引用的优点是,您可以在列的所有单元格中使用相同的表达式,Excel将为每行计算具有不同值的公式。

DAX适用于表,因此所有公式都需要引用列。例如,在DAX中,您以这种方式编写前一个乘法:

Sales[SalesAmount] = Sales[ProductPrice] * Sales[ProductQuantity]

如您所见,每列都以其表的名称为前缀。在Excel中,您不提供表名,因为Excel公式在单个表中工作。但是,在DAX中,引用需要指定表名,因为DAX适用于包含多个表的数据模型,不同表中的列可能具有相同的名称。

DAX中的许多函数的工作原理与等效的Excel函数相同。例如,IF函数在DAX和Excel中以相同的方式读取:

Excel IF ( [@SalesAmount] > 10, 1, 0)
DAX IF ( Sales[SalesAmount] > 10, 1, 0)

Excel和DAX的语法的一个重要不同点是引用整个列的方式。您可能已经注意到,在编写[@ProductQuantity]时,@表示“当前行中的值。”使用DAX时,您不需要指定它。该语言的默认行为是获取当前行的值。如果在Excel中想要引用整个列(即该列中的所有行),则可以通过删除@符号来实现,如图1-6所示。

DAX 权威指南 | 01 DAX是什么?_第6张图片
图1-6 在Excel中,您可以通过省略列名前面的@符号来引用整列。

AllSales列的值在所有行中都相同,因为它是SalesAmount列的总计。换句话说,当前行中列的值与整个列的值之间存在语法差异。

而DAX不同。在DAX中,你可以这样编写图1-6的AllSales表达式:

[AllSales] := SUM ( Sales[SalesAmount] )

使用列来获取特定行的值与使用列作为整体之间没有语法差异。 DAX了解您要对列的所有值求和,因为您在聚合器(在本例中为SUM函数)中使用了列名,这需要将列名作为参数传递。因此,虽然Excel需要使用显式语法来区分要检索的两种类型的数据,但DAX会自动进行消歧。至少在开始时,这可能会令人困惑。

1.2.2 Excel和DAX:两种函数式语言

两种语言非常相似的一个方面是Excel和DAX都是函数式语言。函数式语言基本上由是函数调用的表达式组成。在Excel和DAX中都没有语句、循环和跳转的概念,这些概念对于许多编程语言来说都很常见。在DAX中,一切都是表达式。对于来自不同语言的程序员来说,语言的这一方面通常是一个挑战,但是对于Excel用户来说,这一点也不奇怪。

1.2.3 使用迭代器

迭代器对您而言可能是新的一个概念。在Excel中工作时,您习惯于一步一步地执行计算。在前面的示例中,您已经看到,为了计算销售总额,您创建了一个包含价格乘以数量的列,然后,作为第二步,您将其相加以计算总销售额。这个数字是有用的,例如,作为计算每个产品的销售百分比的分母。

使用DAX,您可以使用迭代器在单个步骤中执行相同的操作。迭代器正如它的名字所暗示的那样:它遍历表并对表的每一行执行计算,聚合结果以生成所需的单个值。

在上面的示例中,您可以使用SUMX迭代器计算所有销售额的总和:

[AllSales] :=
SUMX ( Sales, Sales[ProductQuantity] * Sales[ProductPrice] )

这种方法既有优点也有缺点。优点是您可以将许多复杂的计算作为单个步骤执行,而无需担心添加许多列,这些列最终仅对某些特定公式有用。缺点是使用DAX进行编程的可视性低于Excel。实际上,您没有看到计算价格乘以数量的列;它仅存在于计算的生命周期中。

1.2.4 DAX需要一些理论

让我们明确一点:这不是编程语言之间的区别;这是心态之间的差异。像这个星球上的任何人一样,您可能习惯于在网上搜索能解决与您类似需求的复杂公式和解决方案模式。当使用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的理论局限于几个重要的概念,这些概念将在第4章“理解计算上下文”中进行解释。当你读那一章时,请卷起袖子,准备好回到学校一段时间。一旦你掌握了它的内容,你将了解DAX的所有秘密,接下来的学习主要是获得经验。但是,除非掌握了这一理论,否则请不要试图走的更远。记住:知识是成功的一半。

1.3 DAX针对SQL开发人员

如果您习惯使用SQL语言,那么您已经使用了许多表,并在列之间创建了连接,以便设置关系。从这个角度来看,您将在DAX世界中感到宾至如归,因为DAX中的计算就是查询由关系连接的一组表并聚合值。

1.3.1 理解关系处理

SQL和DAX之间的第一个区别在于关系在模型中的工作方式。在SQL中,您可以在表之间设置外键来声明关系,但引擎从不在查询中使用这些外键,除非您明确说明它们。例如,如果您有Customer表和Sales表,其中CustomerKey是Customer中的主键和Sales中的外键,则可以编写如下查询:

SELECT 
    Customers.CustomerName,
    SUM (Sales.SalesAmount) AS SumOfSales 
FROM Sales 
INNER JOIN Customers 
  ON Sales.CustomerKey = Customers.CustomerKey 
GROUP BY Customers.CustomerName

即使您使用外键在模型中声明了关系,您仍然需要在查询中显式地声明连接条件。虽然这使查询更加冗长,但这很有用,因为它允许您在不同的查询中使用不同的连接条件,从而在查询的表达性方面给予您很大的自由。

在DAX中,关系是模型的一部分,它们都是LEFT OUTER JOINS。在模型中定义之后,您不再需要在查询中指定连接类型:只要您使用与主表相关的列,DAX就会在查询中自动使用LEFT OUTER JOIN。因此,您可以在DAX中将以前的SQL查询编写为:

EVALUATE
SUMMARIZE (
    Sales,
    Customers[CustomerName],
    "SumOfSales", SUM ( Sales[SalesAmount] )
)

由于DAX知道Sales和Customers之间的现有关系,因此它会根据模型自动进行连接。最后,SUMMARIZE函数需要按Customers [CustomerName]执行分组,但您没有任何关键字:SUMMARIZE自动按所选列对数据进行分组。

1.3.2 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中,尽管查询优化器做得很好,但程序员有更多的责任来编写好的代码。

1.3.3 DAX作为编程和查询语言

在SQL中,查询语言和编程语言之间有明显的区别;也就是说,用于在数据库中创建存储过程、视图和其他代码段的指令集。每个SQL语句都有自己的声明,让程序员用代码丰富数据模型。然而,DAX在查询和编程之间几乎没有区别。丰富的函数集操作表,反过来又可以返回表。您在上一个查询中看到的FILTER函数就是一个很好的例子。

因此,在这方面,DAX比SQL简单。一旦您将它作为一种编程语言来学习(通常是它的第一次使用),你就会知道将它用作查询语言所需的一切。

1.3.4 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的子查询稍后被送入FILTER函数,该函数仅保留SumOfSales大于100的行。现在,您可能觉得这段代码不可读,但是,只要您开始学习DAX,您将发现子查询的使用比在SQL中更容易,并且它自然流动,因为DAX是一种函数式语言。

1.4 DAX 针对MDX开发人员(略)

你可能感兴趣的:(DAX 权威指南 | 01 DAX是什么?)