Extreme DAX-第 2 章 模型设计

2022-05-24 17:02:01 浏览数 (1)

设计优良的分析模型是 DAX 高效运行的前提。在本章中,我们将讨论许多与建模有关的主题,这些主题对于理解性能强劲的模型设计非常重要。

本章中的主题包括以下几个方面。

  • Power BI 引擎的数据存储方式。
  • 选择正确的数据类型。
  • 关系。
  • 模型的结构。

为了构建良好的模型,您可能需要适当地转变一下自己的思维方式。无论您在之前的工作环境中一直习惯于使用Excel,还是更多地接触关系型数据库,当您开始接触 Power BI 时,都不得不做出一些改变。如果您更习惯于使用Excel,那么,想要理解数据分析模型中的“关系”这一概念,恐怕需要花上一段时间;即便您更熟悉关系型数据库,这两者之间也存在着诸多不同。关系型数据库专业人士对于“关系”一词肯定是非常熟悉的,然而 Power BI 中的关系却不等于关系型数据库中的关系,它们之间有着根本的不同。因此,我们将在本章中着重讨论这些差异。

2.1 列式数据存储

Power BI 模型的强大功能主要得益于智能的数据存储机制。Power BI模型本质上是数据库,因为与数据库一样,这些模型也对数据进行组织并存储。但是,其内部结构与您熟悉的其他数据库技术有很大不同。

2.1.1 关系型数据库

在过去,企业处理数据的方式是使用关系型数据库管理系统(relational database management system,RDBMS),如 Microsoft SQL Server。一个 RDBMS 中一般有大量的表,每一张表中列的数量都是固定的。每一列都必须具有固定的数据类型,如整数、文本或十进制数字,基于此,RDBMS 可以得出存储单行数据或记录所需的空间,并计算出磁盘上的一个数据文件可以存储多少行。这个特性使得 RDBMS 成为处理事务(process transactions)的应用程序的有效选择,例如来自网购平台的销售记录或公司财务分类帐中的记录。

RDBMS 中有一个概念叫做:索引(index)。通过索引可以快速而高效地查找特定的记录,这意味着也可以使用 RDBMS 有效地处理现有记录上的事务。关系型数据库这一术语之由来,是因为其中的表可以通过关系来连接,这确保了这些表中的数据是一致的;比如,某个 RDBMS 会阻止来自未知客户的销售交易插入。

RDBMS 技术经过长期的优化与迭代更新,目前已经非常成熟。因此,大多数传统的分析平台也依赖于RDBMS技术。但是,数据分析解决方案的技术需求与一个事务系统对技术的要求完全不同。在进行数据分析时,您往往不会从单个行中检索所有列的数据,相反,您可能对同时从多个行中获取数据感兴趣,并且往往只分析其中的一列或几列数据。然而想要从单个列中检索信息,RDBMS 仍需要从存储中读取一整行数据。同样,RDBMS并不擅长聚合多行数据,因此速度相对较慢。

图2.1对此过程进行了可视化说明:按行存储数据(由数字标识)无法有效地检索需要列的所有值。

图2.1 从基于行的存储中检索列的值效率低下

2.1.2 列式数据库

与RDBMS的按行存储数据不同的是,Power BI模型通过按列存储数据来实现这一过程。这背后的基本原理是,在数据分析解决方案中,往往只需要从存储中读取单独的几列,但所有可用的行都得参与计算。当同一列中的所有数据都存储在相邻的位置时,效率很显然是最高的。

另一个原因是,在实际的业务中,单个列中的许多值是相同的;例如,几千或几万种产品往往对应着数以百万计的销售交易记录。此时,列式数据库可以通过仅存储一次特定值并记录它所属的行来高度压缩数据。

列式数据库实现的高压缩率开辟了将整个数据库保存在内存中的可能性,这意味着所有数据都驻留在运行数据库的计算机或服务器的内存中,而不是存储在磁盘上的文件中。而将数据保留在内存中可进一步加快数据检索速度。

列式模型意味着数据聚合异常高效。例如,列式数据库引擎可以简单地获取每个不同的值,然后将其乘以显示该值的行数,而不是对列中所有单独的值求和。简而言之,Power BI 模型的数据库引擎从一开始设计时就旨在支持数据分析的这种典型工作负荷:处理具有特定特征的大量数据,并在这个过程中执行聚合与计算。

但是,需要提醒的是:最终,您仍然需要知道不同列中的哪些值是放在一行中的。仅仅知道编号为103的产品已经全部售出是不够的;您还需要知道它的价格,销售给哪个客户以及销售日期是哪一天。若要实现这一点,模型必须保留指针列表,以跟踪列中的某个值到底位于哪一行中。当向表中添加更多列时,计算量显然会显著增加。因此,在 Power BI 模型中,“窄”的表比“宽”的表更高效。

