PowerBI数据建模基础操作1:数据关系(基数、双向筛选、常规关系、有限关系)与星型架构(维度表、事实表)

文章目录

    • 一、基本概念
      • [1.1 为什么要进行数据建模](#1.1 为什么要进行数据建模)
      • [1.2 语义模型](#1.2 语义模型)
      • [1.3 Power BI Desktop 视图](#1.3 Power BI Desktop 视图)
      • [1.4 导入模式和DirectQuery(待补)](#1.4 导入模式和DirectQuery(待补))
    • [二、 数据建模](#二、 数据建模)
      • [2.1 创建关系](#2.1 创建关系)
        • [2.1.1 基数](#2.1.1 基数)
        • [2.1.2 交叉筛选方向](#2.1.2 交叉筛选方向)
        • [2.1.3 活动关系与角色扮演维度表](#2.1.3 活动关系与角色扮演维度表)
        • [2.1.4 假设引用完整性(假设完全匹配)](#2.1.4 假设引用完整性(假设完全匹配))
        • [2.1.5 创建单独的关系图](#2.1.5 创建单独的关系图)
        • [2.1.6 断开连接的表(待补)](#2.1.6 断开连接的表(待补))
      • [2.2 管理关系](#2.2 管理关系)
        • [2.2.1 使用关系编辑器对话框](#2.2.1 使用关系编辑器对话框)
        • [2.2.2 通过"属性"窗格编辑关系](#2.2.2 通过“属性”窗格编辑关系)
        • [2.2.3 多选编辑](#2.2.3 多选编辑)
        • [2.2.4 关系自动更新设置](#2.2.4 关系自动更新设置)
      • [2.3 优化数据模型](#2.3 优化数据模型)
        • [2.3.1 隐藏表或字段](#2.3.1 隐藏表或字段)
        • [2.3.2 可视化数据排序](#2.3.2 可视化数据排序)
        • [2.3.3 数据格式](#2.3.3 数据格式)
      • [2.4 行级安全性 (RLS)](#2.4 行级安全性 (RLS))
        • [2.4.1 定义角色和规则](#2.4.1 定义角色和规则)
          • [2.4.1.1 角色创建步骤](#2.4.1.1 角色创建步骤)
          • [2.4.1.2 注意事项](#2.4.1.2 注意事项)
          • [2.4.1.3 RLS的筛选方向](#2.4.1.3 RLS的筛选方向)
        • [2.4.2 在 Power BI 服务中管理角色](#2.4.2 在 Power BI 服务中管理角色)
        • [2.4.3 在 Power BI 中使用 RLS 与工作区](#2.4.3 在 Power BI 中使用 RLS 与工作区)
        • [2.4.4 注意事项和限制](#2.4.4 注意事项和限制)
        • [2.4.5 常见问题解答](#2.4.5 常见问题解答)
      • [2.5 相关 DAX 函数](#2.5 相关 DAX 函数)
      • [2.6 源组](#2.6 源组)
        • [2.6.1 源组的划分](#2.6.1 源组的划分)
        • [2.6.2 单源模型和复合模型](#2.6.2 单源模型和复合模型)
        • [2.6.3 源组对关系评估的影响:](#2.6.3 源组对关系评估的影响:)
      • [2.7 关系评估](#2.7 关系评估)
        • [2.7.1 常规关系](#2.7.1 常规关系)
          • [2.7.1.1 数据结构(Data Structure)](#2.7.1.1 数据结构(Data Structure))
          • [2.7.1.2 表扩展(Table Expansion)](#2.7.1.2 表扩展(Table Expansion))
        • [2.7.2 有限关系](#2.7.2 有限关系)
          • [2.7.2.1 有限关系的限制](#2.7.2.1 有限关系的限制)
          • [2.7.2.2 有限关系的查询方式](#2.7.2.2 有限关系的查询方式)
          • [2.7.2.3 其它限制](#2.7.2.3 其它限制)
          • [2.7.2.4 最佳实践](#2.7.2.4 最佳实践)
        • [2.7.3 歧义路径解析机制(双向关系)](#2.7.3 歧义路径解析机制(双向关系))
          • [2.7.3.1 优先级(Priority Tiers)](#2.7.3.1 优先级(Priority Tiers))
          • [2.7.3.2 权重](#2.7.3.2 权重)
        • [2.7.4 不同关系之间的性能评估](#2.7.4 不同关系之间的性能评估)
      • [2.8 模型约束(无法确定各字段之间的关系)](#2.8 模型约束(无法确定各字段之间的关系))
        • [2.8.1 标准星型模型(无度量约束)​](#2.8.1 标准星型模型(无度量约束))
        • [2.8.2 星型模型+度量约束](#2.8.2 星型模型+度量约束)
        • [2.8.3 非星型模型,无约束(危险结构)](#2.8.3 非星型模型,无约束(危险结构))
        • [2.8.4 非星型模型+度量约束](#2.8.4 非星型模型+度量约束)
        • [2.8.5 混合约束](#2.8.5 混合约束)
        • [2.8.6 解决办法](#2.8.6 解决办法)
      • [2.9 关系问题解决指南](#2.9 关系问题解决指南)
    • 三、星型模型
      • [3.1 维度表和事实表](#3.1 维度表和事实表)
      • [3.2 星型模型的重要性](#3.2 星型模型的重要性)
    • 四、维度表
      • [4.1 主键、外键、自然键、代理键、特殊键值](#4.1 主键、外键、自然键、代理键、特殊键值)
      • [4.2 维度属性、历史跟踪属性、审核属性、维度表大小](#4.2 维度属性、历史跟踪属性、审核属性、维度表大小)
      • [4.3 规范化与非规范化](#4.3 规范化与非规范化)
      • [4.4 雪花维度](#4.4 雪花维度)
      • [4.5 层次结构](#4.5 层次结构)
        • [4.5.1 均衡层次结构(Balanced hierarchies)](#4.5.1 均衡层次结构(Balanced hierarchies))
        • [4.5.2 非均衡层次结构(Unbalanced hierarchies,父子结构)](#4.5.2 非均衡层次结构(Unbalanced hierarchies,父子结构))
        • [4.5.3 不规则层次结构(Ragged hierarchies)](#4.5.3 不规则层次结构(Ragged hierarchies))
      • [4.6 渐变维度(SCD)](#4.6 渐变维度(SCD))
        • [4.6.1 Type 1 SCD(覆盖旧值)](#4.6.1 Type 1 SCD(覆盖旧值))
        • [4.6.2 Type 2 SCD(插入新值,保留旧值)](#4.6.2 Type 2 SCD(插入新值,保留旧值))
          • [4.6.2.1 简介](#4.6.2.1 简介)
          • [4.6.2.2 SQL示例](#4.6.2.2 SQL示例)
          • [4.6.2.3 报表示例](#4.6.2.3 报表示例)
        • [4.6.3 Type 3 SCD(保留有限的历史版本)](#4.6.3 Type 3 SCD(保留有限的历史版本))
      • [4.7 杂项维度(Junk dimensions)](#4.7 杂项维度(Junk dimensions))
      • [4.8 退化维度与退化维度表](#4.8 退化维度与退化维度表)
        • [4.8.1 退化维度](#4.8.1 退化维度)
        • [4.8.2 退化维度表](#4.8.2 退化维度表)
      • [4.9 日期和时间维度](#4.9 日期和时间维度)
      • [4.10 一致维度](#4.10 一致维度)
      • [4.11 角色扮演维度(见6.4章节)](#4.11 角色扮演维度(见6.4章节))
      • [4.12 外延维度](#4.12 外延维度)
      • [4.13 多值维度](#4.13 多值维度)
      • [4.14 维度表创建指南](#4.14 维度表创建指南)
    • 五、事实表
      • [5.1 简介](#5.1 简介)
      • [5.2 事实表的类型](#5.2 事实表的类型)
        • [5.2.1 事务事实表(销售表)](#5.2.1 事务事实表(销售表))
        • [5.2.2 周期快照事实表(库存表)](#5.2.2 周期快照事实表(库存表))
        • [5.2.3 累积快照事实表(进度表)](#5.2.3 累积快照事实表(进度表))
      • [5.3 度量值](#5.3 度量值)
      • [5.4 特殊事实表](#5.4 特殊事实表)
        • [5.4.1 无事实的事实表(Factless fact tables,桥接表)](#5.4.1 无事实的事实表(Factless fact tables,桥接表))
        • [5.4.2 聚合事实表(Aggregate fact tables,待补)](#5.4.2 聚合事实表(Aggregate fact tables,待补))
    • 六、模型关系
      • [6.1 一对一关系](#6.1 一对一关系)
        • [6.1.1 退化维度(略)](#6.1.1 退化维度(略))
        • [6.1.2 跨表行数据(一对一关系)](#6.1.2 跨表行数据(一对一关系))
        • [6.1.3 源组内一对一关系优化:合并查询](#6.1.3 源组内一对一关系优化:合并查询)
        • [6.1.4 跨源组一对一关系优化:预先合并数据](#6.1.4 跨源组一对一关系优化:预先合并数据)
      • [6.2 多对多关系](#6.2 多对多关系)
        • [6.2.1 多对多维度表关联(设置双向过滤)](#6.2.1 多对多维度表关联(设置双向过滤))
        • [6.2.2 多对多事实表关联(推荐采用星型架构)](#6.2.2 多对多事实表关联(推荐采用星型架构))
        • [6.2.3 高粒度事实表关联(无法直接建立一对多关系)](#6.2.3 高粒度事实表关联(无法直接建立一对多关系))
          • [6.2.3.1 高粒度时间处理](#6.2.3.1 高粒度时间处理)
          • [6.2.3.2 关联非日期的更高粒度](#6.2.3.2 关联非日期的更高粒度)
          • [6.2.3.3 总结](#6.2.3.3 总结)
      • [6.3 双向关系](#6.3 双向关系)
        • [6.3.1 特殊模型关系](#6.3.1 特殊模型关系)
        • [6.3.2 启用非空选项的切片器(使用度量值进行筛选)](#6.3.2 启用非空选项的切片器(使用度量值进行筛选))
        • [6.3.3 多维度分析(临时激活双向筛选关系)](#6.3.3 多维度分析(临时激活双向筛选关系))
      • [6.4 活动、非活动关系与角色扮演维度](#6.4 活动、非活动关系与角色扮演维度)

全文参考:《在 Power BI 中对数据进行转换、调整和建模》

一、基本概念

1.1 为什么要进行数据建模

使用的数据透视表的都知道,透视表只能从单个表中取数,如果想把其他表中的数据也放进来,只能先利用Vlookup把其他表的数据合并过来,然后再把这个字段放到透视表中。这只适用于数据非常简单的情况,如果数据量大或者维度很多,用透视表就无法满足需求了。

数据建模是在 Power BI 中创建和组织数据结构的过程,关键在于创建数据源之间的关系(具体来说,是不同表之间,列与对应列的关系),以便整合数据,进行深入分析和可视化展示。

  1. 数据源导入:在 Power BI 中,首先你需要从不同的数据源(如 Excel、SQL Server、Azure、Web 数据等)导入数据。
  2. 数据变换(Data Transformation):Power BI 提供了 Power Query 编辑器来进行数据清洗、变换和准备。Power Query 使用 M 语言来编写变换规则。
  3. 建立模型关系:Power BI 使用关系型数据模型,通过"表"来存储数据。这些表可以是直接从数据源导入的,也可以是通过计算创建的。对这些表建立关系后,可以更方便地进行跨表查询。
  4. 查询数据,创建可视化报表

1.2 语义模型

Microsoft 已将 Power BI 数据集(dataset)这一内容类型进行了重命名,改为语义模型(semantic model),原因主要有两个:

  1. "数据集"这个词太广泛了,在不同的数据活动中含义不同
  2. "语义模型"更能体现Analysis Services 数据模型的丰富功能,而Power BI的报告正是基于这些数据模型。

下面是名称更改的一些示例:

旧名称 新名称
数据集 语义模型
共享数据集 共享语义模型
导入数据集 导入语义模型
DirectQuery 数据集 DirectQuery 语义模型
复合数据集 复合语义模型
实时连接数据集 实时连接语义模型
本地数据集 本地语义模型
数据集所有者 语义模型所有者
大型数据集 大型语义模型

1.3 Power BI Desktop 视图

数据加载并处理完毕后,在主页选项下,可以看到以下几种视图,另外左侧还有数据窗格。


表视图

  1. 报表视图(Report View) :Power BI 中最常用的视图,用于创建和编辑交互式报表

    用户可以在报表视图中添加各种可视化组件,如图表、表格、地图等,并将它们放置在报表的页面上。报表视图支持数据的切片、筛选和钻取等交互操作,使用户能够深入分析数据。

  2. 表格视图(Table View):将数据以表格形式展示,有助于用户理解和管理数据结构,以及进行数据清洗和预处理。

  3. 模型视图(Model View):用于查看和管理数据模型,包括表之间的关系、数据类型、度量值等。用户可以在模型视图中创建和管理关系,设置数据模型的属性,如表的主键和外键。

  4. DAX 视图(DAX View):DAX(数据分析表达式)是 Power BI 中用于创建计算列、度量值和时间智能计算的公式语言。在 DAX 视图中,用户可以编写和编辑 DAX 公式。

1.4 导入模式和DirectQuery(待补)

参考《Power BI 中的 DirectQuery》《DirectQuery 模型指导》

在 Power BI 中,数据可以通过多种方式加载到模型中,其中最常见的两种方式是导入模式(Import Mode)和DirectQuery 模式。这两种模式决定了数据如何存储、查询以及与数据源的交互方式。

  1. Import Mode:是指将数据从数据源全部加载到 Power BI 的内存中**(或本地文件中),并存储在 Power BI 的 VertiPaq 引擎中。

    • 存储:导入的数据会存储在 Power BI 内存中,Power BI 可以独立于原始数据源进行查询和分析
    • 性能:查询速度通常更快,因为数据已经存储在内存中,Power BI 可以直接对这些数据进行计算和分析,视觉变化能立即反映。
    • 更新:数据需要定期刷新(手动或自动)以保持最新,刷新操作会重新从数据源加载数据到 Power BI。
    • 适用场景:数据量较小(< 1GB)、更新频率不高的场景。能充分利用 Power BI 的高性能查询引擎,提供高度交互性和功能完整的体验(数据转换和建模)。
  2. DirectQuery模式不缓存数据,而是直接查询数据源。这意味着 Power BI 会将用户的查询转换为 SQL 查询(或其他数据源的查询语言),并实时从数据源返回结果。

    • 存储:数据保留在原始数据源中,比如SQL Server 数据仓库,Power BI 只在需要时查询数据。
    • 性能查询速度取决于数据源的性能和网络延迟,不支持某些高级功能(如某些 DAX 函数)。
    • 更新:数据始终是最新的,因为每次查询都会直接从数据源获取最新数据。
    • 适合场景:数据量太大,或需要近实时查询的场景。

二、 数据建模

参考:《Power BI Desktop 中的模型关系》《创建和管理关系》

2.1 创建关系

现在假设有个电子产品专卖店,销售产品有三类:手机、电脑、平板,每一类又分别来自三个品牌:小米、苹果、三星,销售明细表记录了每个电子产品专卖店2016至2017年的销售明细数据:



通过选择 Power BI Desktop 左侧的"模型"图标进入模型视图 。在模型视图中,可以看到上述四张表。其中,产品明细表和销售明细表之间已经有一条线,这是由于表格导入后,PowerBI会在数据加载后自动检测并创建新关系,没有检测到的表,可以点击一个表中的字段拖动到另一个表的对应字段上,即可弹出新建关系界面:

在Power BI中,关联两个表的列时,数据类型必须一致 。如果关联的是DateTime列,要注意Power BI的引擎只识别DateTime类型,Date、Time、Date/Time/Timezone等格式只是在界面上显示的样式。如果在建模选项卡中把DateTime列设置为Date类型,引擎还是会考虑时间部分,导致关联失败。所以,需要在Power Query编辑器中把时间部分去掉,让数据只保留日期部分,这样才能正确关联。

2.1.1 基数

基数(Cardinality):定义了两个表之间关系的类型,Power BI 会根据数据自动推荐合适的基数。常见的基数类型包括:

  • 一对一(1:1):一个表中的每一行与另一个表中的唯一一行对应。
  • 一对多(1:*:一个表中的每一行可以与另一个表中的多行对应,是最常见的基数类型。
  • 多对多(*:*:两个表之间存在多对多的对应关系,通常需要通过中间表来实现,可以使用它来关联多对多事实或关联更高粒度的事实。

在 Power BI Desktop 中创建关系时,设计器会自动检测并设置基数类型,但有时可能会出错,比如表尚未加载数据;或在一对多关系中,"多"端的列加载时恰好没有重复值,这种情况下,Power BI 可能会错误地将关系设置为一对一。此时,你可以在模型视图中手动调整基数类型。调整时,"一"侧列必须只包含唯一值,或表尚未加载行数据(表为空,不会有数据冲突),否则可能报错。

2.1.2 交叉筛选方向

参考《调整交叉筛选方向》

交叉筛选方向(Cross-Filter Direction) 交叉过滤方向决定两个表之间筛选器的传播方向,这包括:

  • 单向(Single) :筛选器从一个表流向另一个表。默认情况下,一对多关系的筛选器从"一"侧流向"多"侧
  • 双向(Both) :筛选器可以在两个表之间双向流动。一对一关系中,始终为双向筛选

可能的交叉筛选选项取决于基数类型:

基数类型 交叉筛选器选项
一对多(或多对一) 单向(一侧流向多侧)、双向
一对一 双向
多对多 单(Table1 到 Table2)、单(Table2 到 Table1)、双向

在 Power BI Desktop 模型视图中,块表示表,线表示关系。可以通过查看关系线两端的指示符(1 或 *)来判断基数类型,通过关系线的箭头来判断关系的交叉筛选方向。单箭头表示沿箭头方向的单向筛选器;双箭头表示双向关系。选择关系线或将光标悬停在其上方,可以突出显示这些列。

双向筛选特别适用于 星型架构(Star Schema),即一个中心表连接多个维度表。在这种架构下, Power BI 将连接表的所有方面视为一个单一的表,从而简化数据模型的逻辑,并且能够提供更灵活的过滤选项。

传递路径不明:在某些复杂的数据模型中,双向跨表过滤可能会导致歧义。例如,如果数据模型中存在循环(Loops),即多个表之间存在多条路径,Power BI 会根据一定的规则(如优先级和权重)来解决这种不明确性。但如果路径过于复杂,双向跨表过滤可能会导致不清楚过滤应该如何传播,导致查询失败或返回错误的结果。解决歧义的方法有:

  • 删除或标记关系为非活动状态,从而减少数据模型中的关系数量,消除歧义。
  • 使用 USERELATIONSHIP 函数来显式设置关系的权重,从而确定路径传播方向
  • 引入同一个表两次:通过将同一个表以不同的名称引入两次,可以消除循环,使数据模型的结构更接近星型架构。在这种情况下,所有关系都可以设置为双向。

另外,双向关系可能会对性能产生负面影响,因为需要在两个方向上传递过滤器,这意味着查询引擎需要执行更多的计算和数据处理,从而增加了查询的复杂性和执行时间,尤其是在数据量较大的情况下。建议仅在需要时使用双向筛选,例如,当需要在两个表之间进行复杂的交互式分析时。在启用双向关系后,务必对模型进行性能测试,确保查询响应时间在可接受范围内。更多内容,详见《双向关系指南》

2.1.3 活动关系与角色扮演维度表

在 Power BI Desktop 模型视图中,活动关系用实线表示;非活动关系用虚线表示,同一个字段只能存在一个活动关系。更详细的内容,要讲清楚的话,有很多后续章节会讲到的新的概念,所以放到本文4.4章节中。

2.1.4 假设引用完整性(假设完全匹配)

"假设引用完整性"(Assume Referential Integrity)属性,是一个用于优化 DirectQuery 模式下查询性能的设置。仅适用于以下情况:

  • 两个表都使用 DirectQuery 存储模式,且都属于同一个数据源组(Source Group)。
  • 表之间的关系是一对多(1:*)或一对一(1:1),"多"侧列不包含 NULL。
  • 数据完整性有保障(即两个表之间没有不匹配的行),最好存在外键约束

这个属性的作用是告诉 Power BI,在查询数据源时,是否可以假设两个表之间的数据完全匹配。当启用"假设引用完整性"时,Power BI 在查询数据源时会使用 INNER JOIN(内连接,只返回两个表中匹配的行)来连接两个表,而不是 OUTER JOIN(外连接,返回所有行,不匹配的行会填充 NULL 值)。

通常情况下,启用"假设引用完整性"可以提高查询性能,因为 INNER JOINOUTER JOIN 更高效。然而,这种性能提升也取决于数据源的具体实现。

如果数据库中已经定义了外键约束(Foreign Key Constraint),确保了两个表之间的数据完整性;或者即使数据库中没有外键约束,但你确信数据完整性没有问题,都可以启用此属性。

警告:如果数据完整性被破坏(即两个表之间存在不匹配的行), INNER JOIN 会消除这些行。这可能会导致数据丢失或结果不准确。

2.1.5 创建单独的关系图

参考《创建单独的关系图》

默认情况下,数据导入完成后,主页显示的是所有数据表的视图,在下方可以看到 所有表 的标签。如果只想对其中部分数据表建立单独的关系图,操作步骤如下:

  1. 点击"所有表"旁边的+号,创建新的关系图。

  2. 将所需的表从数据窗格拖动到画布区域。

  3. 右键单击表,选择 "添加相关表",与原始表相关的表将显示在新关系图中。

2.1.6 断开连接的表(待补)

模型中很少出现与其它表无关联的表(断开连接表)。这种表不向其他表传递过滤器,而是接受用户输入(如通过切片器视觉对象),供模型计算使用。例如,使用一个断开连接表来加载一系列汇率值,可让用户选择某种汇率单位时,得到相应的计算结果。

Currency ExchangeRate Date
EUR 1.1 2024-01-01
CNY 0.15 2024-01-01
USD 1 2024-01-01

假设有以上汇率表,以及一个销售表。创建一个度量值:Sales in USD:

cpp 复制代码
Sales in USD = 
SUMX(						//SUMX 函数遍历 Sales 表中的每一行。
    Sales,					//对于每一行,通过 RELATED 函数获取对应货币的汇率值(ExchangeRate),并将其乘以销售金额(Amount)。									
    Sales[Amount] * RELATED('CurrencyExchange'[ExchangeRate])  		
)
  • 汇率表是一个断开连接表,因为它不需要通过关系过滤其他表。它只是提供了一个汇率值的集合,供计算使用。
  • 如果用户通过一个切片器选择了一个特定的日期,CurrencyExchange 表会根据该日期提供相应的汇率值,从而动态地计算出对应的美元销售额。
  • what-if 参数可用于创建断开连接的表,详见《创建并使用 What if 参数来可视化变量》

2.2 管理关系

在 Power BI Desktop 中创建关系时,设计器会自动检测并设置基数类型,但有时可能会出错,如表尚未加载数据,或预期包含重复值的列当前包含唯一值。例如,在一对多关系中,"多"端的列通常会包含重复值。然而,如果在数据加载时,这些列中恰好没有重复值,Power BI 可能会错误地将其设置为一对一关系,此时,你需要手动编辑关系。

在 Power BI 中,编辑关系主要有两种方法:使用关系编辑器对话框,或在"模型视图"中通过"属性"窗格编辑关系。

2.2.1 使用关系编辑器对话框

从报表视图建模选项卡、表格视图表工具选项卡、模型视图主页选项卡都可以找到管理关系选项,单击之后将弹出管理关系界面。

选择关系之后,点击编辑,即可弹出关系编辑对话框。你也可以双击或者右键单击任意两个表之间的连接线,也可以打开此编辑器。

点击 自动检测,PowerBI会自动查找新数据或更新数据中的关系(选择不同列时自动验证关系,提供适当的基数和交叉筛选选项)。

2.2.2 通过"属性"窗格编辑关系

通过"属性"窗格编辑关系:在"模型视图"中,选择任意两个表之间的连接线,在右侧"属性"窗格中,可以编辑关系选项;在模型窗格中,可以查看所有已建立的关系。

  • 这是一种简化的关系编辑方式,仅显示表名和列名供用户选择; 不提供数据预览,关系选择的验证仅在点击"应用更改"时进行。
  • 使用"属性"窗格编辑关系可以减少编辑关系时生成的查询数量,这对于大数据场景(尤其是使用 DirectQuery 连接时)非常重要。
  • 无数据预览:在"属性"窗格中编辑关系时,不会显示数据预览。这意味着系统不需要在后台生成额外的查询来获取和显示数据预览,从而减少了查询数量。
  • 延迟验证:在"属性"窗格中,关系的选择和配置在点击"应用更改"之前不会进行验证。这避免了在编辑过程中频繁生成验证查询,进一步减少了查询数量。
  • 多选编辑:在"属性"窗格中,用户可以同时选择多个关系进行批量编辑,并在最后一次性应用所有更改,减少了与数据源之间的往返次数,从而提高了性能。
2.2.3 多选编辑

参考《设置公共属性》

在"模型"视图中,按住 Ctrl 键同时选择多个表,这些表将在建模视图中突出显示。此时,在"属性"窗格中对属性的更改会应用到所选择的所有表中,比如设置"已隐藏"选项为是。隐藏的字段,数据仍然会存在,只是不在"数据"窗格上显示。

也可通过按住 Ctrl 键并选择多条连接线来多选关系,然后在"属性"窗格中同时编辑多个关系的公共属性,并通过"应用更改"一次性处理这些更改。

2.2.4 关系自动更新设置

在 Power BI 中,你可以管理报告和模型中关系的处理和自动调整方式。只需要选择"文件">"选项和设置">"选项",然后在左侧窗格中选择"数据加载",此时会出现"关系"选项,一共有三种:

  1. 在首次加载时从数据源导入关系(默认勾选):勾选时,Power BI 会在首次加载数据时自动检查并应用数据源中定义的关系,这使你可以快速开始使用模型,而无需自己查找或定义这些关系。
  2. 在刷新数据时更新或删除关系(默认不选) :勾选时,如果语义模型刷新时检测到数据源中关系发生了变化或被移除,Power BI会相应地更新或删除它们。   警告:如果你的模型中使用了依赖关系的行级安全(RLS),为了确保数据的安全性,建议不要启用此选项RLS设置通常依赖于数据模型中定义的关系,因为这些关系决定了不同表之间的数据如何关联和筛选。一旦你勾选了此选项,Power BI 会在数据刷新时自动更新或删除数据模型中的关系。一旦这个关系正是 RLS 设置所依赖的,原有的行级安全规则可能无法正常工作,导致某些用户能够访问到他们本不应访问的数据行。这会使得你的数据模型变得不安全,存在数据泄露的风险。
  3. 在数据加载后自动检测新关系(默认勾选):勾选后,Power BI 会在导入数据后,尝试为你自动查找并创建关系,包括"基数"、"交叉筛选方向"和"激活此关系"等选项。如果 Power BI Desktop 无法以高置信度确定存在匹配项,则不会创建关系。

2.3 优化数据模型

参考《优化数据模型》

2.3.1 隐藏表或字段

在 Power BI Desktop 中,如果数据包含报告和可视化任务不需要的字段,可以将其隐藏。如果当前视觉对象中使用了隐藏字段,数据仍然会存在,只是不在"数据"窗格上显示。

  1. 在报表视图中,右键单击该列并选择隐藏
  2. 右键单击模型视图中的表或列,选择在报表视图中隐藏


产品编号字段隐藏后,数据窗格中不再显示此字段

隐藏表对于使用 Power BI 的高级用户或开发者来说仍然是可访问的,他们可以通过编写 DAX 表达式来引用这些隐藏的表和字段。但在报表视图中,该表及其字段将不会显示在字段列表中,也就无法将它们添加到报表中。

对于最终用户而言,隐藏表可以简化报表界面,使得用户只看到与他们的分析相关的字段。但是隐藏表并不是一种安全机制。它不会限制用户访问数据的权限。如果需要控制数据访问权限,应该使用行级安全性(RLS,后面会讲)或其他安全措施。

总的来说,将表设置为隐藏主要是为了改善报表设计和用户体验,而不是作为一种安全措施。隐藏的表仍然可以通过 DAX 公式访问,因此不会影响数据模型的完整性或功能性。

2.3.2 可视化数据排序

按列排序工具可以帮助确保数据按照预期的顺序显示。在表格视图中,选定某个表格的某一列,在功能区会有列工具选项卡,可用于列排序。比如,月份名称默认按字母排序,从 列工具 选项卡中选择 按列排序,选择MonthNo为排列字段, 即可按预期对月份进行排序。

2.3.3 数据格式

在"数据"窗格中选择该列,然后从 列工具选项卡上的 格式 下拉菜单中选择一个格式选项。所创建的任何显示该字段的视觉对象都会自动更新。

2.4 行级安全性 (RLS)

参考《Power BI 行级别安全性 (RLS)》

2.4.1 定义角色和规则

行级安全性(Row-Level Security,RLS)在 Power BI 中用于限制特定用户的数据显示,通过在行级别设置过滤器(一般是DAX公式)来控制数据访问权限。

假设你是一个销售团队的成员,现在两个表:Sales(销售表)和 Customer(客户表),你的角色被设置为只能看到美国的客户数据。那么当你查询客户表时,系统会自动用公式= Customers[Country] = "USA"来过滤数据,你只能看到美国客户的行。

2.4.1.1 角色创建步骤
  1. 导入数据或配置 DirectQuery 连接:首先,将数据导入 Power BI Desktop 报表,或者为数据源配置 DirectQuery 连接。
  2. 打开"管理角色"窗口 :从"建模"选项卡中选择"管理角色"。
  3. 创建新角色:在"管理角色"窗口中,选择"新建"来创建一个新角色。
  4. 命名角色 :在"角色"下,为角色提供一个名称并选择进入。不能使用逗号定义角色,例如 London,ParisRole
  5. 选择要应用 RLS 过滤器的表:在"选择表"下,选择要应用行级安全过滤器的表。
  6. 定义过滤器数据 :有两种方式:
    • 使用默认的编辑器定义角色的过滤条件,表达式将返回 true 或 false 值。
    • 使用DAX 编辑器 编写DAX公式来定义角色,表达式同样返回值 true 或 false,例如:([Rerion] == "West"||[Country] == "United states") && Group == "A"
  7. 保存角色定义:定义好角色后,选择"保存"。
2.4.1.2 注意事项
  1. 通过使用usernameuserprincipalname 并配置正确的表关系,可以在 Power BI Desktop 中启用动态安全性。默认编辑器无法支持此功能,需使用DAX 表达式来定义。

    • 在 Power BI Desktop 中,username返回格式为 DOMAIN\User,userprincipalname返回格式为 [email protected]
    • 在 Power BI 服务和Power BI Report Server 中,两者都返回用户的 User Principal Name (UPN),类似于电子邮件地址。
  2. 两种编辑器可以随时切换,所做的更改在切换时会保留(如果可能的话)。

  3. 如果在 DAX 编辑器中定义的角色无法在默认编辑器中定义,尝试切换回默认编辑器时,会收到警告,提示切换编辑器可能会导致一些信息丢失。为了保留这些信息,可以选择"取消",并继续仅在 DAX 编辑器中编辑该角色。

  4. 在 Power BI Desktop 中无法将用户分配到角色。用户分配是在 Power BI 服务中完成的。

2.4.1.3 RLS的筛选方向

行级安全性筛选默认使用单向筛选,无论关系是设置为单向还是双向。

当启用双向关系时,Power BI 提供了一个额外的属性,允许在应用 RLS 规则时使用双向过滤,方式是选择此关系并勾选"在两个方向上应用安全筛选"的复选框。需要注意的是,如果一个表参与了多个双向关系,只能为其中一个关系选择此选项。当在服务器级别实现了基于用户名或登录 ID 的动态行级安全性时,应选择此选项。

例如,当 Sales表 和 Customer 表之间的关系是双向的,那么当应用 RLS 规则时,过滤器不仅会从 Customer 表传递到 Sales 表,还会从 Sales 表反向传递到 Customer 表,从而确保数据的一致性和安全性。这样即使只是在客户表中启用行级过滤,在销售表中,也只有与美国客户相关的订单数据可以被看到------RLS会根据关系传递过滤条件

要注意的是,行级安全性公式的作用是"过滤",而不是"完全隐藏",它如果用户属于另一个角色,而那个角色的公式允许看到这些数据,那么他们还是能看到的。

2.4.2 在 Power BI 服务中管理角色
  1. 打开工作区中的语义模型:在 Fabric 中,打开保存语义模型的工作区。
  2. 选择"安全"选项:悬停在语义模型名称上,选择"更多选项"菜单,然后选择"安全"。
  3. 管理角色成员 :在角色级别安全页面,可以为已创建的角色添加成员。
    • 添加成员 :可通过输入用户名、用户电子邮件地址,或安全组组名,向角色添加成员。支持的群组类型包括 分发组(Distribution Group)、邮件启用组(Mail-enabled Group)和 Microsoft Entra 安全组(Microsoft Entra Security Group)。Microsoft 365 群组无法添加到角色中。

    • 移除成员:角色名称旁的括号中会显示当前成员数量(例如:Role Name (3) 表示该角色有 3 个成员)。通过选择成员名称旁边的"X"来移除他们。

请注意:只有在 Power BI Desktop 中定义了模型的RLS并将其发布到PowerBI服务,才能在PowerBI服务中为成员分配安全角色,否则会弹出:

验证角色:你可以通过测试角色来验证所定义的角色是否在 Power BI 服务中正常工作。注意:仪表板(Dashboards)不支持此功能。

  1. 启用角色 :在角色旁边,选择"更多选项", 选择"以角色测试"
  2. 查看测试结果 :在页面标题中会显示正在应用的角色。可以通过选择"当前以...查看"来测试其他角色或特定人员。
  3. 返回正常模式 :选择上一步,即可返回正常模式。
  • 以角色测试"功能不适用于启用了单点登录(SSO)的 DirectQuery 模型。
  • 并非报表的所有方面都可以在"以角色测试"功能中进行验证,包括 Q&A 可视化、快速洞察(Quick insights)可视化和 Copilot。
2.4.3 在 Power BI 中使用 RLS 与工作区

RLS只对工作区中被设置为查看者(Viewer)角色的成员起作用,即使该查看者有权限编辑语义模型,RLS 依然有效。而管理员(Admin),成员( Member), 或贡献者(Contributor)这些有编辑权限的角色不受 RLS 限制。所以,想让 RLS 生效,需要将其设置为"查看者"。更多内容详见《工作区中的角色》

2.4.4 注意事项和限制
  • 如果之前在 Power BI 服务中定义了角色和规则,需要在 Power BI Desktop 中重新创建它们,否则之前的规则将失效;
  • 只能在通过 Power BI Desktop 创建的语义模型上定义 RLS。对于通过 Excel 创建的语义模型,需要先将其转换为 Power BI Desktop (PBIX) 文件;
  • 只支持导入和 DirectQuery 连接。比如对于 Analysis Services数据源 ,如果将数据导入 Power BI Desktop,则可以。如果使用实时连接,则需要在本地 Analysis Services 模型中定义 RLS。
  • 服务主体不能添加到 RLS 角色中,因此 RLS 不适用于使用服务主体作为最终有效身份的应用程序;
  • "以角色测试"/"以角色查看"功能不适用于启用了单点登录 (SSO) 的 DirectQuery 模型;
  • "以角色测试"/"以角色查看"功能仅显示来自语义模型工作区的报表,且不适用于分页报表。
2.4.5 常见问题解答
  • RLS 可以限制用户访问的列或度量吗? 不可以,如果用户可以访问某行数据,他们可以看到该行的所有列数据。要限制对列和列元数据的访问,考虑使用对象级安全性
  • RLS 可以隐藏详细数据但提供对可视化中汇总数据的访问吗? 不可以,你保护的是单个数据行,但用户始终可以看到详细数据或汇总数据。
  • 用户可以属于多个角色吗? 可以,用户可以属于多个角色,这些角色是累加的。例如,如果用户同时属于"销售"和"市场"角色,他们可以看到这两个角色的数据。

扩展阅读:

2.5 相关 DAX 函数

函数名称 功能描述 语法格式
RELATED 从关系的"一"侧检索值。 RELATED(<column>)
RELATEDTABLE 从关系的"多"侧检索表行。 RELATEDTABLE(<tableName>)
USERELATIONSHIP 允许计算使用非活动关系。通过修改特定非活动关系的权重来影响其使用。在模型包含角色扮演维度表时非常有用,也可以用于解决筛选路径的不明确性。 USERELATIONSHIP(<columnName1>, <columnName2>)
CROSSFILTER 修改关系的交叉筛选方向(单向或双向),或者禁用筛选器传递。在需要在特定计算中更改或忽略模型关系时非常有用。 CROSSFILTER(<columnName1>, <columnName2>, <filterDirection>)
COMBINEVALUES 将两个或更多文本字符串合并成一个字符串。在 DirectQuery 模型中,当表属于同一源组时,用于支持多列关系。 COMBINEVALUES(<delimiter>, <expression>, <expression>[, <expression>]...)
TREATAS 将表表达式的结果作为筛选器应用到不相关的表的列中。在高级场景中,用于在特定计算期间创建虚拟关系。 TREATAS(table_expression, <column>[, <column>[, <column>[,...]]]})
父函数和子函数 一组相关的函数,用于生成计算列以自然化父子层次结构。然后可以使用这些列创建固定层次结构。 取决于具体的函数

2.6 源组

2.6.1 源组的划分

Source Group(源组) 是根据数据存储位置和连接类型划分的逻辑分组,用于区分不同数据源或存储模式的表, 每个独立的数据源或存储模式会被归为一个 源组。例如:

  • 所有通过 Import 模式加载的表都属于一个源组(对于 Import 模型,其所有数据都存储在 Vertipaq 缓存中)。
  • 每个独立的 DirectQuery 连接 会形成各自的源组。例如:连接到 SQL Server 的表和连接到 Oracle 的表属于不同源组。
  • 双模式(Dual)表根据其当前激活的存储模式归属到对应的源组。
2.6.2 单源模型和复合模型

根据以上概念,PowerBI可分为单源模型和复合模型:

  1. 单源模型 (纯 Import 或纯 DirectQuery):所有表属于 同一个源组,同一 源组内的表共享相同的数据存储位置, 比如同一个 Vertipaq 缓存,或同一数据库连接。

  2. 复合模型 (Composite 模型):混合存储模式或多数据源,这包括:

    • Import 模式的表:归入 Vertipaq 对应的源组。

    • DirectQuery 模式的表:每个独立的数据库连接形成单独的源组。

    • 双模式(Dual)表:根据当前查询需求动态归属到 Import 或 DirectQuery 的源组。

下面是 Composite 模型的一个示例:

在此示例中,Composite 模型由两个源组组成:Vertipaq 源组(三个表)和 DirectQuery 源组(两个表)。 该示例中存在一个跨源组关系,即将 Vertipaq 源组中的表关联到 DirectQuery 源组中的表。

2.6.3 源组对关系评估的影响:
  1. 同一 源组的关系(Intra-Source Group):关系两端表属于同一数据源或存储模式。

    • 性能优势:数据在相同存储位置,查询优化更高效(如利用 Vertipaq 的列式存储索引)。
    • 数据完整性保障:Power BI 可自动验证"一对多"关系的"一"端唯一性,确保逻辑正确性。
  2. 跨源组关系(Cross-Source Group):关系两端表来自不同源组(如 Import 表关联 DirectQuery 表)。

    • 性能损耗:需跨不同存储位置协调数据(如内存计算与实时数据库查询),会导致额外的网络延迟和数据源查询开销。
    • 数据一致性风险:不同 Source Group 的数据更新频率或快照时间可能不一致,导致关联时数据不匹配。
    • 约束限制:Power BI 无法验证跨源关系的"一"端唯一性,需依赖人工确保数据建模正确。

2.7 关系评估

从评估的角度来看,模型关系可分为常规关系(Regular Relationships)和有限关系(Limited Relationships)。此关系属性不可配置,而是是从两个关联表的基数类型和数据源推断出来的,两者的主要区别在于数据的完整性和关系的复杂性,会影响模型性能(过滤器的传递和查询性能)及数据完整性受损时的后果。

2.7.1 常规关系

所有同一数据源组中的一对多或一对一关系都是常规关系。一侧的唯一性保证了数据的引用完整性,即每个"多"侧的值都能在"一"侧找到对应的匹配项。下面的示例中有两个常规关系,均标记为"R":

2.7.1.1 数据结构(Data Structure)

对于 Import 模型,其数据存储在 Vertipaq 引擎缓存中,Power BI 会在数据刷新时为每个常规关系创建一个索引化的数据结构(由所有列到列值的索引映射组成),存储了"一"端列的唯一值与"多"端列值的对应关系,其用途是在查询时加快表的连接操作(类似预计算 JOIN 的映射关系)。

例如Import 模式下的 Product 表与 Sales 表存在一对多关系(ProductID 为键),数据刷新时,Power BI 会为 Product[ProductID] 构建索引,记录每个 ProductID 对应的所有 Sales 行。查询时,直接利用该索引快速定位关联数据,避免实时扫描全表。

2.7.1.2 表扩展(Table Expansion)

表扩展是指在查询时,Power BI 会通过JOIN 操作,将相关表的列合并到一个虚拟表中,以便进行计算(包含基础表的列以及相关表的列)。 对于 Import 表,表扩展在查询引擎完成,对于DirectQuery 表,表扩展在发送到源数据库的原生查询中完成(前提是未启用"假设引用完整性"属性)。JOIN方式 有两种:

  1. LEFT OUTER JOIN一对多关系中 ,表扩展 使用LEFT OUTER JOIN将数值从"多"侧扩展到"一"侧。如果"多"侧没有匹配值,"一"侧补上空值(即空白虚拟行,仅适用于常规关系)。
  2. FULL OUTER JOIN一对一关系中 ,表扩展使用FULL OUTER JOIN 语义,无匹配时可在两侧均补上空值。
  • 空白虚拟行代表引用完整性违规(没有完全匹配),理想情况下不应该存在,可通过清理或修复源数据来消除。
  • 非活动关系也会进行扩展,即使它并未被直接用于计算。
  • 双向筛选关系对表扩展没有影响,因为表扩展 的行为主要与关系的方向性有关,而与关系的筛选器传播方向无关。无论单向还是双向,数据都是从"多"侧扩展到"一"侧。

例如,有以下三个关联的表------Category 表→Product 表→Sales 表,都是一对多的关系,所有关系的两侧都有匹配的值,这意味着不存在引用完整性冲突。在查询时,Power BI 会创建一个扩展表,该表包含 Category、Product 和 Sales 表的所有列。这个扩展表是一个非规范化的数据视图,将三个表的数据整合在一起。

现在将向 Sales 表添加一个新行, ProductID 的值为 9,在 Product 表中没有对应的 ProductID 值,这是一个引用完整性冲突。在扩展表中, Category 和 Product 表的新行列值显示为 (Blank),表示这些列没有对应的值。

2.7.2 有限关系

当Power BI 无法确定"一"侧列包含唯一值时,关系为有限关系,原因包括:

  • 多对多关系(Many-to-Many Relationships):使用了多对多基数类型,即使一个或两个列中只包含唯一值。
  • 跨源组关系(Cross Source Group Relationships):关系跨越了不同的数据源组,仅适用于复合模型。

下面的示例中有两个有限关系,均标记为"L"。这两个关系分别是一对多跨源组关系,和 Vertipaq 源组中包含的多对多关系。

2.7.2.1 有限关系的限制

在有限关系中,数据查询的过程与常规关系有所不同。对于导入模型,有限关系不会创建数据结构。在有限关系中,永远不会进行表扩展。

  1. 为什么受限关系(Import模型)不能创建数据结构?
    • 基数类型限制:受限关系通常是多对多或跨源组,这些关系的复杂性导致无法在数据刷新时预先构建高效的索引映射(Vertipaq缓存结构)。
    • 跨源组问题:复合模型中,表可能分布在不同的数据库、文件或其他数据源,它们的数据结构和存储方式可能不同。Power BI 无法预先为这些跨源组的关系创建统一的数据结构
    • 引用完整性缺失 :有限关系无法保证"一"侧的唯一性,因此无法像一对多关系那样通过主键-外键映射创建确定性的索引。
  2. 为什么受限关系不能进行表扩展(Table Expansion)?

表扩展的本质是使用LEFT OUTER JOIN或FULL OUTER JOIN,生成包含所有可能的行(包括空白虚拟行)的虚拟表,以维护引用完整性。

有限关系中,无法确定哪一侧是唯一的主键表,因此无法安全应用外连接语义(如LEFT OUTER JOIN)。由于无法保证键值的唯一性,添加空白行可能导致数据逻辑错误(例如重复或歧义值)。另外,生成大量虚拟行会显著增加计算负担,尤其是当"多"侧表规模庞大时。取而代之的是使用INNER JOIN语义处理连接(仅返回匹配的行,不生成空白行)。

2.7.2.2 有限关系的查询方式

在有限关系中,Power BI 需要在查询时实时解析表连接并执行查询。具体来说:

  • 动态解析表连接:当用户在 Power BI 中执行一个查询时,查询引擎会首先解析查询请求,确定需要从哪些表中获取数据以及这些表之间的连接关系。对于有限关系,由于缺乏预构建的数据结构,因此需要在查询时动态解析表连接。
  • INNER JOIN:使用内连接合并相关表,仅保留匹配的行,不会像常规关系那样添加空白虚拟行来补偿引用完整性冲突。
  • 过滤、聚合,返回结果:在表连接之后,查询引擎会应用任何指定的过滤条件和聚合操作,然后将处理后的结果返回给用户。由于没有表扩展,结果集仅包含查询中明确请求的列和行,没有额外的虚拟行或列。

总之,有限关系在查询时需要 Power BI 动态解析表连接,使用 INNER JOIN 语义确保数据的匹配性,并在连接后的数据上应用过滤和聚合操作,最终返回符合查询要求的结果集。有限关系在查询时需要更多的计算资源,因为没有预建的索引映射来加速连接,但避免了引用完整性风险。

在 Power BI Desktop 模型视图中,有限关系通过基数指示器后面的类似于圆括号的标记 ( ) 表示。

2.7.2.3 其它限制
  • RELATED函数不可用:因无法确定"一"侧的确定性,无法通过RELATED跨表获取值。
  • RLS限制:行级安全性(RLS)需在特定拓扑结构(明确的"一"端,且同源组)中生效,受限关系的动态连接可能破坏过滤上下文。

行级安全性(RLS)通过在模型中定义角色和 DAX 过滤规则,限制用户可见的数据范围,过滤器的传播依赖于表之间的明确关系路径。例如,现在有Sales 表和 Products 表,通过规则 Sales[Region] = "North" 会让用户仅看到"North"区域的销售数据。假设它们是常规关系,那么RLS 对 Sales 的过滤会通过表扩展(LEFT OUTER JOIN)传播到 Products,确保用户只能看到与"North"区域相关的产品。

如果是受限关系,无预定义的"一"端,就无法明确确定过滤器的传播方向;动态解析连接,意味着Power BI 会先执行INNER JOIN连接,再应用过滤器。动态连接破坏了 RLS 预期的"过滤上下文传递路径",导致过滤器无法按预期扩散到相关表,用户可能看到与"North"区域无关的产品(如果这些产品在连接结果中存在)。

2.7.2.4 最佳实践
  • 优先使用常规关系:确保数据模型符合星型/雪花架构,避免多对多和跨源组设计,尽量统一存储引擎。

  • 修复引用完整性:清理数据源中不匹配的键值,减少受限关系的使用场景。

  • 隔离敏感数据:避免在RLS关键路径中使用受限关系,比如,对需严格遵循RLS规则的表(如用户表、销售表),使用常规关系以确保筛选器正确传播。

  • 显式传递过滤器,或使用双向筛选(谨慎!):若必须使用受限关系,可在"多"侧表中添加显式DAX筛选器,模拟RLS效果。或者在模型关系设置中启用双向筛选(仅限必要场景),但需注意性能问题和循环依赖风险。

    c 复制代码
    //在 Products 表中添加计算列,强制依赖 Sales 表的 RLS 过滤
    FilteredProduct = 
    IF (
        RELATED ( Sales[Region] ) = "North",
        Products[ProductName],
        BLANK ()
    )
  • 性能监控:通过Power Query Profiler分析查询性能,必要时优化数据模型或使用混合模式。检查 ​DAX Studio 中的筛选器上下文,确认RLS是否生效。

特性 常规关系 受限关系
数据特性 同源组一对一、一对多 同源组多对多、跨源组(复合模型)
数据结构 刷新时预构建索引映射 无预构建结构,动态解析
表扩展 支持(LEFT/FULL OUTER JOIN) 不支持(使用INNER JOIN)
空白行处理 添加空白行(未知成员) 过滤不匹配行
RLS 过滤器传播 自动沿关系方向传递 可能中断或失效
安全风险 高(需严格测试 RLS 效果)
性能 高(利用预构建结构,推荐) 较低(动态解析)
2.7.3 歧义路径解析机制(双向关系)

在Power BI模型中,​双向关系(Bi-directional Relationships)​ 允许数据在两个表之间自由流动,但也会带来传播路径过多,以及路径不明确的问题。此时,Power BI 根据优先级和权重来选择过滤器传递路径。

2.7.3.1 优先级(Priority Tiers)

Power BI将按以下顺序选择路径(优先级从高到低排列),在匹配到第一条规则时生效。

优先级 路径描述
1 只包含一对多单向关系(One-to-Many)的路径。
2 包含一对多和多对多( many-to-many)关系的路径。
3 只包含多对一关系(Many-to-One)的路径。
4 从源表到中间表是一对多关系,从中间表到目标表是多对一关系的路径。
5 从源表到中间表是一对多或许多对多关系,从中间表到目标表是多对一或许多对多关系的路径。
6 其他复杂路径。
2.7.3.2 权重

​  优先级相同的情况下,PowerBI会选择权重较高的路径来传播。默认情况下各关系权重相等,但你可以通过USERELATIONSHIP函数显式设置关系的权重,嵌套调用时,​最内层权重最高。

  • 路径中所有关系的最大权重值决定该条路径的最终权重值
  • 若优先级和权重均相同,但路径不同,Power BI将抛出"Ambiguous Path"错误。

示例代码:

c 复制代码
Product Sales = 
CALCULATE(
    CALCULATE(
        SUM(Sales[SalesAmount]), 
        USERELATIONSHIP(Sales[ProductID], Product[ProductID])    //高权重
    ),
    USERELATIONSHIP(Inventory[ProductID], Product[ProductID])   // 低权重
)
2.7.4 不同关系之间的性能评估

不同模型关系在过滤传播(filter propagation)时性能如下(从高到低):

  1. 源组内一对多关系;
  2. 通过中间表实现的多对多模型关系,并且涉及至少一个双向关系;
  3. 多对多基数关系(Many-to-many cardinality relationships)。
  4. 跨源组关系(Cross source group relationships)

使用中间表和双向关系的多对多模型(第二种)性能优于直接的多对多关系(第三种)​,详见本文3.10章节。

2.8 模型约束(无法确定各字段之间的关系)

参考《故障排除》

Power BI 会尝试通过数据模型中的关系来推断字段之间的相关性,以便在可视化中显示相关数据。然而,有时这些推断并不明确,可能会导致可视化中出现错误,提示无法确定这些列之间的关系。下面是一个具体的示例模型:

Power BI通过以下规则确定字段间关系:

  • 显式关系:基于模型中定义的关系(如Product[Key] ↔ Purchases[ProductKey])
  • 隐式约束:当无显式度量约束时,自动添加COUNTROWS(关联表)>0作为过滤条件
  • 用户约束:优先使用报表中配置的汇总列(如SUM(Purchases[Qty]))或模型度量(如[Total Sales])
2.8.1 标准星型模型(无度量约束)​

模型图的右侧,包含 Vendor(供应商)、Purchases(采购)和 Product(产品)三张表。Purchases 是事实表(Fact Table),Vendor 和 Product 是维度表(Dimension Tables)。维度表与事实表之间的关系是 一对多(1-to-Many)。

对于产品 X销售额是多少,供应商 Y的销售额是多少,供应商 Y 销售哪些产品,模型都可以回答。通过 Purchases 表关联 Product 和 Vendor。Power BI 会隐式添加一个约束条件 CountRows(Purchases) > 0,以确保返回的数据是有意义的。这样可以避免显示从未有过销售的产品和供应商的组合(这些组合对分析没有意义)。

2.8.2 星型模型+度量约束

此时向报表中添加汇总度量值(如汇总列的总和、平均值或计数),这种情况下,Power BI 会尝试返回对用户提供的约束有意义的组合。此时,PowerBI不需要再添加自己的隐式约束,因为用户提供的约束已经足够。

sql 复制代码
SELECT 
    Product[Color], 
    Vendor[Name], 
    SUM(Purchases[Qty]) 
FROM 
    Purchases 
WHERE 
    SUM(Purchases[Qty]) > 0 -- 用户约束优先
GROUP BY 
    Product[Color], Vendor[Name]
2.8.3 非星型模型,无约束(危险结构)

对于模型的中间部分,包含 Sales(销售)、Product(产品)和 Purchases(采购)三张表。这里有一个维度表(Product)和两个事实表(Sales 和 Purchases)。 Purchases 和 Sales之间是间接的多对多(Many-to-Many)关系。

如果 Power BI 尝试返回这两个表的所有组合(比如Sales[CustID]与Purchases[VenID]),由于 无法找到一个明确的约束条件,所以会产生大量的无意义数据(交叉连接)。此时Power BI 会在可视化中报错,提示无法推断字段之间的关系。

解决方案:

  1. 创建中间表SalesPurchases,将模型重构为星型模型。
  2. 添加约束列:见下一节
2.8.4 非星型模型+度量约束

在上一个场景的基础上,以汇总列(例如 Count of Product[ProdID])或模型度量值 (Sales[Total Qty]) 的形式添加用户提供的约束。在这种情况下,Power BI 将用户的约束视为 Power BI 需要应用的唯一约束,然后返回满足该约束的非空组合(用户已将 Power BI 引导至所需的方案)。

2.8.5 混合约束

当用户仅对部分数据内容提供度量约束,会使用混合约束。比如只对事实表1使用了度量:SUM(FactTable1[Value]),此时:

  • 对FactTable1与维度表A应用度量约束

  • 对FactTable2与维度表B尝试添加隐式约束COUNTROWS(FactTable2)>0。若FactTable2无数据满足COUNT>0,则会报错:

    解决方案: 将混合约束转换为模型度量。

    c 复制代码
    [Combined Metric] = 
    IF(
        ISBLANK(SUM(FactTable1[Value])),
        COUNTROWS(FactTable2),
        SUM(FactTable1[Value])
    )
2.8.6 解决办法

当出现"无法确定字段之间的关系"错误时,可以尝试以下步骤:

  1. 检查数据模型:模型是否适合你想要回答的问题?是否可以调整表之间的关系?是否可以避免创建间接的多对多关系?
  2. 调整关系 :比如将倒 V 形架构转换为两个表,并使用直接的多对多关系(参考《Power BI Desktop 中的多对多关系》);
  3. 添加约束:以汇总列或模型度量的形式在可视化中添加约束。

2.9 关系问题解决指南

参考《关系问题解决指南》

当你在 Power BI 报表中结合多个表的数据来创建可视化效果时,如果它没有正确显示结果(或者根本没有显示任何结果),很可能是表之间的关系设置有问题,你可以按照以下清单进行问题排查:

  1. 切换可视化为表格或矩阵视图,或打开"查看数据"窗格,这样可以更直观地查看查询结果,便于发现数据问题;

  2. 如果查询结果为空,切换到表格视图,检查表格是否已加载数据行;

  3. 切换到模型视图,查看表格间的关系及其属性,确认表之间存在关系;

  4. 检查基数是否设置正确,避免"多"侧列包含唯一值而被错误设置为"一"侧的情况。

  5. 确保关系处于活动状态(实线)。

  6. 验证关系的交叉过滤方向,是否符合你的分析需求

  7. 在模型视图中选择关系线,或者将光标悬停在关系线上,检查关系线两端的列是正确关联的(避免对不相关的列创建了关系)

  8. 关联的列需要有相同或兼容的数据类型,这样才能确保过滤器能够正确匹配数据并进行传播

  9. 切换到表格视图,验证相关列中是否存在匹配值。

故障排除指南 可能的原因
可视化无结果显示 • 模型尚未加载数据。 • 在筛选上下文中不存在数据。 • 强制实施了行级安全(RLS)。 • 表之间不传播关系------请遵循上述检查清单。 • RLS 强制实施,但未启用双向关系进行传播
可视化对象中,每个分组显示相同值 • 不存在关系。 • 表之间不传播关系------请遵循上述检查清单。
可视化结果显示,但结果不正确 • 可视化设置错误。 • 度量值计算逻辑有误。 • 模型数据需要刷新。 • 源数据不正确。 • 关系列关联错误(例如,ProductID 列映射到 CustomerID 列)。 • 关系的"一"侧列包含重复值。
在 Power BI 报表中,出现了空白的分组或筛选器选项(BLANK),但原始数据并没有空值(BLANK)。 • 普通关系中,"多"侧列包含不匹配值,为了维护数据完整性,填充了BLANK • 普通一对一关系中,相关列包含 BLANK • 非活动关系的"多"侧列存储 BLANK,或有未存储在"一"侧的值。
可视化数据缺失 • 应用了不正确的/意外的筛选器。 • 强制实施了行级安全(RLS)。 • 有限关系中,相关列存在 BLANK 或数据完整性问题 ,导致不匹配值被删除 • 两个 DirectQuery 表之间的关系被设置为假设引用完整性,但数据没有完全匹配,导致不匹配值被删除。
RLS 未正确强制实施 •表之间不传播关系------请遵循上述检查清单。 • RLS 已强制实施,但未启用双向关系进行正确传播

三、星型模型

3.1 维度表和事实表

星型模型(Star Schema)是数据仓库中一种简单且直观的模型,它将模型表分为维度表(Dimension Tables)和事实表(Fact Tables)。通过这种结构化的方式,能够高效地支持数据的筛选、分组和汇总,从而优化Power BI语义模型的性能和可用性。

  • 维度表(Dimension Table):包含描述性的信息,提供了用于分析的维度,例如,日期、产品、客户、地区等。维度表中的数据是规范化的,每个维度表都包含一个作为唯一标识符的关键列,以及其他用于数据筛选和分组的列。
  • 事实表(Fact Table)
    • 存储观察或事件:事实表用于存储具体的业务观察或事件,如销售订单、库存余额等,随着时间的推移不断增长。
    • 包含维度键和度量列:事实表事实表通常会有一个或多个外键列,用于引用维度表中的主键;以及用于数值汇总的度量列。
    • 维度键列决定了事实表的维度性,而维度键值则决定了事实表的粒度(比如日期维度可以是年-月-天)
    • 事实表通常以 f_Fact_ 作为前缀命名,以便于识别。

下面是一个简单的示例,在Adventure Works中,Sales表是事实表,其它五个表是维度表,通过模型关系连接,过滤器能直接或间接传递到Sales表。

3.2 星型模型的重要性

Power BI 语义模型 的设计目标是为报表可视化提供高效的数据查询支持(每个可视化都会生成一个查询,这些查询会被发送到Power BI的语义模型。),而星型模式的结构恰好满足这一需求:

  • 维度表 :用于 筛选(Filtering)分组(Grouping)
  • 事实表 :用于 汇总(Summarization)聚合(Aggregation)

表的类型 (维度表还是事实表)并非由用户手动设置,而是由 模型关系的基数 决定。常见的关系基数是一对多(one-to-many)或其逆向多对一(many-to-one)。在这种关系中,"一"侧总是维度表,而"多"侧总是事实表。

一个结构良好的模型设计应该是:

  • 表类型分离:包括维度表或事实表,避免将维度和事实混合在一个表中
  • ​一致性粒度:事实表应该始终以一致的粒度如按天、按月)加载数据。
  • 关系优化:建立正确数量的表,并确保关系合理。

星型模式的优势:

  • ​查询性能优化:星型模式通过减少表关联次数和简化数据结构,显著提升查询速度。
  • 模型清晰性:明确的表类型和关系使模型更易于理解和维护。
  • 支持复杂分析:通过维度表的筛选和事实表的汇总,支持多维数据分析

总结:星型模式是 Power BI 语义模型设计的核心原则之一,通过将数据分为 维度表事实表,并建立合理的关系,可以显著提升模型的性能和可用性。虽然遵循星型模式是推荐的最佳实践,但在实际项目中,设计者可以根据具体需求灵活调整,以实现最优效果。

四、维度表

参考《维度建模:维度表》《星型架构》

4.1 主键、外键、自然键、代理键、特殊键值

  • 主键:维度表通常有一个主键,用于唯一标识。这个主键可以是一个单一的字段,也可以是多个字段的组合(复合主键)。主键列不能包含空值(NULL),应当保持稳定。

  • 外键(Foreign Keys):外键用于引用其他维度表。

    • 外键列可以包含 NULL 值,表示该记录在参照表中没有对应的关联记录。
    • 维度表中的主键是相关事实表中的外键,用于引用该特定维度。
    • 外键可形成雪花维度,或者与外延维度相关。
    • 创建外键可以让建模工具(如 Power BI Desktop)自动检测并创建语义模型中表之间的关系。
  • 自然键(Natural Key) :是源系统中的键,用于将维度数据与源系统关联,有时也被称为业务键,即用于唯一标识业务实体的属性,例如产品名称,具体取决于业务逻辑和数据模型设计。

  • 代理键(Surrogate Key):是维度表中的单列唯一标识符,是维度表的主键,用于与维度模型中的其他表相关联 ,比如产品表中的ProductKey。在Power BI中,当维度表没有单一唯一列时,需要添加代理键以成为关系中的"一"侧,比如通过Power Query添加索引列来实现此要求。

    代理键的作用:

    • 隔离数据仓库与源数据变化 ,避免因源数据标识符冲突或变更影响数据仓库。
      源系统中的数据标识符可能会发生变化,如果数据仓库直接使用这些标识符,那么任何变化都可能导致数据仓库中的引用失效,从而引发数据不一致的问题。通过使用代理键,数据仓库可以避免这种情况,从而确保数据的一致性和稳定性。
    • 合并多个数据源(避免重复的标识符冲突)。例如,不同的部门或系统可能使用不同的代码来标识同一个产品。
    • 支持Type-2 SCD,实现历史数据追踪。每当维度数据发生变化时,都会在维度表中插入一条新的记录,同时保留旧记录,代理键为每条记录提供了一个唯一的标识符用于数据追踪。
    • 优化存储,减少事实表宽度:代理键通常使用整数数据类型,这可以优化存储空间,通过替换字符串类型的外键,还可以减少事实表的宽度。
  • 特殊维度键值(Special dimension members):可使用以下代理键值插入表示缺失、未知、不适用或错误状态的行。

键值 用途
0 缺失(在源系统中不可用)
-1 未知(在事实数据表加载期间查找失败)
-2 N/A(不适用)
-3 错误

4.2 维度属性、历史跟踪属性、审核属性、维度表大小

  1. 维度属性(Dimension Attributes) :是维度表中为相关事实表中的数值数据提供上下文的列 ,通常是文本列,例如 FirstName。维度属性用于在分析查询中进行筛选和分组 ,但不进行聚合。当人们提到按某个维度分析时,表明需要相应的维度属性。
  2. 历史跟踪属性(Historical Tracking Attributes):可选,用于记录源系统中特定变化发生的时间等信息,支持数据仓库准确描述过去,详见下文渐变维度(SCD)。
  3. 审核属性(Audit Attributes) :可选,用于记录维度记录的创建或修改时间、人员等信息,有助于诊断 ETL (Extract,Transform,Load)过程中的问题,例如AuditCreatedDate,AuditCreatedBy 等。

维度表的大小因业务需求而异。大型维度表可能包含数百万行和数百个属性,支持复杂的过滤、分组和历史分析。小型维度表可能仅有几条记录和少量属性,如查找表。对于大型维度表,数据处理需要合并、去重和标准化数据,并分配代理键。对于多个小型维度表,可考虑整合为杂项维度(Junk dimensions,见下文)。

4.3 规范化与非规范化

  1. 数据规范化(Normalization):规范化表是指将数据存储在多个表中,每个表只包含特定的主题或实体,通过键(如ProductKey)将它们关联起来。这种方式可以减少数据冗余、保证数据一致性(优化存储)。

    • 只存储键值 :在规范化表中,销售表只存储产品键(ProductKey),不直接存储产品的详细信息,而是通过外键关联独立的维度表(如产品表)来获取详细信息(如产品名称、类别、颜色、尺寸等)。
    • 减少数据冗余:通过将数据分解到多个表中,避免重复存储相同的信息。
    • 提高数据一致性 :如果产品信息发生变化(如产品名称更改),只需要更新产品表,因为数据只在一个地方存储。
  2. 非规范化(Denormalization) :本质是故意引入冗余(比如层次结构)以提高查询性能,尤其是在需要频繁连接多个表的情况下。例如在非规范化表中,销售表不仅存储产品键(ProductKey),还存储产品的详细信息(如产品名称、类别、颜色、尺寸等)。

维度 规范化 反规范化
数据冗余 高(冗余存储描述性字段)
更新效率 高(仅更新维度表) 低(需更新所有冗余字段)
查询性能 低(需多表关联) 高(单表快速读取)
适用场景 事务型系统(OLTP,日常事务的系统) 如银行交易、订单处理等需要频繁更新和维护的数据 分析型系统(OLAP,如数据仓库) 如财务报表生成、客户行为分析等需要服装数据分析的场景

在 Power BI 的星型模式设计中,​规范化是默认策略(使用 Power Query 拆分原始数据为星型模式),但需根据实际场景权衡是否反规范化。若模型复杂度过高(如多层雪花维度)或查询性能瓶颈显著,反规范化可作为优化手段。最终目标是 ​平衡数据一致性、存储效率和查询速度。

4.4 雪花维度

雪花维度(Snowflake Dimensions)是规范化的维度,将一个业务实体(比如产品)相关信息分解到多个相互关联的表中,因此整体模型结构看起来像一个雪花的形状。雪花维度适用于:

  • 维度极大,存储成本高于查询性能需求。
  • 需要将维度与更高粒度的事实相关联。
  • 需要在更高粒度上跟踪历史变化

例如,在Adventure Works数据仓库中,产品维度被分解为三个表:

  • ProductCategory:存储产品类别的信息
  • ProductSubcategory:存储产品子类别的信息。
  • Product :存储产品详细信息。

此时,你可以将模型设计为雪花维度,以便用户可以在报告中按不同粒度进行钻取和分析:

也可以将源表合并成一个单一的非规范化模型表(Snowflake 表),来提高查询性能,具体根据数据量和模型可用性要求权衡利弊。

特性 雪花模型 (Snowflake Schema) 非规范化模型 (Denormalized Table)
数据冗余 低:维度表被拆分为多个子表,高规范化
数据一致性 高(数据在多个表中只存储一次) 低(数据在多个地方存储,可能会导致不一致)
查询性能 较差(需要更多的表连接,查询较慢) 较好(简单的连接,查询速度快)
模型管理 复杂(需要管理更多的表和关系) 维度表和事实表直接相连,关系简单,结构清晰,易于理解和维护
用户体验 数据窗格中,用户会看到更多的表,这可能会使体验变得不那么直观。 简单直接
适用场景 小型到中型数据集 大型数据集
复杂查询支持 高(通过表之间的关系支持复杂查询) 可能限制(数据合并在一个表中可能限制某些复杂查询)

4.5 层次结构

维度表通常支持层次结构(Hierarchies),以在不同汇总级别探索数据,比如年-月-日层次。可通过三种方法将层次结构存储在维度中:

  • 单个非规范化的维度列
  • 雪花维度的多个相关表
  • 维度中的父子(自引用)关系

层次结构可分为均衡层次结构(所有成员具有相同数量的级别)、不均衡层次结构(基于父子关系,级别数量由维度行决定)和锯齿层次结构(成员的父级位于非直接上级级别)。

4.5.1 均衡层次结构(Balanced hierarchies)

均衡层次是最常见的层次类型,其特点是所有成员具有相同数量的层级(从根节点到叶节点的路径长度相同)。例如,在日期维度中的日历层次,包含年、季、月和日期等各级别。每个日期都属于某一年、某一季和某一月,因此从日期到年的路径长度是固定的,即每个日期都经过四个层级(日、月、季、年)。

下图示例中,销售区域是均衡层次结构,包含两个级别,即销售区域组(欧洲、北美洲、太平洋)和销售区域(法国、德国...)。

均衡层次的级别可以基于单个非规范化维度的列,或者由形成雪花维度的多个表构成。在非规范化维度中,表示较高级别的列包含冗余数据。

事实表通常与层次结构最低级别相关联,这样事实数据可以向上汇总到最高层次级别。事实数据也可以与任何级别相关,这由事实数据表的粒度决定。例如,销售事实表可能按日期级别存储,而销售目标事实表可能按年度级别存储。

4.5.2 非均衡层次结构(Unbalanced hierarchies,父子结构)

非均衡层次结构不太常见,其特点是基于父子关系,层次结构的级别数量由维度行决定,而不是由特定的维度表列决定。

员工层次结构是一个典型的不平衡层次示例,其中每个员工维度行与同一表中的一个汇报者行相关联。在这种情况下,任何员工都可能成为汇报者的上级,因此层次结构的不同分支可能具有不同数量的级别,具体取决于他们向谁报告。

不平衡层次的维度表,例如,销售员维度表,通常包含一个代理键(Salesperson_SK)和一个外键列(ReportsTo_Salesperson_FK ),用于引用主键列。对于不平衡层次,事实数据与维度的粒度相关联,例如,在汇总每个销售员的销售数据时,需要考虑单个销售员及其所有下属。

查询父子层次结构可能会很复杂且速度较慢,特别是对于大型维度。建议将层次结构自然化,即将层次级别存储为维度中的列,这样查询工具可以直接访问列数据而不需要递归地解析父子关系。以下是使用deepseek得到的完整示例:

  1. 原始销售员维度表结构:需要递归查询才能获得完整层级路径。比如,查询"赵六的所有上级"需要3次递归(王五→李四→张三),统计"张三团队总销售额"需要遍历所有下属层级。
sql 复制代码
Salesperson_Dim
| Salesperson_SK (代理键) | Salesperson_Name | ReportsTo_Salesperson_FK (外键) |
|------------------------|------------------|----------------------------------|
| 001                    | 张三             | NULL                             |  -- 顶级销售总监
| 002                    | 李四             | 001                              |  -- 张三的下属
| 003                    | 王五             | 002                              |  -- 李四的下属
| 004                    | 赵六             | 003                              |  -- 王五的下属
  1. 自然化处理后的结构使用PATH父子结构函数处理
sql 复制代码
Salesperson_Dim
| Salesperson_SK | Name  | Level1_SK | Level2_SK | Level3_SK | Level4_SK | Depth |
|----------------|-------|-----------|-----------|-----------|-----------|-------|
| 001            | 张三  | 001       | NULL      | NULL      | NULL      | 1     |
| 002            | 李四  | 001       | 002       | NULL      | NULL      | 2     |
| 003            | 王五  | 001       | 002       | 003       | NULL      | 3     |
| 004            | 赵六  | 001       | 002       | 003       | 004       | 4     |

查询性能对比示例:

▶ 原始结构查询(递归CTE):

sql 复制代码
WITH RECURSIVE Hierarchy AS (
  SELECT Salesperson_SK, Salesperson_Name, ReportsTo_Salesperson_FK, 1 AS Level
  FROM Salesperson_Dim WHERE Salesperson_SK = '004'  -- 赵六
  UNION ALL
  SELECT d.Salesperson_SK, d.Salesperson_Name, d.ReportsTo_Salesperson_FK, h.Level + 1
  FROM Salesperson_Dim d
  JOIN Hierarchy h ON d.Salesperson_SK = h.ReportsTo_Salesperson_FK
)
SELECT * FROM Hierarchy;

▶ 自然化后查询(直接连接):

sql 复制代码
SELECT 
  f.Salesperson_SK,
  l1.Name AS Level1_Manager,
  l2.Name AS Level2_Manager,
  l3.Name AS Level3_Manager
FROM Salesperson_Dim f
LEFT JOIN Salesperson_Dim l1 ON f.Level1_SK = l1.Salesperson_SK
LEFT JOIN Salesperson_Dim l2 ON f.Level2_SK = l2.Salesperson_SK
LEFT JOIN Salesperson_Dim l3 ON f.Level3_SK = l3.Salesperson_SK
WHERE f.Salesperson_SK = '004';
维度 原始父子结构 自然化结构
查询复杂度 需要递归查询,复杂度O(n) 简单JOIN操作,复杂度O(1)
执行时间 10万行数据约800ms 10万行数据约50ms
聚合计算 需要多次递归汇总 直接WHERE Level1_SK=001即可
索引优化 难以有效索引 可为每个Level列创建B-tree索引
存储空间 更小(仅存储父键) 更大(需要存储层级路径)

实际业务应用场景:

  1. 快速路径查询:直接通过Level列获取任意节点的完整汇报链

  2. 高效聚合 :统计某分支的销售总额只需:

    sql 复制代码
    SELECT SUM(s.SalesAmount)
    FROM Sales_Fact s
    JOIN Salesperson_Dim d ON s.Salesperson_SK = d.Salesperson_SK
    WHERE d.Level1_SK = 001  -- 张三团队
  3. 层级分析 :快速计算各层级的销售指标

    sql 复制代码
    SELECT 
      l1.Name AS TopManager,
      l2.Name AS MidManager,
      SUM(s.SalesAmount) AS TotalSales
    FROM Sales_Fact s
    JOIN Salesperson_Dim d ON s.Salesperson_SK = d.Salesperson_SK
    LEFT JOIN Salesperson_Dim l1 ON d.Level1_SK = l1.Salesperson_SK
    LEFT JOIN Salesperson_Dim l2 ON d.Level2_SK = l2.Salesperson_SK
    GROUP BY l1.Name, l2.Name

维护注意事项:

  1. 需要ETL过程在数据加载时预计算层级关系
  2. 当组织结构变化时,需要批量更新相关路径
  3. 建议设置定期重建层级(如每天凌晨)
  4. 最大层级数需要留有余量(例如预设6列应对未来扩展)

这种方法本质上是将图结构数据(树形)转换为维度平面化存储,用空间换时间,是数据仓库处理层次结构的经典优化方案。

4.5.3 不规则层次结构(Ragged hierarchies)

不规则层次结构是指层次结构中某个成员的父级并非是其直接上级(存在跳级) 。比如在地理层次结构中,新西兰既没有州也没有省,在这种情况下,缺失级别的值会重复父级的值(StateProvince列将重复country/region 的值)。

4.6 渐变维度(SCD)

渐变维度(Slowly Changing Dimensions,SCD)是指那些随着时间的推移而逐渐发生变化的维度数据,例如客户的联系信息,其电子邮件、地址和电话号码都会逐渐变化。如果维度属性经常变化,比如股票价格,则是快速变化的维度。

星型架构中有两种常见的SCD,Type 1 SCD和Type 2 SCD。维度表可以是Type 1或Type 2,也可以是两种都有。

4.6.1 Type 1 SCD(覆盖旧值)

Type 1 SCD直接使用新值覆盖旧值,适用于存储补充信息的列,如客户的电子邮件地址或电话号码,一般只需要保持最新值就行。

通过非增量刷新Power BI模型维度表,可以实现Type 1 SCD的效果,确保加载最新的值。

4.6.2 Type 2 SCD(插入新值,保留旧值)
4.6.2.1 简介

Type 2 SCD通过在维度表中插入新行来记录维度成员的时间版本,保留旧版本,其中包含:

  • 历史追踪属性列 :比如IsCurrent列,用于标识每个成员是否为当前版本,以便轻松筛选当前维度成员。
  • 版本有效期列:存储每个版本的开始日期和结束日期。如果发现某个维度成员的属性发生了变化,就需要标记当前版本为过期,然后插入一个新的成员版本,其开始日期从上一个版本的结束日期开始。
  • 代理键列:由于存储多个版本时自然键会重复,因此需要代理键用于唯一标识。

例如,在Adventure Works中,每个销售人员被分配到一个销售区域。当销售人员更换区域时,必须创建该销售人员的新版本,以确保历史事实仍然与之前的区域相关联。

除了存储销售人员及其关联区域的版本,该表还应包括开始和结束日期值,以定义时间有效性。当前版本可定义一个空的结束日期(或12/31/9999),这表示该行是当前版本。

该表还必须有一个代理键,因为此时的业务键------employee ID,不再是唯一的。还可以包含描述成员和版本的标签属性,比如"Lynn Tsoflias (Australia)",必要时包含有效日期。通过这种设计,Power BI语义模型应支持查询成员的历史数据,无论其变化如何。

注意:应当避免在维度表上有太多 Type 2 SCD 变化,因为这可能导致版本过多,使分析师难以理解。如果某个属性变化频繁,比如销售区域变化频繁,可以将其作为维度键存储在事实表中。

4.6.2.2 SQL示例
sql 复制代码
CREATE TABLE d_Salesperson
(
    <...>

    --历史追踪属性 (SCD type 2)
    RecChangeDate_FK INT NOT NULL,
    RecValidFromKey INT NOT NULL,
    RecValidToKey INT NOT NULL,
    RecReason VARCHAR(15) NOT NULL,
    RecIsCurrent BIT NOT NULL,

    <...>
);
  • RecChangeDate_FK:存储变更生效的日期,允许查询变更发生的时间。
  • RecValidFromKey,RecValidToKey:存储行的有效日期范围。考虑将日期维度中最早的日期存储为 RecValidFromKey 以表示初始版本,并将当前版本的 RecValidToKey 存储为 01/01/9999。
  • RecReason:可选,允许记录版本插入的原因,可以编码哪些属性发生了变化,或者可以是源系统中表示特定业务原因的代码。
  • RecIsCurrent:允许仅检索当前版本,在 ETL 过程加载事实表时查找维度键时使用。

如果源系统不存储版本(比如业务系统中的数据),就需要使用一个中间系统(比如数据仓库)来检测和存储这些变化,并在维度表中适当地管理变化。

4.6.2.3 报表示例

在Adventure Works这个例子中,为了满足查询销售人员历史数据和特定版本数据的要求,Power BI语义模型的维度表需要设计两个列:

  1. 第一个列用于筛选销售人员本身(比如销售人员的名称或 ID)。
  2. 第二个列用于筛选销售人员的特定版本,版本列需要无歧义的描述,比如某个时间段内的销售人员记录,例如"David Campbell (12/15/2008-06/26/2019)"或"David Campbell (06/27/2019-当前)",避免混淆。

一个好的报表设计是包含一个层次结构,允许可视化向下钻取到版本级别。下图展示了一个典型的SCD Type 2维度表设计,用于管理销售人员的变化并保留历史记录。通过代理键和版本列,可以灵活地查询历史和当前数据,支持复杂的业务分析和报告需求。

  1. 左侧的表格是一个维度表,名为"Salesperson",主要包含了以下列:
    • EmployeeID:Source or Business Key,用于标识销售人员的唯一业务键
    • SalespersonKey:代理键(Surrogate Key),用于在数据仓库中唯一标识每个版本的销售人员。
    • Salesperson:用于筛选所有版本的销售人员。
    • Salesperson Version:用于筛选特定时间范围内的销售人员版本。
  2. 右侧的表格展示了销售数据,按层次结构分组为Group(销售组)和Sales(销售额)。层次结构示例:
    • North America:北美组总销售额为 $417,300.08
    • United States:美国区域总销售额为 $417,300.08
    • Northeast:东北区域总销售额为 $417,300.08
    • David Campbell :销售人员 David Campbell 的总销售额为 $172,114.67
      • David Campbell (12/15/2008-06/26/2019):这个版本的销售额为 $52,307.11
      • David Campbell (06/27/2019-Current):当前版本的销售额为 $119,807.56
4.6.3 Type 3 SCD(保留有限的历史版本)

Type 3 SCD中,不需要记录所有的历史版本变化,比如只需要保存初始值和当前值,或者上一版本和当前版本。以销售员被分配到不同的销售区域为例,Type 3 SCD 变化会直接覆盖当前的维度行,并在特定的列中记录之前的信息(下图是记录之前的销售区域外键为4,并记录变更的日期 20240523)。

4.7 杂项维度(Junk dimensions)

很多属性取值不多(比如订单状态只有"已发货""待付款"等几种,性别只有两种),就可以把这些属性放到一个Junk dimensions里。通过将多个小维度表整合成一个单一维度表,可以减少模型存储大小,同时避免数据窗格太过杂乱。

Junk dimensions通常是所有维度属性成员的笛卡尔积(所有小维度的组合)。为了区分每一行,还会加一个唯一的编号(代理键)。这个表可以用专门的数据工具(如数据仓库)来创建,也可以用Power Query来创建。下面是一个简单的示例,订单状态有三种,而交付状态有两种,可以将它们整合成一个Junk dimensions

4.8 退化维度与退化维度表

4.8.1 退化维度

参考《星星架构(退化维度)》

退化维度(Degenerate dimensions)是一种在事实表中直接包含维度属性的设计方案,适用于那些需要频繁用于筛选或分组的维度属性,比如Adventure Works中的销售订单号。如果单独为这个字段创建一个表,会浪费存储空间,还会让数据窗格看起来很乱。

在Power BI里,可以直接把销售订单号这个字段放在事实表里,这样就可以用它来筛选或分组数据。这其实是打破了"模型表要么是维度表,要么是事实表"的常规规则。


在图中,订单号(OrderNumber)在事实表中作为一个字段存在,而不是作为一个独立的维度表,这就是退化维度的实现方式。

4.8.2 退化维度表

参考:《一对一关系指南(退化维度)》

退化维度表是指将事实表中用于筛选或分组的列分离到一个独立的表中,与用于汇总事实行的列分隔开来。比如销售表里除了订单号列还有订单行号列 ,而且也需要用来筛选,那么可以创建一个退化维度表(Sales Order),把订单号和订单行号放在一起。

下图中,有一个名为"Sales"的事实表,其中包含销售订单行的详细信息。OrderNumber列存储订单号,OrderLineNumber列存储订单内的行序列号(一个顾客订单内包含多件商品)。

为了优化模型,将这两个退还维度从事实表中分离出来,创建了一个名为Sales Order的维度表;并使用它们的值创建了一个代理键列OrderLineNumberID(用于唯一标识),计算方式是 O r d e r N u m b e r × 1000 + O r d e r L i n e N u m b e r OrderNumber \times1000+OrderLineNumber OrderNumber×1000+OrderLineNumber。

分离后的Sales事实表和Sales Order维度表,通过OrderLineNumberID建立了一对一的关系。这么做的优点是:

  • 减少存储空间:这官方文档不是前后矛盾吗。
  • 简化模型计算:分离后的表结构更清晰,计算更简单。
  • 提升查询性能。
  • 提供更直观的体验:能更方便地在数据窗格中找到和使用所需的字段。
  • 支持更丰富的设计:支持对订单和订单行进行筛选、分组或向下钻取的报表设计。

4.9 日期和时间维度

日期维度(Date Dimension)是最常用的分析维度,每行代表一个日期,支持按年、季、月等特定时期过滤或分组。其自然键应使用日期数据类型,代理键使用 YYYYMMDD 格式的整数数据类型 。常见属性包括年、季、月、周、日,财政年相关属性,是否节假日等,详见《日期维度》。日期维度应包含所有事实表使用的日期范围,并可扩展至未来日期。

日期维度中不应包含时间粒度,如果事实表需要按时间维度分析,则应同时具有日期维度和时间维度。自然键使用时间数据类型,代理键可使用有意义且人工可读的格式,如 HHMMHHMMSS。常见属性包括:

  • Hour、HalfHour、QuarterHour、Minute
  • 时间段标签(上午、下午、晚上、夜晚)
  • 白班、晚班
  • 高峰或非高峰标志

4.10 一致维度

一致维度(Conformed Dimensions)与多个事实表相关,是多个星型模型中的共享维度,提供一致性和减少开发维护工作。例如,日期维度通常与多个事实表相关,需确保其包含适用于所有事实表分析的属性。下面的关系图中,事实表Sales 表和 Inventory 表都与一致性维度表 Date 和 Product 相关:

当两个不同的实体(例如员工和用户)实际上是同一组人时,可以将这两个实体的属性合并到一个统一的维度表中,保持数据的一致性,并简化数据模型。

4.11 角色扮演维度(见6.4章节)

4.12 外延维度

当维度表只与其他维度表相关时,称为外延维度 (Outrigger Dimensions,也叫子维度)。下面的关系图中, Geography 维度是子维度。 它不直接与 Sales 事实数据表相关。 相反,它通过 Customer 维度和 Salesperson 维度与事实表间接相关。又比如,客户表中的客户出生日期可以通过使用日期维度的代理键来存储,此时日期维度为外延维度。

4.13 多值维度

当一个维度属性需要存储多个值时,就需要设计一个多值维度(Multivalued dimensions)。多值维度通过创建一个桥接表(bridge table)来实现,桥接表用于存储实体之间的多对多关系。详见5.4.1章节。

4.14 维度表创建指南

  1. 确保列名具有自描述性(self-describing):这意味着列名应该清楚地表明它所包含的信息,以便让报告的制作者和阅读者更容易理解每个列的用途。比如同时有"订单日期"表和"发货日期"表,你可以将其年份分别命名为"订单年份"和"发货年份"。
  2. 提供表描述(table descriptions)以反馈过滤传播设置:在Power BI中,你可以为每个表添加描述性信息,它们会在数据窗格中作为工具提示显示出来,指明模型中设置的过滤规则,比如"Filters reseller sales by order date".

五、事实表

参考《Microsoft Fabric 仓库中的维度建模:事实数据表》

5.1 简介

下面是一个销售事实表 f_Sales的示例:

sql 复制代码
CREATE TABLE f_Sales
(
    --Dimension keys
    OrderDate_Date_FK INT NOT NULL,
    ShipDate_Date_FK INT NOT NULL,
    Product_FK INT NOT NULL,
    Salesperson_FK INT NOT NULL,
    <...>
    
    --Attributes
    SalesOrderNo INT NOT NULL,
    SalesOrderLineNo SMALLINT NOT NULL,
    
    --Measures
    Quantity INT NOT NULL,
    <...>
    
    --Audit attributes
    AuditMissing BIT NOT NULL,
    AuditCreatedDate DATE NOT NULL,
    AuditCreatedBy VARCHAR(15) NOT NULL,
    AuditLastModifiedDate DATE NOT NULL,
    AuditLastModifiedBy VARCHAR(15) NOT NULL
);
  1. 维度键(Dimension keys) :是相关维度中代理键(或更高级别属性)的引用。例如,OrderDate_Date_FKShipDate_Date_FK 是两个日期维度键,分别代表订单日期和发货日期的不同角色,但都引用同一个物理日期维度。在加载事实表时,可以使用特殊键值来表示缺失、未知、不适用或错误状态。
  2. 属性(Attributes) :属性就是一些额外的信息,它们决定了事实数据的详细程度,但它们既不是维度键,也不是度量值。本例中SalesOrderNoSalesOrderLineNo 这两个属性,分别记录了销售订单的编号和行号,在分析时可以当作退化维度来使用(见本文4.8章节)。
  3. 度量值(Measures):这是事实表的核心部分,通常为可加性的数值类型(可聚合汇总),例如Quantity 列表示数量。
  4. 审核属性(Audit attributes):见本文4.2章节。

5.2 事实表的类型

根据业务场景和数据特性,事实表可分为以下三种类型:

  1. 事务事实表(Transaction fact tables)
  2. 周期快照事实表(Periodic snapshot fact tables)
  3. 累积快照事实表(Accumulating snapshot fact tables)
5.2.1 事务事实表(销售表)

事务事实表用来记录具体的业务活动,每行记录以维度键和度量值,以及其他属性的形式存储事实。比如销售订单表中,每一行都包含了事件的关键信息------时间(维度键)、销量(度量值)以及其他一些细节(属性)。

所有数据在写入时就已经完全确定,并且通常不会更改(除非纠正错误)。事务事实表通常以尽可能低的粒度级别存储事实,例如记录每一行的销售订单,便于在更高粒度进行汇总分析。

5.2.2 周期快照事实表(库存表)

周期快照事实表就像是一个"定期拍照"的工具,用来记录某个时间点的关键数据,而不是记录每一笔具体的事务。比如,你有一个商店,只需要知道结束时每种商品的库存数量,就可以在每天结束的时候记录一次这些数据,而不是记录每一笔进货或出货的详细情况。

它的主要作用是帮助你看到数据随时间的变化趋势,比如库存是增加了还是减少了,或者某个指标是变好了还是变差了。这种表的数字(度量值)通常是"半可加性"的,意思是不能随便加在一起。比如,你不能把不同天的库存数量加起来,因为它们代表的是不同时间点的情况,但你可以把同一天不同产品的库存数量加起来。

5.2.3 累积快照事实表(进度表)

累积快照事实表是用来记录一个复杂业务流程的"进度条",比如一个订单从下单到发货再到收款的整个过程。这个过程可能要好几天、几周甚至几个月才能完成。

这种表会在流程开始的时候先记录一行数据,然后每完成一个关键步骤(比如下单、发货、收款),就会更新这一行数据。每个步骤都有一个对应的日期字段,如果某个步骤还没到,那个日期字段就先标记为" N/A"。

这种表的度量值通常会记录持续时间(比如每个步骤的持续时间),帮助你了解业务流程的效率,比如哪里拖了后腿,或者整个流程平均要多久完成。

5.3 度量值

度量值可分为三种类型:

  1. 可加性度量值(Additive measures):可以跨任何维度求和,例如订单数量和销售收⼊(前提是收⼊只用一种货币记录)。
  2. 半可加性度量值(Semi-additive measures) :只能跨某些维度求和,例如:
    • 库存余额:你可以把同一天不同产品的库存数量加起来,但不能把不同天的库存数量加在一起,因为它们是不同时间的快照。
    • 销售收⼊:如果你的销售数据使用多种货币单位记录(比如美元、欧元),你可以把同一货币的销售金额加起来,但不能把不同货币的金额直接加在一起,因为货币不一样。
  3. 不可加度量值(Non-additive measures):不能跨任何维度求和,例如温度读数、单价、比率等。尽管某些不可加性度量值不能求和,但它们仍然可以通过计数、非重复计数、最小值、最大值、平均值等方式进行聚合。此外,不可加性度量值在计算中可能变得可加,例如单价乘以订单数量得到可加性的销售收⼊。

5.4 特殊事实表

5.4.1 无事实的事实表(Factless fact tables,桥接表)

无事实的事实表是一种特殊的表,它里面没有数值数据(度量),只有用来连接其他表的键(维度键)。比如记录客户在特定时间和日期登录网站的情况(日期和是否登录)。虽然这个表里没有具体的数字数据,但可以通过这个表的行数分析客户登录的时间和数量。

又例如,销售人员可以分配到一个或多个销售区域,一个区域也可以有多个销售人员负责。这时,可以创建一个无事实的事实表(在多对多维度关系设计中,称为桥接表),里面只有两列:销售人员的键和区域的键。这个表不存储任何数字数据,只用来表示销售人员和区域之间的关系。

  1. Salesperson(销售人员):存储销售人员的详细信息。
  2. Salesperson Region(销售人员区域):桥接表,存储销售人员和区域之间的关系(多对多),这是一个多值维度表
    • SalespersonKey:销售人员键,外键,关联到销售人员表
    • SalesTerritoryKey:区域键,外键,关联到区域表
  3. Region(区域):存储区域的详细信息。

上图展示了如何在Power BI中使用无事实的事实表(桥接表)来定义维度之间的多对多关系。这种设计在Power BI语义模型设计中非常重要,它能帮助优化模型结构,提高性能,并使数据更易于理解和分析(2.7章节:通过中间表实现的多对多模型关系)。

特性 直接多对多设计:Salesperson ↔ Region 使用中间表将其拆分为两个单向关系: Salesperson → Salesperson Region ← Region
索引优化 有限关系中,查询时动态解析表连接,性能低下 常规关系可以创建数据结构,构建索引,加速查询
数据计算方式 计算所有可能的组合(完全笛卡尔积),即使实际数据中大部分组合无效 分步过滤:先在Sales表中筛选出符合条件的订单,再通过OrderDetails关联到对应的产品。每一步仅保留必要数据,避免全量数据计算。
内存占用 完全笛卡尔积会占用大量内存和计算资源 低,仅保留实际存在的数据关联
筛选器传播路径 可能因多方向传播引发逻辑冲突 传播路径明确
适用场景 小规模数据集(如产品类别≤1000) 大规模数据集(如订单量≥100万)
开发速度 快(无需中间表设计) ,适合快速原型开发 慢(需额外表和关系配置), 适合生产级性能优化
维护复杂度 低(结构简单) 高(需管理中间表关系)
灵活性 低,在数据结构发生变化时,需要重新定义关系和调整查询。 高,如果需要添加新的维度或调整关系,中间表可以更容易地进行修改和扩展。
5.4.2 聚合事实表(Aggregate fact tables,待补)

聚合事实表是对基础事实表的汇总(可以是更高粒度或更低粒度),用于加速常用维度查询的查询性能。可以通过《用户定义的聚合》来实现,或者通过 DirectQuery 存储模式,使用数据仓库合并事实数据表来实现。

六、模型关系

6.1 一对一关系

参考《一对一关系指南》

6.1.1 退化维度(略)
6.1.2 跨表行数据(一对一关系)

下面是一个简单的跨表行数据示例,图中 Product 维度表 包含三列,分别是 Color(颜色)、Product(产品)和 SKU(库存单位)。 Product Category 维度表 :包含两列,分别是 Category(类别)和 SKU(库存单位)。 两个表通过 SKU 列建立一维关系。这种关系是双向过滤的,即在两个方向上都生效。

SKU(Stock Keeping Unit)直译为"库存进出计量基本单元",是用来记录和管理产品库存的唯一标识符(企业内部编码) ,通常是一个数字或字母组合,用于区分不同的产品、颜色、尺寸、款式等属性。如果一款T恤有三种颜色(Red,Blue,Green)和两种尺码(S,M),那么它会有6个SKU。电商平台和供应链管理、销售和财务分析中也会用到SKU。

在表格视觉对象中,SKU CL-02 的 Category 值为 BLANK(空白),因为没有对应的行在 Product Category 表中。所以对于跨表行数据,应尽可能避免创建一对一模型关系,因为这种设计可能会导致:

  • 数据窗格杂乱、列出不必要的表
  • 报表作者很难找到相关字段,因为它们分布在多个表中
  • 限制创建层次结构的能力(创建层级结构需要字段及其下级字段在一个表中,比如产品类别->产品)
  • 常规关系中,当不完全匹配时,会出现空白行,以维护引用完整性
6.1.3 源组内一对一关系优化:合并查询

对于源组内的一对一关系,建议使用 Power Query的合并查询方式,将其合并到一个表中。

  1. 使用左外联接方式 (Left outer join,默认方式),合并结果是左表所有行+右表匹配行。对于右表不匹配的行,将填充null值。
  2. 对右表禁用加载 :Power Query默认会将所有查询的结果加载到数据模型中,这可能会导致不必要的内存消耗。通过禁用加载,数据刷新时仍会执行其转换步骤,只是结果不会加载到模型中,以减小模型大小,并有助于让"数据"窗格保持整洁。
  3. 替换缺失值:报表视觉对象中缺失值null会显示 为BLANK(空白行),可考虑将其进行替换,比如替换为未定义(Undefined)。
  4. 创建层次结构:如何合并后的表,两个列之间存在层次关系,比如Category -> Product。

如果您喜欢使用分开的表来组织字段,可以使用文件夹功能来组织和分类字段,比如将Category 字段放在Marketing 显示文件夹中(display folders)。

6.1.4 跨源组一对一关系优化:预先合并数据

如果表之间是一对一跨源组关系,除非预先合并数据源中的数据,否则有限关系中,未匹配行将从查询结果中被直接删除。因此,请注意确保相关表中有匹配的行。下图展示了跨源组一对一 VS 合并表查询的结果:左侧未匹配的行被直接删除,右侧保留空白行,并替换为自定义值。

6.2 多对多关系

参考《多对多关系指南》《Power BI Desktop 中的多对多关系》

6.2.1 多对多维度表关联(设置双向过滤)

经典的多对多场景是将两个维度表关联起来,例如银行的客户和账户。一个客户可以拥有多个账户,一个账户也可以有多个客户(这些客户通常会被称为"联合帐户持有人")。在这种情况下,我们需要通过一个桥接表(无事实的事实表)来建立两者之间的关系(两个一对多关系)。下面是三个模型表的简单示意图。

  • Account:维度表,包含账户名称和账户唯一标识符;
  • Customer:维度表,包含客户名称和客户唯一标识符;
  • AccountCustomer:桥接表,用于建立账户和客户之间的多对多关系。

假设现在有一个新的事实表Transaction,它记录了帐户交易信息,有三行:

整个模型关系图更新如下(桥接表被隐藏):

在查询模型时,可能会遇到筛选无法正确传播的问题。例如在下图中,有两个用于汇总Amount列的视觉对象,左侧是按帐户分组,所以显示的是账户余额 ;右侧是按客户分组,所以显示的是客户余额

明显可以看出,客户余额的显示不正确,每个客户的余额都显示为总余额。这是因为在 Power BI 的关系模型中,过滤器默认是单向传播的,即从"一"侧传播到"多"侧。当 AccountAccountCustomer 表之间的关系是单向时,从 Customer 表到 AccountCustomer 表的过滤器无法进一步传播到 Account 表,也就无法影响 Transaction 表的筛选。因此,当按客户分组时,Transaction 表中的数据没有被正确筛选,导致所有客户的余额都显示为总余额。

解决方法是将 Account 和 AccountCustomer 表之间的关系设置为双向筛选,确保筛选能够正确地从维度表传播到事实表。

设置之后,汇总结果显示正确:

多对多维度关系指南:

  • 添加相关实体 :将每个与多对多关系相关的实体作为模型表添加,并确保每个表都有一个 ID 列。
    例如,在银行客户和账户的关系中,分别创建 Account(账户)表和 Customer(客户)表,并确保每个表都有唯一的标识列,如 AccountID 和 CustomerID。
  • 使用桥接表创建一对多关系 :创建一个桥梁表来存储关联的实体,比如桥接表AccountCustomer,以创建一对多关系。
  • 设置一个双向关系,以便过滤器能够继续传播到事实表。
  • 如果不应该有缺失值,禁用 Is Nullable 属性 。在维度表中,ID 列通常是主键,不应该有空值。为了确保数据的完整性,对其禁用 Is Nullable 属性,这样当数据刷新时,碰到缺失值会刷新失败。
  • 隐藏桥接表和不适合的 ID 列 :隐藏桥梁表(除非它包含报告所需的其他列或度量),以及不适合用于报告的 ID 列,例如代理键(Surrogate Key),以简化报表界面。
  • 隐藏ID列:如果需要显示 ID 列,确保它位于关系的"一"侧,始终隐藏"多"侧的列(将过滤器应用于"一"侧可以带来更好的过滤性能)。
  • 添加报告说明:在报表中添加说明(比如添加文本框描述或视觉标题),解释为什么某些列被隐藏,或者如何正确使用过滤器,以帮助用户更好地理解和使用报表。

不建议直接关联维度表之间的多对多关系,虽然这确实可以实现。更普遍的做法是,维度表应该有一个 ID 列,始终作为关系的"一"侧。

6.2.2 多对多事实表关联(推荐采用星型架构)

另一种不同的多对多场景是多对多事实表,例如销售订单表Order和已发货订单表Fulfillment。一个销售订单可以拆分成多个发货订单,也可能还没有发货;一个发货订单可以含有多个销售订单。如果直接将这两个表通过OrderID 列进行多对多关联,默认Order 表单向筛选 Fulfillment 表,示意图如下:

直接多对多关联的局限性 :下图中视觉对象按Order 表汇总了销售信息和发货信息,结果显示正确,但这种方式,视觉对象只能按Order 表的OrderID 列进行筛选或分组,无法利用其他维度进行分析。此外,有限关系中,数据完整性问题可能导致查询时遗漏某些行。

建议采用星型架构设计,添加维度表(如订单行、日期、产品等),并通过一对多关系将它们与事实表关联。

模型现在有四个额外的维度表:OrderLine,OrderDate,Product,FulfillmentDate,都与事实表进行一对多的关联。

  • OrderLine 表的OrderLineID 列,计算方式是 O r d e r I D × 100 + O r d e r L i n e OrderID\times 100+OrderLine OrderID×100+OrderLine;
  • 事实表调整:Order表(销售订单)和 Fulfillment(已发货订单)表现在各自包含一个 OrderLineID 列,用于与 OrderLine 表关联,而不再包含原来的 OrderIDOrderLine 列;
  • Fulfillment 表的扩展:Fulfillment 表添加 OrderDate 列以与 OrderDate 表关联,添加ProductID 列以与 Product 表关联;
  • 所有 ID 列(例如OrderLineID和ProductID)都被隐藏,以简化用户界面。用户只能看到有意义的业务字段,如订单日期、产品名称等。

采用星型架构的好处:

  1. 灵活的过滤和分组:报表可按维度表中的任何可见列进行过滤或分组。
  2. 全面的数据汇总:可以汇总事实表中的任何可见列
  3. 可以使用更多的额维度进行过滤:例如,如果用户按某个产品类别进行筛选,那么 Order 表和 Fulfillment 表中的相关数据都会被正确筛选出来。
  4. 关系明确,数据完整:所有关系都是一对多的常规关系,确保数据的完整性不被破坏。
6.2.3 高粒度事实表关联(无法直接建立一对多关系)

当事实表的数据粒度高于维度表时,无法直接建立一对多关系。如下图所示,现在有四个表:

  • 维度表Date,Product
  • 事实表Sales,Target,其中,Target 表包含三列:Category(类别)、TargetQuantity(目标数量)和 TargetYear(目标年份)。它的数据粒度是年度和产品类别,也就是说,每年每个产品类别都有一个目标值,用于衡量销售业绩。

两个事实表详细信息如下图所示,由于 Target 表的数据粒度比维度表高(年度 vs. 日期;产品类别 vs. 产品),无法直接创建常规的一对多关系(上图中Target 表未自动创建关系)。

6.2.3.1 高粒度时间处理

当事实表中存储比天更高粒度的时间数据时,将列的数据类型设置为日期,并在其中存储时间周期的第一天(比如每年的 1 月 1 日,每月 1 号),这样就可以创建与日期表的一对多关系。如果这列使用日期键,则设置为整数。

需要注意的是,要确保按月或按天的筛选能够产生有意义的结果 。比如若直接对目标数据按日期或月份进行汇总,那么系统可能会错误地将整个年度的目标值都归到该年度的第一天上,其它日期都是 BLANK(空白行)。比如用户对年度销售目标 向下钻取到月份时,TargetQuantity2020-01行得到了汇总结果(显示270),这种显示错误是因为系统不知道如何将年度总目标分配到年度中的各个月和日 (矩阵的行已启用显示不含数据的项)。

解决办法是,通过度量值控制汇总逻辑,避免在较低时间粒度下出现不合理的汇总结果。一种方法是在查询较低粒度的时间周期时直接返回 BLANK 。另一种方法是使用复杂的 DAX 定义,控制较低粒度时间周期的数值分配方式,比如:

c 复制代码
//仅在日期和月份列未被筛选时返回值,否则返回BLANK,从而避免显示不合理的值。
Target Quantity =
IF(
    NOT ISFILTERED('Date'[Date])
        && NOT ISFILTERED('Date'[Month]),
    SUM(Target[TargetQuantity])
)

下面是使用度量前后的对比结果:

6.2.3.2 关联非日期的更高粒度

对于将维度表中的非日期列与高粒度的事实表相关联的情况,需要采用不同的设计方法。如果维度表和事实表中的关联列都包含重复值 (比如Product 表和 Target 表的Category 列,一种产品类别都会有几种产品),此时没有"一"侧,只能创建单向多对多关系(维度表->事实表)

  • Target 表包含2019和2020年的销售目标数据;Product 表包含具体的产品及其ID,以及颜色
  • 两个表都有Category 列,都有服饰和配饰类别。

下面按照维度表Product 表进行汇总统计,其中按Category 列分组统计结果显示正确,但是按Color列分组统计结果显示错误。

这是因为Target 表的粒度高于 Product 表,Target 表中没有按颜色细分的目标数量,因此按颜色筛选时,颜色列的筛选器会导致多个类别值被传播到目标表,从而得到错误的汇总结果。

比如按蓝色进行分组时,筛选会传播到 Category 列,筛选出所有属于蓝色产品的类别(Clothing + Accessories,结果是100);按绿色分组时,筛选出Clothing类的结果,为40

解决方案同样是使用度量值来控制事实数据的汇总逻辑,比如定义:

c 复制代码
Target Quantity =
IF(
    NOT ISFILTERED('Product'[ProductID])
        && NOT ISFILTERED('Product'[Product])
        && NOT ISFILTERED('Product'[Color]),
    SUM(Target[TargetQuantity])
)

如果对 Product 表中的低粒度列( ProductID、Product、Color)进行了筛选,则返回空白值(BLANK),避免不正确的汇总;否则正常汇总 Target 表中的 TargetQuantity 列。下面是对比效果:

最终的模型设计如下所示:

6.2.3.3 总结
类别 建模策略 查询较低粒度数据时,使用度量值控制汇总逻辑,确保筛选汇总结果有意义
时间相关的高粒度事实列 将其存储为时间周期的第一天,建立一对多关系 1. 直接返回 BLANK; 2. 使用复杂的 DAX 定义,控制较低粒度时间周期的数值分配方式。
非时间相关的高粒度事实列 没有"一"侧列时,创建多对多关系 如果事实表中不存在这个维度(比如 Target 表中没有进行颜色细分), 使用度量值控制汇总结果为 BLANK 。

6.3 双向关系

参考《双向关系指南》

通常情况下,建议尽量减少使用双向关系,因为这会对模型查询性能产生负面影响,并对报表用户带来令人困惑的体验,但是有三种情况例外:

  1. 特殊模型关系
  2. 切片器带有数据选项
  3. 多维度分析
6.3.1 特殊模型关系
  1. 所有一对一关系都必须是双向的,否则无法进行配置。通常不建议这么做,而是将其合并到一个维度表中,详见本文4.1章节
  2. 使用桥接表来关联两个维度表,此时必须设置一个双向关系,以便过滤器能够继续传播到事实表。详见本文4.2章节。
6.3.2 启用非空选项的切片器(使用度量值进行筛选)

假设有以下三个表,其中Customer 表和 Product 表是维度表,每个表都与 Sales 表具有单向一对多关系。

现在,你在报表页面上创建了两个切片器和一个卡片图。第一个切片器基于顾客表的Country-Region字段,选项为美国和澳大利亚;第二个切片器基于产品表的Product字段,选项有帽子、牛仔裤和T 恤。此时,两个切片器会列出所有可能的选项,即使实际上某些组合没有销售数据(比如澳大利亚的顾客只买过T 恤)。

这种情况下,用户可能会感到困惑,因为切片器中显示了很多看似可用但实际上没有任何数据支持的选项。

如果要过滤掉空值组合,只保留"带有数据"的选项组合,最直接的方式是将 Product 表与 Sales 表之间的关系设置为双向筛选来实现此行为。


但是一般不建议直接设置双向关系,因为可能对查询性能产生负面影响。一种更好的方式是,将视觉级别筛选器应用于产品切片器本身。具体方法是在 Sales 表中添加一个度量值:

c 复制代码
Total Quantity = SUM(Sales[Quantity])

然后在产品切片器上筛选Total Quantity 不为空即可。这样筛选时,会自动排除空值组合。

6.3.3 多维度分析(临时激活双向筛选关系)

假如我们想要回答"向澳大利亚客户销售了多少种颜色的产品"或"有多少个国家/地区购买了牛仔裤"等多维度问题,通过在事实表和维度表之间设置双向关系,可以实现跨维度表的过滤和分析。但同样,这种设计可能会对性能产生负面影响,且会列出所有可能的组合(包括空值组合)。

这种情况下,可以使用CROSSFILTER 函数在需要的时候临时激活双向筛选关系,然后使用MIN,MAX,DISTINCTCOUNT等函数进行不同的汇总统计。例如,在计算不同国家的销量时,使用以下度量:

c 复制代码
Different Countries Sold =								 //计算所有已购买产品的客户的国家/地区值的非重复计数
CALCULATE(
    DISTINCTCOUNT(Customer[Country-Region]),			 
    CROSSFILTER(
        Customer[CustomerCode],
        Sales[CustomerCode],
        BOTH											//设置筛选关系为双向
    )
)

DISTINCTCOUNT:用于返回列中非重复值的数据,DISTINCTCOUNT(Customer[Country-Region])表示在客户表中,统计出不同国家/地区的数量。DISTINCTCOUNT(Product[Color])可以统计出产品表中不同颜色的数量。

这样,在计算该度量值时,客户表和销售表之间的关系会双向过滤,从而实现跨维度表的分析,同时避免了在模型级别设置双向关系可能带来的性能问题。

建议使用

6.4 活动、非活动关系与角色扮演维度

参考《角色扮演维度表》《角色扮演维度》《活动与非活动关系指南》

角色扮演维度(Role-playing Dimensions)可简单理解为同一个维度在事实数据表中被多次引用,根据不同的业务需求(不同角色)来过滤相关的事实数据。

例如,在Adventure Works的例子中,Reseller Sales事实表(Fact表)有订单日期、发货日期和交货日期 三个字段,Date维度表(Dim表)的DateKey以三种关系和Reseller Sales表相关联,但实际上Date表只有一个日期维度。

在Power BI中,两个表之间一个键(一个字段)只能有一个活动关系(Active Relationship),其它关系必须设置为非活动状态(Inactive Relationships)。非活动状态不会自动用于数据筛选和计算,如果报表视觉对象不需要同时按不同角色进行筛选 ,可以考虑使用非活动关系,然后通过 USERELATIONSHIP 函数在需要的场景中进行动态激活,比如:

cpp 复制代码
// 计算发货订单
Orders Shipped =
CALCULATE(
    COUNTROWS(ResellerSales)
    ,USERELATIONSHIP('Date'[DateKey], ResellerSales[ShipDateKey])
)

这允许你在不同的计算场景中灵活切换关系,而不会影响模型的整体结构。不过这样做,可能会很繁琐,尤其是当Reseller Sales表定义了许多度量时。

另外,对于常规关系,为了满足匹配完整性,对于非活动关系也会进行表扩展,所以对于不匹配的行,会添加null值(视觉对象中显示为BLANK)。例如按销售日期(季度)进行筛选,在表中同时按月统计销售订单和发货订单数据时,因为存在销售订单还没有发货的情况,Quarter切片器会有Blank选项。

一个更常见的做法是为每个角色创建单独的维度表,这样可以避免定义多个度量,并且允许同时按不同的日期角色进行过滤。对于导入模型,添加另一个维度表会导致模型大小增加,刷新时间更长,但功能需求更加重要,而且维度表通常比事实表存储的行数少,所以推荐这么做。

建议对角色扮演维度添加前缀以进行更明显的区分,比如三个表都有DateKey字段,最后添加不同的前缀;建议在角色扮演表中添加说明,这样在"数据"窗格中,当报表作者将光标悬停在表上时,工具提示中会显示说明信息。

一般来说,建议尽可能定义活动关系,因为它们提供了更广泛的使用范围和灵活性。活动关系可以让报表作者更自由地使用你的模型,而不需要额外的 DAX 函数来激活关系。

相关推荐
akbar&1 小时前
计算机四级 - 数据库原理 - 第8章「分布式、对象-关系、NOSQL数据库」
数据库·笔记
多多*1 小时前
牛客周赛84 题解 Java ABCDEFG AK实录
数据库·windows·macos·github·objective-c·mybatis·cocoa
椅子哥2 小时前
MyBatis操纵数据库-XML实现(补充)
xml·java·数据库·spring·mybatis·springboot
!chen2 小时前
查询修改ORACLE的server、客户端和导出dmp文件 字符集编码
数据库·oracle
是懒羊羊吖~2 小时前
【sql靶场】第15、16关-post提交盲注保姆级教程
数据库·笔记·sql·时间盲注·布尔盲注·sql靶场
Pandaconda4 小时前
【后端开发面试题】每日 3 题(十二)
数据库·后端·面试·负载均衡·高并发·后端开发·acid
zhouwhui4 小时前
【openGauss】物理备份恢复
数据库·gaussdb
DB_UP5 小时前
数据库的高阶知识
数据库
E___V___E6 小时前
MySQL数据库入门到大蛇尚硅谷宋红康老师笔记 高级篇 part 8
数据库·笔记·mysql