2.2 数据类型和编码

Power BI 模型包含有限数量的数据类型。为数据选择正确的类型非常重要,因为它决定了数据的存储或编码方式,以及模型处理数据的效率。以下是能够被 Power BI 识别的所有数据类型。

  • 文本(Text):最常见的数据类型就是文本。几乎所有数据都可以存储为文本。在Power BI 模型中,通过 Power Query 加载数据时,会将所有数据类型统一转换为 Text。很显然,当您忘记在 Power Query 中显式进行类型转换时,数字列也会存储为文本。(当然,你可以更改模型中的数据类型,这将自动在 Power Query 中添加一个更改数据类型的步骤。)
  • 整数(Whole Number):正如您所猜测的那样, “整数”数据类型用于存储整数。由于 Power BI 模型存储和压缩数据的方式,这是最高效的数据类型之一。
  • 十进制数字(Decimal Number):通常,数据类型为数字的都使用这个格式存储。从非常小的数到非常大的数,以及分数值,它几乎可以存储。最多可以存储 15 位数字。
  • 定点小数(Fixed Decimal Number):这种类型用于存储具有固定四位小数的小数值,有时也被称为“货币”类型(Currency)。最多可以存储包括四位小数在内的19位数字。这意味着此数据类型的覆盖范围小于十进制数字类型。定点小数类型通常用于存储货币金额,同时也可用于不需要很多小数的任何值。
  • 日期/时间、日期、时间(Date/Time, Date, Time):Power BI 模型使用与 Excel 类似的结构存储日期和时间值。这意味着其值是十进制数字,整数部分表示日期,小数表示时间。

与 Excel的不同点在于基本参考日期:在 Power BI 模型中,数字 1 对应于 1899 年 12 月 31 日,而在 Excel 中,数字 1 对应于 1900 年 1 月 1 日(均在零点)。小数是在此基础上添加二十四小时制的一天中的时间;例如,值 2.5 表示 1900 年 1 月 1 日中午。

您有三种选择来存储日期/时间数据。日期/时间数据类型同时存储日期和时间。日期数据类型仅存储日期,这意味着此数据类型等效于整数。时间数据类型仅存储时间部分,它一直是小数。

  • 真/假(True/False):真/假或布尔数据类型只能存储两个值:真和假。虽然使用时限制比较多,但使用此类型可以非常有效地存储数据。
  • 二进制(Binary):二进制类型用于存储不能表示为文本的数据,如图像数据或文档。无法使用此数据类型执行聚合或计算,但它可用于存储需要在报表中使用的图像。

为了实现高效的模型,为数据选择合适的数据类型至关重要。Power BI 模型旨在尽可能高效地将一系列唯一值存储在列中。虽然我们在使用计算机时早就不必考虑位和字节的概念了,但是在设计模型时,考虑计算机使用的单个0和1仍然会有所帮助。模型将确定存储值所需的位数;由于所有数据都运行在内存中,因此能节省一些内存就尽量节省一些。

例如,假设有一列都是介于 0 和 10 之间的整数。在数字表示法中,数字 10 表示为 1010 或 4 位。因此,该值便可以用4位的字进行编码,直接表示该值。此方法称为数值编码(value encoding)。在配备 64 位处理器的现代计算机中,使用 4 位显然要比将值存储为 64 位的数字要高效得多。

数值编码只对整数有效,因此,整数格式自然可以进行数值编码。但是一些披着其他数据类型外衣但是本质是整数的数据类型,同样也可以使用数值编码:比如日期和布尔值。还有一个是你可能想不到的:定点小数。定点小数由于是固定的4位小数:它可以被当成是一个整数除以10000的结果。实际上,DAX 引擎能够在进行数值编码之前先进行基本的转换,例如将所有的值减去相同的数字。

其他数据类型不能直接表示为整数,数据库仍然需要找到一种方法来将这些值存储在最小的位数中。方法是通过保留带编号的值列表并存储数字,而不是直接存储原始值。这称为哈希编码(hash encoding)。哈希编码列的工作方式不如数值编码列高效,因为数据库每次使用这一列时都需要在这些数字和值之间进行转换。

需要强调的一点是,Power BI 模型会根据列中的数据类型和值选择最佳编码形式。这意味着,哪怕数据类型是整数或者本质是整数,到最后仍然可能使用哈希编码。举一个极端的例子,有一个数字列,不仅包含0到10之间的数字,还包含数字1,000,000时,直接存储这些值所需的位数比较多,以至于引擎将决定改用哈希编码。

我们在实际项目中经常看到这种情况,特别是在存储日期时。假设您有一个包含员工的表,其中包含他们的入职日期以及离职日期。对于所有在职员工,离职日期当然是空的;但是习惯上我们并不是空着这个字段,而是设置一个特定的未来日期。很多时候这是一种有效的方式,但是如果选择像 9999 年 12 月 31 日这样的日期,则肯定无法享受对日期列进行数值编码的优势。建议使用不太遥远的未来的一天,例如 2029 年 12 月 31 日(当然,具体取决于你的实际方案)[1]。

2.3 关系

Power BI 模型中一个最容易被误解的元素是关系的概念。当你使用Power BI时,无论你之前是更多接触Excel,还是更加熟悉关系型数据库,你都需要从头开始学习Power BI 模型中的关系。

2.3.1 Excel 中的数据

让我们先关注 Excel 中的数据。在 Excel 中最接近数据库的概念是 Excel 表的概念。您可以将 Excel 表视为“扁平的”数据库。这种存储数据的方式有许多缺点。

例如,图2.2显示了某个存储在 Excel 工作表中的数据。

图2.2 Excel中的表格

图2.2展示的表中包含由员工销售订单的订单金额和日期。这样一个扁平的数据库存在诸多问题。

  • 显然,有关员工的所有信息(如工作角色和出生日期)都在该员工销售的每个订单中重复。因此,大量信息是冗余的,这占用了大量的存储空间。
  • 多次存储信息会增加数据出错的风险。
  • 当员工的某些属性(如其工作角色)发生更改时,必须在与该员工关联的所有行中进行更改。
  • 当一个实体有多个相同类型的属性时,情况会变得更糟。在我们的示例中,Giuliana 似乎有两个工作角色,并且每个销售订单仅与其中一个工作角色相关联。当我们按工作角色汇总销售额时,顾问(Consultant)的结果将仅包含Giuliana 的其中一个订单,从商业角度来看,有些时候说得通,但是有些时候这样得到的结果是错误的。
  • 最大的麻烦可能产生于从多个不同的数据源获取数据时。让我们设想这么一个场景,我们不仅有销售数据,还有目标数据。将来自不同数据源的数据合并到一个扁平的数据表中需要花费大量精力。实际上,Excel 用户将大部分时间花在设置单个扁平的数据表上,以便他们下一步能够使用数据透视表。

在Excel中,这些问题实际上没有解决方法。的确是这样,除非你开始使用 Power Pivot,而它和Power BI模型从本质上而言是等效的。在正式讨论 Power BI 里的方法之前,让我们看一下如何在关系型数据库中处理数据。

2.3.2 关系型数据库中的数据

在关系型数据库或 RDBMS 中,数据被分隔到多个表中。通常,这些表通常是关于那些组织的实体(如客户、员工、产品等)。表中的每一行都有一个标识符或(key),可以实现固定地引用其他表中的行;例如,在图2.3所示的销售订单表中,可以只包含客户和产品的键,而无需包含所涉及的客户和产品的所有属性。

图2.3 关系型数据库中的关系

显然,在录入销售订单时,没有客户键或者存在未知键,是没有任何意义的。这就是为什么在关系型数据库中,您需要定义表之间的关系,以表示表中的哪些列指向其他表中的键。关系型数据库会确保定义关系的列仅包含相关表的已知键。如果一条记录未存在于与之相关的表中,那么数据库将阻止其插入或更改。换句话说,关系型数据库中的关系充当对存储数据的约束,并用于强制实施参照完整性(referential integrity)。

2.3.3 Power BI的关系模型

终于,我们要开始讨论 Power BI 了。在 Power BI 模型中,关系是表与表之间的连接。这么一看,它们应该与关系型数据库中的关系相当,但实际上,它们完全是两个不同的事物。

Power BI 模型中关系的基础是具有唯一键的数据表。具有相同键值的另一个表可以与其相关,但在这个表中,键值不必是唯一的。这种类型的关系称为一对多关系,这意味着有一个表的键只出现一次,而另一个表的同一键可以多次出现。同关系型数据库一样,您可以将具有唯一键的列称为主键列(primary key column),将具有非唯一键的列称为外键列(foreign key column)。

在如图2.4所示的Power BI Desktop中的结构里,我们可以在模型视图中查看 Power BI 模型的结构以及其中的表和关系。

图2.4 Power BI 模型中两个表之间的关系

Power BI 模型中的关系与关系型数据库中的关系之间存在两个根本的区别。首先是参照完整性。关系型数据库中的关系充当数据约束,然而 Power BI 中的关系却并没有这么严格的要求。坦率地说,Power BI 并不在乎你的数据是否一致。当一些值只在外键列出现而不存在于主键列时,关系仍然可以存在。

如图2.5所示,模型会将每个未知的外键的值连接到一个空白行。模型中不会显示这个空白行,但是在报表中会显示。

图2.5 未知值与空白行相对应

这样做的一个优点是,您不必担心加载或刷新数据表的顺序,而在关系型数据库中,这是需要仔细考虑的。当然,缺点也随之而来,那就是在创建关系时必须小心,尤其是在模型视图中通过拖放字段来执行此操作时。当你拖拽字段放在错误的关系目标上时,Power BI 不会报错也不会有任何提示,它只会悄无声息地创建一个没有任何意义的关系。

Power BI和关系型数据库中的关系之间,还有另一个重要区别是筛选器传递(filter propagation)。Power BI 模型中的关系会主动筛选数据。更具体地说,当一个表中的某些行被选择时,另一个表中的相关行也会自动选择(沿着关系的箭头方向)。这是 Power BI 模型的核心设计原则,在进行 DAX 设计计算时需要充分考虑这一点。

而在关系型数据库中,关系并没有此功能。在查询关系型数据库时,用户必须指定要在哪些表上组合哪些(主键和外键)列。这使得查询关系型数据库非常灵活,但同时也迫使数据库为每个查询执行大量的工作。不过,这样导致的结果是,从一系列表中检索数据同时还需要处理大量关系会很低效。

2.3.4 关系属性

在 Power BI 模型中的表和表之间创建关系时,可以对驱动其行为的关系设置多个属性。这些属性与关系的主要目的,也就是筛选器传递,直接相关。

1.活动关系和非活动关系

要使关系能够进行筛选器传递,表之间必须存在明确的连接。假设对于销售交易记录,订单日期(order date)和付款日期(payment date)这两列同时存在。如果从这两列到日期表都存在关系,并且在日期表中选择了一行,那么我们在探讨应当筛选哪些销售交易记录时,会产生如下的疑问:是在该日期订购的交易记录,还是已付款的交易记录,还是将两者都筛选出来?

为了处理这个问题,Power BI 模型只允许两个表之间有一个活动的关系存在。当两个表通过其他表连接时,这同样适用:只允许单个活动关系路径。如图2.6所示,它是 fSales 表(销售表)的 Order Date 列(订单日期列)与 Calendar 表(日历表)的 Date 列(日期列)之间的关系。当你创建第二条路径的关系时,之前的关系将变为非活动状态。在模型视图中,非活动关系用虚线来表示。

图2.6展示了继续添加两个关系之后的模型视图:分别在 fSales[Delivery date] 与 Calendar[Date] 两列之间和 fSales[Payment date] 与 Calendar[Date] 两列之间建立关系。

图2.6 一个活动关系和两个非活动关系

在某个特定的计算中,可以使用 USERELATIONSHIP 函数来激活非活动关系,同时原有的活动关系在该计算中暂时失效。

2.交叉筛选方向

通过关系进行的筛选器传递通常仅从主表(primary table)到外表(foreign table)。如图2.7所示,在模型视图中,筛选器传递或者交叉筛选(cross filter)的方向通过关系线中间的小箭头显示。

图2.7 关系的交叉筛选方向

我们也可以更改交叉筛选的方向,让筛选器在两个方向上传递。这个操作是在编辑关系对话框中完成的,方法是将交叉筛选器方向设置为两个。看上去,在两个方向上进行筛选似乎应该是默认的简便设置,但,不要这样做!实际上,只有在某些特定方案中我们才会使用双向的交叉筛选关系。请尽量避免使用双向关系,否则您的报告中将会出现许多奇怪的现象、许多非活动关系以及高度复杂的 DAX 计算。

使用双向交叉筛选的一个特定场景是在处理多对多关系时。举个例子,假设一个包含客户(customer)和分支机构(branch office)的模型,如图2.8所示。每一个客户由一个或多个分支机构提供服务,反过来,每一个分支机构又服务于多个客户。

图2.8 客户和分支机构

Customer 表和 Branch office 表都有唯一的键列,但它们都没有包含外键的列:每一行都必须关联到另一个表中的多行。解决此问题的方法是:使用一个包含所有客户键和分支机构键组合的中间表[2],Branch office Customer 表。接下来,可以分别从中间表到 Customer 表和 Branch office 表创建关系,如图2.9所示。

图2.9 中间表

但是,以上的关系并没有正确地从 Customer 表到 Branch office 表进行交叉筛选,反过来也是如此:您可以在 Customer 表中选择一行,关系会将所选内容传递到中间表,但接下来却不会传递到 Branch office 表,因为此关系是单向的。

图2.10给出了解决方案:将两种关系都设置为双向的交叉筛选。此时,在 Customer 表中选择某一行时,左侧的关系将向右传递到中间表,右侧的关系再向右传递到 Branch office 表。反过来,在 Branch office 表中选择某一行时,关系会将所选内容传递到中间表,然后再将该选择传递到 Customer 表。

图2.10 通过中间表实现多对多关系

3.基数

模型中的默认关系是一对多关系,其中一个表包含一个唯一的主键,另一个表包含与外键相同的值,这些值并不是唯一的。此关系属性的正式叫法是基数(Cardinality)。

关系也可以有其他的基数。将一对多关系中的两个表位置换一下就是多对一关系。

关系可以具有一对一基数,这意味着在关系的两端,键都是唯一的。默认情况下,一对一关系的交叉筛选器方向是两个。因此,在几乎所有情况下这两个表都充当一个表。需要提醒的是,应避免在模型中建立一对一关系:除非有特定原因将它们分开,否则应将两个相关表合并为一个表(想要了解这些原因可能是什么,请参阅第8章“使用 AutoExist”)。

关系基数的最后一个选项是多对多。在这种情况下,两个相关表都不包含唯一的键。同样,您可能有特定的理由使用这种关系。但是,我们强烈建议不要使用多对多关系,因为这些关系很容易将你的模型搞得一团糟。本章后面 “在 Power BI 模型中要避免的关系型数据库原则” 部分将详细介绍多对多关系。

2.4 高效的模型设计

关系和筛选器传递的概念让 Power BI 模型可以实现强大的分析能力。因此,在建模时,思考模型的设计非常重要:模型应包含哪些表,这些表中需要包含哪些列,需要建立哪些关系?简而言之,模型的整体结构是什么?您在模型设计中所做的选择将决定模型能够达到什么样的效果。

2.4.1 星型结构和雪花结构

使用关系型数据库进行数据分析的最佳做法是使用一个特定的数据库结构,称为星型结构(star schema),如图2.12所示。星型结构的基本思想也适用于 Power BI 模型。

图2.12 通用的星型架构的结构

处于星型结构模型中心的表是事实表(fact table)。事实表包含已经发生、将要发生或应该发生的事情,如销售交易、财务分类账交易、客户查询、学生注册和销售机会等。

通过外键列,事实表与那些描述事实的不同实体(如客户、产品、成本中心、学生、日期等)的表建立关系。在星型结构的概念中,这些表称为维度表(dimension table);但是,在 Power BI 模型中,我们更愿意将它们称为筛选表(filter table),原因如下所述。

筛选表中的列被用来筛选报表中的结果,可以将它们用作矩阵或表中的行标签,或者作为图表的轴,又或者将它们作为切片器字段。事实表中包含报告需要进行聚合的数据。每个键值可以在事实表中多次出现,对应于同一天出现的多个事实,或者针对同一客户的多个数据,等等。

在一个纯粹的星型结构模型中,筛选表之间没有任何关系。当筛选表与其他筛选表相关时,生成的模型结构称为雪花结构(snowflake),如图2.13所示。

图2.13 雪花结构

2.4.2 星型结构的问题

在关系型数据库专业人士的眼中,雪花模式通常被认为是劣质设计。他们通常会花费大量时间和精力来设计纯星型架构。许多 Power BI 顾问都具有关系型数据库的应用背景,他们总是习惯于把在关系数据库中学到的知识迁移到 Power BI 上。因此,在收集有关 Power BI 建模的信息时,你总会听到他们说“生成星型架构!”

本节有些挑衅性的标题是为了讨论 Power BI 建模中真正重要的内容到底是什么。需要说明的一个事实是,Power BI 模型不是关系型数据库。然而,Power BI 模型的许多概念和术语却与关系型数据库非常像!这会导致一些 Power BI 模型设计人员将从关系型数据库学到的知识应用到 Power BI 模型当中。结果就是,这样做出来的模型效果欠佳。

需要注意的是,星型结构的概念是在列式数据库出现之前时开发的。关系型数据库的星型结构可最大限度地减少查询数据库时的连接数,这一点很重要,因为关系型数据库在同时联接多个大数据量的表时往往会遇到麻烦。这是数据仓库的典型工作负荷,因为在传统上,数据仓库被当作报告的数据源。我们所说的“传统上”,是指在 Power BI 模型出现之前;如今,数据仓库只是 Power BI 模型的数据源,在将数据导入模型时,根本不需要任何连接。

“为什么使用星型架构?”这个问题通常可以用它的反面来解答,它与“规范化的事务架构”不同。实际上,商业智能需要对许多数据行进行聚合,而事务处理则需要插入或更新单个数据行,同时保护数据的一致性。对于分析而言,基于星形架构的模型绝对是很有必要的。

然而,许多人将“很有必要使用星型架构”翻译为“不要使用雪花结构”。或者,换种说法,每个维度表都应与事实数据表直接相关。虽然在直接查询报表的数据仓库中可能需要这样,但对于 Power BI 模型,不能笼统地这样说。得益于关系的存在,Power BI 模型的技术性能表现非常好,基于此,Power BI 模型对数据的压缩能力非常强大,使用雪花结构不必成为一个大问题。根据经验,在设计模型时,星型结构是一个很好的起点,但是没有必要费心费力地去避免使用雪花结构。

为什么对星型结构与雪花结构进行如此长篇幅的阐述?因为这是将传统数据仓库的想法一股脑地应用于 Power BI 的主要表现。在我们的咨询工作中,我们经常需要与 IT 部门打交道,并且通常需要花费大量的时间和精力来解释 Power BI 与传统的数据库是有着根本不同的。我们特意对 Power BI 解决方案的某些元素使用不同的术语,以强调这些差异,并使业务人员更容易理解。

在下一节中,我们将讨论在进行 Power BI 解决方案建模时,应用传统的关系型数据库和数据仓库的原则会出现的几个问题。

2.4.3在 Power BI 模型中要避免使用的关系型数据库原则

在上一节中,我们提醒过不要盲目地将关系型数据库世界中的经验教训应用于 Power BI 模型。下面,我们将讨论几个具体的例子。

1.相互依存的维度

什么是维度?在数据仓库中,维度是一个表,其中包含有关存储在事实数据表中的事实的描述性属性。维度 这一名词来自数学和物理学中的概念;在这里,维度是描述一个对象的独立参数,如高度或宽度。

这与描述旧的数据分析方法(在列存储解决方案存在之前)的术语“多维建模”一起表明,数据仓库中的维度应该是独立的实体。当然,我们也遇到过多个维度表之间紧密关联的数据仓库结构。举个例子,现在有一个客户(Customer)维度表和一个市场细分(Market Segment)维度表。如果一个客户属于多个细分市场,那么维度之间确实应该是独立的;但在许多组织中,每个客户都属于单个细分市场。

这在数据仓库中谈不上是什么问题,但在 Power BI 模型中还真是个问题。假设你有一个 Power BI 报表,其中包含细分市场和客户的切片器。用户理所当然地期望,当他们选择某个细分市场时,客户切片器将仅显示与所选细分市场相关的客户。换言之,您的模型需要将市场细分维度表上的筛选传递到客户表,反之亦然。使用具有单个交叉筛选器方向的默认一对多关系不会有这个效果,我们需要对关系启用双向交叉筛选,从而生成如图2.14所示的模型。

图2.14 相互依赖的维度之间需要双向的交叉筛选

但是,当你试图添加第二个事实表时,你会发现这是一个非常不好的 Power BI 建模案例:之前的活跃关系总会有一些变得不活跃,而且不可避免。更好的设计方案是将属于一起的筛选器表进行聚类,并只允许其中一个表与事实表建立关系,并且设置为具有单个交叉筛选器方向。如果需要,可以使用双向交叉筛选器让几个筛选表实现集群(cluster),如图2.15所示。这样做的一大优点是可以省去事实数据表中的多个键列。

图2.15 筛选表集群

当然,有些人会说,可以将集群中的筛选表组合成一个大表,这样模型就会变成一个标准的星型结构。的确可以,但是没有必要非得这样做。此外,还有一些不这样做的理由:您可能有其他不同粒度的事实表需要专门与其中一个筛选表相关联(例如每个细分市场的目标),并且,相比于组合成单个大表,一个一个独立的表更容易被业务人员看懂。即便没有这种业务需要,至少您可以节省一些时间去更好地解决实际业务问题。

2.只有一个事实表

这个比较简单:我们经常会遇到一些人,他们听到星型结构需求时,直接将其翻译为“你应当只有一个事实表”。虽然这可以解决许多双向交叉筛选器关系的问题,但是想要创建单一的事实表,需要进行大量的工作,并且会导致事实表中具有太多的列。因此,我们给出结论:在模型中拥有多个事实表是完全没有问题的!

3.数据仓库作为单一信息源

从上面的讨论中可以清楚地看出,Power BI 在技术上没有必要使用经过全面设计的数据库架构的(关系型)数据仓库。由于数据仓库仅向 Power BI 模型提供数据表,因此实际上并不需要关系型数据仓库这样的架构优先(schema-first)体系结构。事实上,您可以使用更简单的数据优先(data-first)结构,比如数据湖。

但是,在许多情况下,企业拥有数据仓库才是常态。这通常伴随着“所有业务逻辑都必须在数据仓库中实现”这一口号的要求。从 Power BI 的角度来看,这并不是最好的方式。

该策略的主要缺陷是数据仓库只有一种与外界通信的方式:数据。一个报表通常包含以基本方式聚合或高度复杂的方式聚合的数据(本书的第二部分将仅介绍聚合数据的高级方法)。但事实是,报表中所需要的许多结果无法通过求某一列值的总和或平均值这样标准的聚合来完成。因此,不可能在数据仓库中实现所有业务逻辑。

数据仓库设计的初衷总有一条是为了实现尽可能多的业务逻辑。但问题还是一样,数据仓库只能以数据的形式进行通信。这将会导致事实表包含大量的列,每个列都有特定的业务规则或聚合。但是,你并不希望在 Power BI 模型的事实表中包含如此多的列!

4.使用多对多关系

您应该不惜一切代价去避免的一件事是:在两个事实表之间建立直接关系。由于事实表很少包含具有唯一值的列,因此一般而言这个关系将具有多对多基数。(不过,如果事实表确实包含具有唯一值或几乎唯一值的列,则应该反思一下,模型是否真的需要这一列。)

多对多关系不仅会因为筛选器传递受阻而导致意外的结果,而且模型的性能也会因此大打折扣。这是因为在这种情况下往往有太多的行是相关的。关系对性能的影响与主键(或关系的“一”端)的唯一值的数量高度相关。因此,不要让这个数字变得太大;根据我们的经验,最好不要超过100,000行。

对于多对多关系,另一个稍微有用一点的案例是将事实表与具有不同粒度的筛选表相关联。例如,您的模型包含一个 Product 表(产品表),其中包含对多个产品进行分组的 Category 列(类别列),销售记录可能存储在产品级别。目标或销售预测可以在产品类别这一级别上给出。Power BI 允许创建目标事实表与 Product 表 Category 列之间的多对多关系,如图2.16所示。

图2.16 使用多对多关系

虽然这个模型可以正常地工作,但我们更喜欢如图2.17所展示的那样,使用包含 Category 唯一值的中间筛选表来实现。

图2.17 使用中间表

通过使用中间表,所有结构都是通过常规的一对多关系实现的,这些关系具有一致的行为,并且 DAX 引擎已针对这些关系进行了优化。与其他关系的一个重要区别是,当遇到不匹配的值时,多对多关系不会自动添加空白行到筛选表中,这可能会导致意想不到的结果。毕竟,一旦出现数据不一致的情况,我们通常可以在 Power BI 模型的可视化对象中看见空白标签,这些空白标签是由空白行引起的。如果没有这些空白,我们无法知晓是否存在数据不一致的行为。

使用前面讨论的筛选表集群是使用常规关系处理事实表中不同粒度的最好方法。

2.5内存和性能注意事项

Power BI 模型的设计会极大地影响模型大小,模型大小又与模型的性能高度相关。在本节中,我们将分享一些优化模型性能的最佳实践,作为本章所讨论主题的概括。根据经验,就模型大小而言,较小的模型运行起来更快。Power BI 模型大小可以通过文件大小作来判断;您还可以使用特定的社区驱动的工具(如 DAX Studio)获得更详细的大小和性能的说明。

在设计 Power BI 模型时,请务必遵循以下准则。

  • 列越少越好。得益于列式数据库的概念,Power BI 模型实现了极高的数据压缩率。但是,它仍然需要记录哪些值是处于同一行中。表中的列越多,模型在查询哪些内容位于何处的工作量就越大。因此,请尽可能地让每个表的列数少一些。

我们观察到很多人的常规操作都是将源表的所有列一股脑全都加载到 Power BI 模型当中,或许是出于方便吧(或者可以说就是因为懒)。请注意,相比于找出那些用不到的列然后删除它们,其实根据需要去添加列或许更容易。模型永远不会有机地缩小,它只会变得越来越臃肿!

  • 选择合适的数据类型。Power BI 模型内部引擎对存储的数据进行比特级的优化,列式数据库的所有优化都基于此。这意味着任何不是整数的数据类型都必须使用其他方式进行处理,即使用一系列值的字典。当然,这并不是说只有整数这一种数据类型才能有效使用;前文提到过,有几种数据类型的本质也是整数,如日期、定点小数和布尔值。

在模型的关系中同样需要考虑数据类型,因此在建立关系时请尽可能地选择那些使用整数类型的列。

  • 拥有大量的行不是什么要紧的问题,但要注意大量的非重复值。同样,得益于列式数据库的概念,Power BI 模型可以高效地存储大量的行。它将自动检测在列中存储值的最佳方式,但是,非重复值越多,需要的存储空间就越大。到目前为止,列中唯一值的数量是最值得我们去注意的事情!

通常,节省内存的一种有效方法是删除事实表中的唯一键。许多事务系统为每个事务提供唯一标识符,Power BI 模型在加载这些表时,这些事务是最“昂贵”的列之一。我们遇到过这样的情况:仅仅是从最大的事实表中删除了一个唯一值的列,模型的大小竟然缩小了 90% 以上!

与数据类型一样,非重复值的数量也会对关系产生影响。关系的主键值的数量应保持相对小一些。如果你的模型中有些关系包含着数十万甚至上百万个唯一键值,那么最好你应当马上重构模型。

  • 避免异常值。在相当多的源系统中,开发人员使用一些特殊值来表示计划的真实数据缺失,或者由于其他什么原因这么做。为了确保不会与真实数据混淆,特殊值通常是异常值(outliers),比如“9999年12月31日”。这些异常值可能会导致 Power BI 模型像面对其他效率较低的数据类型一样使用字典来存储列,即使这些值同样是整数值。这是因为在将值存储为整数时,模型必须考虑列中最小值和最大值之间所有可能的值,在这种情况下,使用字典反而会效率更高一些。

若要避免这种情况发生,请将这些值设置为空白或选择一些接近真实值的特殊值。

  • 你真的需要所有的历史数据吗?很明显,想要让模型小一些,那么加载的数据量就小一些。我们见过相当多的源系统,保留着很长时间的历史数据。尤其是将数据仓库用作 Power BI 模型的数据源时。您完全可以加载从 2000 年开始往后的销售交易记录,但是你试着灵魂拷问一下自己:都 2022 年了,谁还会去分析 2010 年之前或 2015 年之前的销售数据?对于少数想要从“上古时期”中汲取智慧的“考古学家”来说,最好给他们单独做一个模型。对于日常使用的更丰富、更精细的模型来说,只要包含最近几年的数据即可。
  • 在某些情况下:拆分列。在某些极端情况下,将一个复杂的列拆分为两个具有更少唯一值的列,可能很有用。组合键(composite key)可能会发生这种情况,例如,由类别代码和序列号组成的产品代码:“A82.019”。单独的类别代码列和序列号的列将分别含有更少的非重复值,并且可以更高效地存储。这种方法在一些更复杂的处理中具有明显的缺点,并且,该组合列很有可能需要被用来建立关系;所以,不到万不得已,尽量还是不要这样做。

当模型的数据量不可避免地与日俱增时,以上所述的许多注意事项也会逐步产生更大的影响。但是,即使使用小的模型,也尽量牢记它们。毕竟,等到问题已经出现了再想着去调整模型,可能就不是那么简单了。

总结

在本章中,我们讨论了 Power BI 模型的基本概念。想必您已了解 Power BI 模型与其他数据管理产品的根本不同之处(基于内存的列存储),并深刻体会到由此带来的优势可以实现最佳的模型设计。

优质的 Power BI 模型是由一系列事实表、筛选表以及它们之间的关系构成的高效结构。从数据的粒度、唯一值和值分布的角度仔细甄别,并在此基础上对结构和数据类型做出良好的设计选择,考虑到以上这些,模型的性能一般会比较好。也许更为重要的是,这样精心设计的模型会为后续的一系列 DAX 运算奠定良好的基础。

在下一章中,我们将介绍在 Power BI 模型中使用 DAX 的多种方式。

译者注:

  1. 译者注:2029年12月31日是以47,483这个5位的数字来存储,引擎会自动选择数值编码;而9999年12月31日以2,958,465这个7位的数字来存储,引擎会自动选择使用哈希编码。↑
  2. 译者注:可以使用笛卡尔积来实现。↑

另外,我刚在Github上搞了一个博客,会将一些原来在公众号和知乎上发布的文章都集中到这里,也会与后续的公众号保持同步,请大家多多关照。

网址是:Powerbipro.cn

域名不错吧,想不想要个同域名的Power BI账号?

0 人点赞