构建 Medallion 架构——精简与优化 Gold 层

77 阅读52分钟

在第 5 章里,我们构建了 Bronze 层,解决了在来源系统多样且复杂的前提下,仍要做出可查询结构的难题。我强调了这一基础层在稳定性与可靠性方面的重要性。进入第 6 章后,我们把重心转向 Silver 层,以提升数据质量并建立稳健的数据处理框架。你学会了如何按组织的具体需求来调整 Silver 层的设计,以适配不同的处理要求。我们在充分精炼与校验后的数据集处收尾,为下一步做好了铺垫。

在本章中,我们将延续该练习,深入设计 Medallion 架构的最后也是最细致的阶段——Gold 层。该层对决策与报表至关重要,是数据精炼的顶点,旨在为高性能查询与分析而优化,并在支撑关键业务职能方面发挥核心作用。

你将沿用前两层相似的方法论,之前掌握的技能会自然迁移到 Gold 层的语境中,也可同样应用于其他基于 Delta Lake 与 Spark 的环境。教程的一部分还将构建语义层——这是进一步的精炼/抽象层,向业务提供更贴近业务视角的数据观。完成练习并学习大量示例后,我们会讨论数据的使用方式以及如何从 Gold 层产出数据产品,最后以数据治理与 Microsoft Purview 的详细讨论收束全章。

Gold 层的设计

当你抵达 Gold 层,就来到了 Medallion 架构中最复杂的部分。该层是数据精炼的最终阶段,为决策与报表而生。通常要在此合并来自多个来源的数据、进行统一与对齐,并以一致的方式呈现。出于本章练习的聚焦考虑,你将只对一个来源的数据做进一步精炼。这既简化了流程,也更突出该层的核心目标:提升业务决策与报表能力。

用星型模型改造数据

Gold 层最适合采用星型模型。此类数据模型特别契合数据仓库与商业智能场景。正如“星型模型”一节所述,星型模型以中心事实表为核心,若干维度表向外辐射如星芒。该结构能简化复杂的数据关系,便于业务用户进行报表与分析;同时还能优化查询性能,非常适合数据分析与报表环境。

构建星型模型的第一步,是识别相关的业务实体与业务事件,它们将分别对应维度与事实。通常需要与业务用户密切协作,因为他们最了解业务目标与流程背景。在沟通中还要讨论数据粒度,即数据的明细层级。比如,业务也许希望在报表中看到单个产品与单笔销售订单,但不需要构成每张订单的底层事务明细。

结合本章练习与 AdventureWorks 示例,相关主题可能包括产品、产品类别、客户、地址与日期。确定主题域后,再确定中心事实表——该表应包含用于分析的关键度量(如销售额、收入或客户数)。随后设计维度表,为事实提供上下文(如时间、地域、产品或客户等属性)。最后通过外键在事实与维度之间建立关系,完成模型。务必注意:在向星型模型装载数据时,一定要先装载维度表,再装载事实表,以保证键关系的完整性。

针对 AdventureWorks 示例,星型模型包含以下表:

  • 地址维度(Address dimension)
    保存地址明细,如地址行 1、城市、州/省、国家等,用于唯一标识与分类地址。
  • 客户维度(Customer dimension)
    保存客户信息,如姓名、公司名、销售人员等,用于识别与分析客户画像与行为。
  • 日期维度(Date dimension)
    提供时间维度,如日期、日、月、年,是进行时序分析与报表的基础。
  • 产品维度(Product dimension)
    保存产品明细,如产品名、类别、型号名等,用于唯一标识与分类产品。
  • 销售事实(Sales fact)
    作为中心事实表,记录交易数据,包含产品键、客户键、销售金额等字段,是跟踪与分析销售绩效的关键。

在 Medallion 架构的 Gold 层中,出于决策与历史准确性的要求,通常会使用 SCD(尤其是 SCD2)来确保数据既能反映当前状态,也能保留历史状态,从而支持深度分析与策略洞察。本教程将继续采用该方法,即便 Silver 层已做过历史化处理——主要是为了加深你的理解与动手体验。

警告
在 Silver 层实施 SCD2 非常有价值,因为它以真实业务语境保存历史变更。是否在 Gold 层再做 SCD2,应以具体业务需求为驱动:若终端用户需要直接查询历史状态用于报表/分析,那么在 Gold 层实施 SCD2 就有价值。出于演示目的,我们在 Gold 层继续使用它,但实际项目可因情而异。

接下来你将学习如何以星型建模的方法构建 Gold 层:创建表、处理维度表、处理事实表,并将数据以 Delta 表写入 Gold 层(见图 7-1 所示的三个关键步骤)。在完成星型设计并做一些阶段性结论后,我们还会构建语义模型以建立表间关系;最后你将学习如何创建 Power BI 报表,并用任务流文档化端到端项目设计,形成清晰的总览。

image.png

图 7-1. 本教程的总体目标

创建 Gold 层表

回到 Microsoft Fabric,打开开发 Workspace。在“Open notebook”菜单中选择“New notebook”。几秒后会出现一个仅含单元格的新 Notebook。将其重命名为 create_gold_tables(此流程与第 5、6 章相同)。在左侧选择 Gold Lakehouse,确保操作环境正确。

开始编码吧:将以下代码粘贴到一个单元格中。脚本会在 Gold 层为地址、客户、日期、产品四个维度表以及销售事实表创建空表。该代码遵循“模式演进与管理”中的最佳实践,包含定义 schema 与将表保存为 Delta 表的必要步骤。为便于维护,建议为每张表拆分成独立代码单元执行:

from pyspark.sql.types import *

# Create the schema
spark.sql(f'CREATE SCHEMA IF NOT EXISTS adventureworks')

# Define the schema for address
schemaAddress = StructType([
    StructField("ID", StringType()),
    StructField("AddressID", IntegerType()),
    StructField("AddressLine1", StringType()),
    StructField("AddressLine2", StringType()),
    StructField("City", StringType()),
    StructField("StateProvince", StringType()),
    StructField("CountryRegion", StringType()),
    StructField("current_flag", BooleanType()),
    StructField("current_date", DateType()),
    StructField("end_date", DateType())
])

# Create the DataFrame
dfAddress = spark.createDataFrame([], schemaAddress)

# Create the table
dfAddress.write.mode("append").saveAsTable("adventureworks.dimension_address")

# Define the schema for customer
schemaCustomer = StructType([
    StructField("ID", StringType()),
    StructField("CustomerID", IntegerType()),
    StructField("Title", StringType()),
    StructField("FirstName", StringType()),
    StructField("MiddleName", StringType()),
    StructField("LastName", StringType()),
    StructField("CompanyName", StringType()),
    StructField("EmailAddress", StringType()),
    StructField("Phone", StringType()),
    StructField("current_flag", BooleanType()),
    StructField("current_date", DateType()),
    StructField("end_date", DateType())
])

dfCustomer = spark.createDataFrame([], schemaCustomer)
dfCustomer.write.mode("append").saveAsTable("adventureworks.dimension_customer")

# Define the schema for date
schemaDate = StructType([
    StructField("ID", StringType()),
    StructField("OrderDate", DateType()),
    StructField("Day", IntegerType()),
    StructField("Month", IntegerType()),
    StructField("Year", IntegerType())
])

dfDate = spark.createDataFrame([], schemaDate)
dfDate.write.mode("append").saveAsTable("adventureworks.dimension_date")

# Define the schema for product
schemaProduct = StructType([
    StructField("ID", StringType()),
    StructField("ProductID", IntegerType()),
    StructField("ProductNumber", StringType()),
    StructField("Color", StringType()),
    StructField("Size", StringType()),
    StructField("Weight", StringType()),
    StructField("CategoryName", StringType()),
    StructField("ProductModelName", StringType()),
    StructField("current_flag", BooleanType()),
    StructField("current_date", DateType()),
    StructField("end_date", DateType())
])

dfProduct = spark.createDataFrame([], schemaProduct)
dfProduct.write.mode("append").saveAsTable("adventureworks.dimension_product")

# Define the schema for sales
schemaSales = StructType([
    StructField("SalesKey", StringType()),
    StructField("AddressKey", StringType()),
    StructField("CustomerKey", StringType()),
    StructField("ProductKey", StringType()),
    StructField("DateKey", StringType()),
    StructField("Revenue", DoubleType()),
    StructField("OrderQty", IntegerType()),
    StructField("UnitPrice", DoubleType()),
    StructField("current_flag", BooleanType()),
    StructField("current_date", DateType()),
    StructField("end_date", DateType())
])

dfSales = spark.createDataFrame([], schemaSales)
dfSales.write.mode("append").saveAsTable("adventureworks.fact_sales")

运行所有单元格并校验结果:现在你应当在 Gold 层拥有 5 张空表。以这种方式管理 DDL,便于版本化模式并在演进时保持向后兼容。下一步从维度表开始装载数据。

构建地址维度表

接下来进入维度表的构建。先从地址维度开始。新建一个 Notebook,命名为 dimension_address。写 Gold 层时,建议启用 V-order 优化写入,因此请在 Notebook 开头加入如下会话配置(Gold 层所有表均建议启用):

# Set up the session for V-Order writing
"spark.sql.parquet.vorder.enabled", "true"
"spark.microsoft.delta.optimizeWrite.enabled", "true"
"spark.microsoft.delta.optimizeWrite.binSize", "1073741824"

这些设置开启 V-order 写入;optimizeWrite 可减少文件数、增大单文件体积,默认值约 1GB(1,073,741,824 字节)。

在下一个代码块中粘贴:

from pyspark.sql.functions import *

# Load data to the DataFrame
address = spark.read.table("silver.adventureworks.hist_address") \
.where(col("current") == True)
address = address.dropDuplicates(["AddressID"])
address = address[["AddressID", "AddressLine1", "AddressLine2","City", "StateProvince", "CountryRegion"]]

# Add hash code using all selected columns
dimension_address = address.withColumn("ID",
sha2(concat_ws("||", *address.columns), 256))

上述代码先加载并过滤数据,再以所有列生成每行的哈希作为代理键(surrogate key) ,为维度记录提供稳定唯一标识。代理键有助于在业务标识或数据随时间变化时,维持历史数据与关联关系的一致性与准确性。

关于自增列(IDENTITY COLUMNS)
撰写时,Fabric Lakehouse 尚不支持自增列(通常用于自动生成每行唯一 ID)。随着对 Delta Lake 3.3.0 的支持,此情况可能会改变。这里我们用 SHA-2 哈希函数来承担“自增”的角色,从而保证唯一性。

将代码运行后,随时可用 display(dimension_address) 查看 DataFrame 内容。

再新增一个代码块,用 Delta Lake 的 merge 来更新 dimension_address 表(本示例按 SCD2 语义设计):

  • 若 DataFrame 中的记录与 Gold 层表中已有记录(按代理键)匹配,则更新为当前记录并刷新当前日期。
  • 若无匹配,则插入新记录。
  • 若 Gold 层表中存在无对应来源的记录,则把旧记录标记为非当前并设置终止日期。
from delta.tables import *

deltaTable = DeltaTable.forPath(spark,
'Tables/adventureworks/dimension_address')

deltaTable.alias('gold') \
  .merge(
    dimension_address.alias('updates'),
    'gold.ID = updates.ID'
  ).whenMatchedUpdate(set =
    {
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedInsert(values =
    {
      "ID": "updates.ID",
      "AddressID": "updates.AddressID",
      "AddressLine1": "updates.AddressLine1",
      "AddressLine2": "updates.AddressLine2",
      "City": "updates.City",
      "StateProvince": "updates.StateProvince",
      "CountryRegion": "updates.CountryRegion",
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedBySourceUpdate(set =
    {
      "current_flag": lit("0"),
      "end_date": current_date()
    }
  ).execute()

运行完成后,即可在 Gold 层填充地址维度数据。请再做一些一致性检查,确认结果正确。

注意
撰写时,Lakehouse schemas 仍处于预览阶段,DeltaTable.forName 尚未完全可用,因此需要用 DeltaTable.forPath 来访问 Delta 表。未来预计会有所变化。

验证无误后,继续下一步:构建客户维度表。

创建客户维度表(Creation of the dimensional table for customer)

接下来,新建一个名为 dimension_customer 的笔记本。几乎重复地址维度表的步骤,但这次应用于客户数据。使用以下代码片段:

from pyspark.sql.functions import *

# 将数据加载到 DataFrame,作为构建 Gold 层的起点
customer = spark.read.table("silver.adventureworks.hist_customer") \
.where(col("current") == True)
customer = customer.dropDuplicates(["CustomerID"])
dimension_customer = customer[["CustomerID", "Title", "FirstName", \
"MiddleName", "LastName", "CompanyName", "EmailAddress", "Phone"]]

# 使用所选列添加哈希码
dimension_customer = dimension_customer.withColumn("ID", \
sha2(concat_ws("||", *dimension_customer.columns), 256))

最后,在新的单元格中粘贴以下代码以处理 dimension_customer 表的更新:

from delta.tables import *

deltaTable = DeltaTable.forPath(spark, \
'Tables/adventureworks/dimension_customer')

deltaTable.alias('gold') \
  .merge(
    dimension_customer.alias('updates'),
    'gold.ID = updates.ID'
  ).whenMatchedUpdate(set =
    {
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedInsert(values =
    {
      "ID": "updates.ID",
      "CustomerID": "updates.CustomerID",
      "Title": "updates.Title",
      "FirstName": "updates.FirstName",
      "MiddleName": "updates.MiddleName",
      "LastName": "updates.LastName",
      "CompanyName": "updates.CompanyName",
      "EmailAddress": "updates.EmailAddress",
      "Phone": "updates.Phone",
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedBySourceUpdate(set =
    {
      "current_flag": lit("0"),
      "end_date": current_date()
    }
  ).execute()

再次运行笔记本中的单元格以验证结果。

创建日期维度表(Creation of the dimensional table for date)

现在来创建日期维度表。该表为事实表中的销售数据提供时间上下文,包含日、月、年等字段。注意:该表不需要做历史化(historize),因为日期属性是固定不变的。

新建名为 dimension_date 的笔记本,将以下代码粘贴到单元格中,构建日期维度表:

from pyspark.sql.functions import *

# 加载数据
dim_date = spark.read.table("silver.adventureworks.hist_salesorderheader") \
.where(col("current") == True)

dim_date = dim_date.dropDuplicates(["OrderDate"]).select(col("OrderDate"), \
        dayofmonth("OrderDate").alias("Day"), \
        month("OrderDate").alias("Month"), \
        year("OrderDate").alias("Year")
    ).orderBy("OrderDate")

# 使用所选列添加哈希码
dim_date = dim_date.withColumn("ID", \
sha2(concat_ws("||", *dim_date.columns), 256))

上述代码从 OrderDate 列中提取日、月、年,并按 OrderDate 排序。为与其他表保持一致,最终为每行生成唯一哈希码作为该维度表的代理键(surrogate key)。

下一步,将该 DataFrame 与 Gold 层目标 Delta 表合并,处理更新:

from delta.tables import *

deltaTable = DeltaTable.forPath(spark, \
'Tables/adventureworks/dimension_date')

deltaTable.alias('gold') \
  .merge(
    dim_date.alias('updates'),
    'gold.ID = updates.ID'
  ).whenNotMatchedInsert(values =
    {
      "ID": "updates.ID",
      "OrderDate": "updates.OrderDate",
      "Day": "updates.Day",
      "Month": "updates.Month",
      "Year": "updates.Year",
    }
  ).execute()

再次运行笔记本中的单元格以验证结果。

创建产品维度表(Creation of the dimensional table for product)

新建名为 dimension_product 的笔记本。该笔记本先从产品、产品类别、产品模型三张表加载并过滤数据,仅保留基于特定标识的最新且唯一的记录;随后选择必要的列并完成关联。重复此前的步骤,使用以下代码:

from pyspark.sql.functions import *

# 加载数据
product = spark.read.table("silver.adventureworks.hist_product") \
.where(col("current") == True)
product = product.dropDuplicates(["ProductID"])
product = product[["ProductID", "Name", "ProductNumber", \
"Color", "Size", "Weight", "ProductCategoryID", "ProductModelID"]]

category = spark.read.table("silver.adventureworks.hist_productcategory") \
.where(col("current") == True)
category = category.dropDuplicates(["ProductCategoryID"])
category = category[["ProductCategoryID", "Name"]]
category = category.withColumnRenamed("Name", "CategoryName")

model = spark.read.table("silver.adventureworks.hist_productmodel") \
.where(col("current") == True)
model = model.dropDuplicates(["ProductModelID"])
model = model[["ProductModelID", "Name", "CatalogDescription"]]
model = model.withColumnRenamed("Name", "ProductModelName")

# 关联
dimension_product = product.join(category, on="ProductCategoryID", how="left")
dimension_product = dimension_product.join(model, \
on="ProductModelID", how="left")

# 选择所需列
dimension_product = dimension_product[["ProductID", "Name", "ProductNumber", \
"Color", "Size", "Weight" , "CategoryName" , "ProductModelName"]]

# 使用所选列添加哈希码
dimension_product = dimension_product.withColumn("ID", \
sha2(concat_ws("||", *dimension_product.columns), 256))

在新的单元格中粘贴以下代码以处理 dimension_product 表的更新:

from delta.tables import *

deltaTable = DeltaTable.forPath(spark, \
'Tables/adventureworks/dimension_product')

deltaTable.alias('gold') \
  .merge(
    dimension_product.alias('updates'),
    'gold.ID = updates.ID'
  ).whenMatchedUpdate(set =
    {
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedInsert(values =
    {
      "ID": "updates.ID",
      "ProductID": "updates.ProductID",
      "ProductNumber": "updates.ProductNumber",
      "Color": "updates.Color",
      "Size": "updates.Size",
      "Weight": "updates.Weight",
      "CategoryName": "updates.CategoryName",
      "ProductModelName": "updates.ProductModelName",
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedBySourceUpdate(set =
    {
      "current_flag": lit("0"),
      "end_date": current_date()
    }
  ).execute()

输入完产品更新代码后,运行笔记本中的单元格。这将于 Lakehouse 的 Gold 层中创建产品维度表。运行后请验证所有维度表是否正确设置。现在可以进入数据项目的最后一步:创建销售事实表。

创建销售事实表(Creation of the fact table for sales)

下面编写脚本以填充事实表——星型模型的核心。事实表存放用于分析的定量数据,并通过外键与维度表相连。练习中,事实表包含与销售相关的数据及关联至维度表的键。外键对多维分析至关重要。

在 Lakehouse 中新建名为 fact_sales 的笔记本。整体流程与产品、客户类似:粘贴代码进行选择、关联与过滤。本步骤包含少量业务逻辑:为每条销售明细计算收入(Revenue)。

使用以下代码构建并填充销售 DataFrame:

from pyspark.sql.functions import *

# 加载数据
orderdetail = spark.read.table("silver.adventureworks.hist_salesorderdetail") \
.where(col("current") == True)
orderdetail = orderdetail.dropDuplicates(["SalesOrderID"])
orderdetail = orderdetail[["SalesOrderID", "SalesOrderDetailID", \
"ProductID", "OrderQty", "UnitPrice"]]
orderdetail = orderdetail \
.withColumn("Revenue",orderdetail["OrderQty"] \
* orderdetail["UnitPrice"] )

orderheader = spark.read.table("silver.adventureworks.hist_salesorderheader") \
.where(col("current") == True)
orderheader = orderheader.dropDuplicates(["SalesOrderID"])
orderheader = orderheader[["SalesOrderID", "CustomerID", \
"BillToAddressID", "OrderDate"]]
orderheader = orderheader \
.withColumnRenamed("SalesOrderID", "SalesOrderID2")

# 关联
sales = orderdetail.join(orderheader, \
orderdetail['SalesOrderID'] == orderheader['SalesOrderID2'], "left")

sales = sales.withColumn('SalesKey', concat(sales['SalesOrderID'], \
sales['SalesOrderDetailID']))

# 选择所需列
sales = sales[["SalesKey", "ProductID", "CustomerID", \
"BillToAddressID", "Revenue", "OrderDate", "OrderQty", "UnitPrice"]]

接下来,为事实表引入来自维度表的代理键。为加速处理,我们将事实表与维度表按业务键(如 CustomerIDProductIDOrderDate 等)关联,并仅使用当前生效的数据。从维度表中选取代理键并重命名以对齐事实表 schema。

代理键对保持数据一致性至关重要:它们是数据库生成的稳定唯一标识,降低对可能随时间变化的业务键的依赖;即便业务键属性发生变更,代理键仍保持不变,保证历史数据连接的稳定;同时还能为不同版本分配不同标识,确保事实表准确连接到对应版本的维度记录。

继续粘贴以下内容:

# 读取维度表的当前数据
dimension_address = spark.read.table("adventureworks.dimension_address") \
.where(col("current_flag") == True)
dimension_customer = spark.read.table("adventureworks.dimension_customer") \
.where(col("current_flag") == True)
dimension_product = spark.read.table("adventureworks.dimension_product") \
.where(col("current_flag") == True)
dimension_date = spark.read.table("adventureworks.dimension_date")

# 使用业务键将事实与维度表关联
fact_sales = sales.join(dimension_address,(sales.BillToAddressID \
    == dimension_address.AddressID), "left") \
    .join(dimension_customer,(sales.CustomerID \
    == dimension_customer.CustomerID), "left") \
    .join(dimension_product,(sales.ProductID \
    == dimension_product.ProductID), "left") \
    .join(dimension_date,(sales.OrderDate \
    == dimension_date.OrderDate), "left") \
    .select(col("dimension_address.ID").alias("AddressKey"), \
    col("dimension_customer.ID").alias("CustomerKey"), \
    col("dimension_product.ID").alias("ProductKey"), \
    col("dimension_date.ID").alias("DateKey"), \
    col("SalesKey"), col("Revenue"), col("OrderQty"), col("UnitPrice"))

最后,处理 fact_sales 表的更新:

from delta.tables import *

deltaTable = DeltaTable.forPath(spark, \
'Tables/adventureworks/fact_sales')

deltaTable.alias('gold') \
  .merge(
    fact_sales.alias('updates'),
    'gold.SalesKey = updates.SalesKey \
    AND gold.AddressKey = updates.AddressKey \
    AND gold.CustomerKey = updates.CustomerKey \
    AND gold.ProductKey = updates.ProductKey \
    AND gold.DateKey = updates.DateKey' \
  ).whenMatchedUpdate(set =
    {
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedInsert(values =
    {
      "SalesKey": "updates.SalesKey",
      "AddressKey": "updates.AddressKey",
      "CustomerKey": "updates.CustomerKey",
      "ProductKey": "updates.ProductKey",
      "DateKey": "updates.DateKey",
      "Revenue": "updates.Revenue",
      "OrderQty": "updates.OrderQty",
      "UnitPrice": "updates.UnitPrice",
      "current_flag": lit("1"),
      "current_date": current_date(),
      "end_date": """to_date('9999-12-31', 'yyyy-MM-dd')"""
    }
  ).whenNotMatchedBySourceUpdate(set =
    {
      "current_flag": lit("0"),
      "end_date": current_date()
    }
  ).execute()

输入并运行上述代码后,Gold 层中的事实表将被创建。请验证结果,应与图 7-2 的示例一致。

image.png

至此,你已完成 Oceanic Airlines 的 Gold 层搭建。该层可支持高性能查询与分析,从数据中挖掘有价值的洞见。还剩最后一步:将这些笔记本纳入数据管道。建议将“创建 Gold 层表”的步骤关联到 Silver 层最后的“历史化”步骤之后;随后加入各维度笔记本(彼此无依赖,可并行运行);最后加入处理事实表的笔记本,完成整体设置。图 7-3 展示了数据管道的最终阶段。

image.png

当你调整完数据管道后,Gold 层即可全面支撑高性能查询与分析,帮助你从数据中获取洞察。

将幂等性作为关键设计原则(IDEMPOTENCY AS A KEY DESIGN PRINCIPLE)
在本练习中,我们始终围绕幂等的数据管道设计:管道可多次运行且不会产生副作用,每次都得到相同结果。比如,若来源系统数据无变化,重新运行管道也不会带来任何改动:相同的 Parquet 文件仍会被摄入到 Bronze 层、相同的 Delta 表会被构建、Silver 层不会新增记录。因为在缓慢变化维(SCD)中,唯有 SCD2 能确保真正的幂等性。幂等设计对保持整条管道的数据一致性与完整性至关重要。

现在,继续构建语义模型并在表之间创建关系。这一步对确保数据模型结构良好、表间交互高效至关重要。通过它,你将提升数据的一致性与可用性,为后续深入而可靠的分析铺平道路。让我们进入最后阶段。

语义模型的创建(Creation of the Semantic Model)

现在 Gold 层已经搭建完成,是时候利用这些数据来创建报表并开展分析了。为此,建议你在工作区中创建一个语义模型。语义模型本质上是一种对数据进行表达与组织的方式,使其更易于理解与分析。可以把它看作是把包含技术性列名的复杂数据模型,翻译成更贴近业务、易于使用的设计。这项强大的能力显著提升了业务用户的自助分析能力,使其无需深入掌握数据处理或查询语言也能与数据交互。此外,语义模型还能与 Power BI、Excel、移动应用以及 Microsoft Power Platform 的其他组件配合使用。

警告
在构建语义模型时必须格外谨慎,因为这会对 Gold 层的整体设计与功能产生重大影响。一旦出错,可能导致低效、成本上升,甚至数据处理与报表中的错误。

在 Gold 层中,你使用了 SCD2 来建表。这意味着你保留了数据变化的完整历史。在表设计中,你包含了 current_flagcurrent_dateend_date 列,以便同时访问数据的当前与历史版本。

不过这里会遇到一个两难:Direct Lake(直接查询 Delta 表的一种模式)存在限制——它是按数据现状直接读取,不支持在查询中使用 SELECTWHERE 子句。这一约束迫使我们考虑替代方案,每种方案各有利弊:

  1. 在 Gold 层创建视图并将其接入语义模型。
    视图中可筛除历史记录,仅保留当前数据。示例 SQL 如下:

    IF OBJECT_ID('adventureworks.v_dimension_customer', 'V') IS NOT NULL
        DROP VIEW adventureworks.v_dimension_customer
    GO
    
    CREATE VIEW adventureworks.v_dimension_customer
    AS SELECT * FROM adventureworks.dimension_customer WHERE current_flag = 1;
    

    该做法确保报表只包含当前数据。但需注意:当语义模型连接到 Lakehouse 的视图时,Direct Lake 会自动降级为 DirectQuery,可能带来性能下降。

  2. 在 Power BI 中使用 Import 模式。
    这种方式允许在导入前进行选择、过滤或处理。尽管会产生数据复制、需要初次加载,牺牲了实时性并可能增加复杂度与成本,但它提供灵活定制与快速访问能力,也能同时服务更多用户而无需反复回源取数。

  3. 在报表端加载所有 Delta 表数据后再应用筛选。
    这种方式较低效:需要在 Power BI 内部执行额外过滤,可能导致性能变慢,也会引发一个问题——如果还要在报表层过滤历史数据,那么 Gold 层是否已做好对外消费的准备?

  4. 调整 Gold 层设计以提升消费效率。
    做法很多。就本练习而言,你已在 Silver 层完成历史化,因此可以考虑把 Gold 层改为仅保留当前数据(重写转换逻辑),降低模型复杂度与提升性能。但这样可能限制某些需要历史数据的未来用例。另一种思路是在保留现有通用数据模型的同时,增加面向特定用例优化的增量表或副本。这样既保留通用治理层,也为特定场景提供专门的“语义层”。代价是需要编写更多管道逻辑,复杂度上升,但能提高复用效率,并让不同用例获得更量身定制的数据模型。

理解上述权衡与适用性对做出合理决策十分关键。对企业而言,最佳实践通常是在 Import 模式优化 Gold 层以更好适配 Direct Lake 之间做取舍。也可以组合策略——例如在 Gold 层既使用视图也适度复制数据——以打造更灵活高效的数据模型,兼顾性能与易用性。

带着这些权衡与实践建议,下面动手创建一个语义模型,包含本练习生成的 Gold 表。该模型要求用户在报表中自行应用筛选,以实现更灵活的探索与分析。

操作步骤:

  1. 在工作区中进入你的 Gold Lakehouse

  2. 在 Lakehouse 资源管理器顶部功能区点击 “New semantic model(新建语义模型)”

  3. 为新语义模型命名(例如 “Sales” )。

  4. 选择要纳入语义模型的 Gold 表:dimension_addressdimension_customerdimension_datedimension_productfact_sales。(如图 7-4 所示)

  5. 建立表之间的关系。例如,将事实表 fact_salesCustomerKey 与维度表 dimension_customerID 关联。其余表按需重复此过程。

  6. 定义关系的基数(cardinality) 。典型做法是维度 ID 到事实表外键为 一对多

  7. 重命名表并隐藏不必要的列,以提升可用性。例如将 dimension_customer 重命名为 customer,并隐藏 IDCustomerID 列。

  8. 在销售表上创建度量值(measure) 。例如创建总营收度量,方法是在 TotalRevenue 列上右键选择 New measure,并在顶部用 DAX 定义:

    TotalRevenue = SUM(sales[Revenue])
    

    点击 Confirm 完成。

image.png

(图 7-4:为维度与事实表创建新的语义模型)

创建完成后,你的语义模型应类似图 7-5 的结构。

image.png

(图 7-5:地址、客户、日期、产品与销售的语义模型总览)

性能提示(Power BI DAX 与建模):
DAX(Data Analysis Expressions)是 Power BI 的公式语言,用于在模型内进行计算与派生信息。但大量复杂 DAX 也会带来计算开销。因此建议尽可能在数据源侧预计算,再导入 Power BI。同时,优化 DAX(减少上下文切换、选用高效函数)能显著提升性能。良好的模型设计(尤其是星型模型)与正确的关系基数设置,会让 Power BI 报表与其他消费应用更快速且可扩展

当你在语义模型中建立好关系与度量后,就可以在 Power BI 中创建报表与仪表板了——这也标志着本次练习的最后一个开发环节完成。

创建首个 Power BI 报表(Creation of the First Power BI Report)

Power BI 是一套软件服务,帮助用户可视化数据、在组织内共享洞见,或将其嵌入应用和网站。它允许连接各种数据源,将数据转换为模型,并创建美观的可视化报表。比如,你可以在 Lakehouse 数据之上制作交互式报表并与同事分享。

本练习中,你将把语义模型作为数据源。开始创建报表:点击 New Report(新建报表) 并等待加载。报表画布打开后,从表中拖拽字段到画布上。例如,从 sales 表中拖入 TotalRevenue 字段,从 customer 表中拖入 StateProvince 字段。

接着创建可视化。在可视化窗格中选择簇状柱形图并拖到报表中。对可视化进行布局与格式设置,使数据更清晰、更有吸引力。

完成设置后,你的最终报表应类似图 7-6 所示。

当你对报表的布局与内容满意后,将其保存(例如命名为 RevenuePerState),并发布到工作区。该报表将直接连接到 Lakehouse 的 Gold 层,确保始终反映最新数据。

此时你可以将报表分享给组织成员,或把它嵌入到 PowerPoint 演示文稿中。链接至 PowerPoint 的好处是:信息与 Lakehouse 的 Gold 层保持连接,既能轻松保持演示数据的实时更新,又能遵循 Lakehouse 定义的安全实践与策略。

image.png

图 7-6. Power BI 示例报表:按州/省汇总的总收入

创建任务流(Creation of Task Flows)

Microsoft Fabric 提供 任务流(task flows) 功能,用于在工作区内可视化工作流。它有助于理解不同项目之间的关系与协作方式,便于在工作区复杂度提升时进行导航。

创建任务流时,将滑块调至中间位置(如图 7-7),然后开始把任务拖入流程中。每个任务可代表不同类型的工作,如数据处理、存储或可视化。该可视化工具适合记录你在本练习中执行的各个步骤。

设置完成后,任务流会自动显示在你的工作区内。图 7-7 展示了如何对整个流程进行文档化的示例。

image.png

图 7-7. 工作区内各项活动的画布式呈现

回顾与 Gold 层设计增强建议(Enhancements for Gold-Layer Design)

你已接近教程尾声。可以看到,星型模型设计既简洁又高效:它为数据提供清晰简明的视图,加快分析与决策。最初 AdventureWorks 源库中的数据采用复杂的 3NF 结构,非开发人员使用困难。通过将其转化为更友好的结构,你同时提升了可读性与性能。维度建模也有助于为 BI 工具构建多层模型。

不过,你仍可从以下方面增强设计:

为非 SQL 用户简化
对不熟悉 SQL 的终端用户或数据科学家来说,一张“大表”可能比星型更易理解(尽管在查询与维护上未必更高效)。此外,在语义模型中使用易懂的表名与列名也能帮助非 SQL 用户。因此,你的 Gold 层可以增加更易用的子层

提升数据质量
建议在 Silver 层清洗与关联之后,在 Gold 层再增加一次数据质量步骤,以确保集成数据集的构建成功。
此外,为了保证能准确关联记录,你可以在 Silver 层为每个维度灌入一个占位记录(如各列均为 0)。当事实表存在无法在维度里找到的 ID 时,该占位记录能帮助发现缺失数据并确保连接仍可成功。

增强性能
对模式添加分区可减少查询扫描数据量、提升性能。还可以为维度哈希值建立更小的查找表,以减少连接时的扫描数据量。

加入审计与处理列
在事实与维度表中增加 created_atupdated_atsource_system 等列,便于追踪数据血缘与排障。

实施数据安全
可引入行级安全相关列,基于角色/权限限制访问。例如在 AdventureWorks 中加入 HumanResources.Employee 表,按部门限制访问;配合 USER_NAME()Employee.LoginID 匹配控制可见性。参阅 Microsoft Fabric 与 Power BI 的行级安全文档。

考虑增加子层
根据数据复杂度与用户需求,可在 Gold 层增加更多适用性(fit-for-purpose)子层。不同应用对结构、粒度或汇总层级的要求不同;可按使用特性独立优化(如按分区维度拆分)。对共性需求,可先建公共集成层(统一维度与常用数据),再为特定用例加专门子层。

考虑物理服务层(serving layer)
有时需要把 Gold 层的数据复制到一个或多个服务中,以便终端用户更易访问(如 Azure Cosmos DB、Azure SQL Database 或实时智能数据库)。选择服务需要综合一致性、可用性、缓存、时效性、索引等多种因素进行权衡。典型的 Lakehouse 架构常由多种技术服务组合而成(如无服务器 SQL、列存、关系库、时序库等)。

引入低代码/零代码工具
为特定用户群体,可在 Gold 层之上提供低/零代码层,让用户无需编写笔记本或 SQL 即可自助转换数据。
在 Microsoft Fabric 中,可使用 Dataflow Gen2(见图 7-8)实现可视化、低代码的数据转换。

image.png

另有 Data Wrangler 工具(见图 7-9),与 pandas 集成,带可视化,便于加速数据探索与准备。

image.png

加入 Apache Airflow 编排
视组织需求,可用 Airflow 编排数据管道(参见“Orchestration with Apache AirFlow”),以实现自动化、调度与监控。

特征工程与机器学习
将 Silver 与 Gold 层面向特征工程与实验进行设计;必要时增设额外的 Lakehouse 与 Spark 环境以支持这些活动。

综合这些改进,并把语义模型纳入设计,你可以使 Gold 层更贴合用户需求、也更能应对数据复杂性。最终,Gold 层的形态取决于组织成熟度与数据战略、关注重点以及数据的使用方式。

接下来,我们将以要点总结与对 Microsoft Fabric 使用的反思作为本次练习的收尾。随后,我们将探讨数据产品的理念,以及如何使用 Microsoft Purview 来治理这些数据产品。

Microsoft Fabric 实战(Microsoft Fabric in Practice)

在本教程的尾声,我们用 Microsoft Fabric 为 Oceanic Airlines 构建了 Medallion 分层,并强调了它在大规模数据运营中的能力。需要指出的是,Microsoft Fabric 不仅仅包含 域(Domains)工作区(Workspaces)Lakehouse。它还整合了 Copilot、各类 AI 工具时序 KQL 数据库SQL 数据库实时分析事件流机器学习,以及与 Microsoft Purview 的集成,并将这一切封装进统一的 SaaS 体验中。

这种一体化、简化的使用方式降低了新手上手门槛,但可定制性相对较弱。对于需求非常特定、或需要对数据运营进行更细粒度控制的组织来说,Fabric 的“All-in-One”模式可能显得有些受限。再者,作为相对较新的平台,Microsoft Fabric 在某些场景下可能仍存在功能空缺。理解各平台之间的差异,对于认识它们如何支撑不同的应用场景、以及为何市场同时存在独立与混合方案,至关重要。

从 Microsoft Fabric 在数据架构方面的能力出发,我们有必要进一步探讨**数据产品(Data Products)**这一概念,以及其在这些架构中的应用。数据产品本质上是可复用的构件,旨在在组织内的多种语境下易于消费、具备可扩展性并能产生价值。它们对于产出可行动的洞见、推进业务决策与运营至关重要。

Medallion 架构中,数据产品是核心组成部分。数据产品通常位于 Gold 层,在那里它们被精炼并优化,以便终端用户或下游应用消费。接下来,让我们更深入地理解数据产品的内涵,并探讨在 Gold 层中设计与管理数据产品的最佳实践。

数据产品(Data Products)

在“将银层数据作为产品”中,我们介绍了数据产品的概念,强调了其作为宝贵资产需要精心管理与关注。在 Medallion 架构中,Gold 层对数据产品的定义与创建至关重要。该层的目标是让数据可被终端用户与应用直接使用,确保“可用于业务”。

警告
行业里并不存在放之四海而皆准的数据产品标准——没有统一的分类法、元数据标准或互操作标准。更让人困惑的是,很多观点偏概念化与理论化。比如,关于数据产品是否必须同时包含数据、元数据、处理代码与支撑基础设施,仍在争论不休。
建议: 制定你自己的标准与指引。不要等待外部统一标准;主动建立适合你组织的规范,以保证各团队的一致性与清晰度。

不同组织在 Gold 层界定与设计数据产品的做法并不一致。小型组织通常聚焦于将可消费的数据暴露为洞见。这些数据往往融入更广义的数据湖仓架构中,并不严格区分“用例特定数据”与“数据产品数据”。此时,数据目录只需将标记为“可消费”的一切资产罗列出来即可。

大型组织通常采取另一种策略:他们会设定标准,将用例特定数据与面向更广泛消费的数据区分开来。可选做法包括:在 Gold 层内创建独立子层、在目录中清晰打标数据产品,或将其存放在单独的 Lakehouse 实体中。另一个办法是数据虚拟化:通过视图或快捷方式构建虚拟层,而非物理拷贝。我们会在“分离的数据产品层”中进一步探讨这些实践。

无论采用哪种方式,想要高效地开发与管理数据产品,清晰的指引都是必要的。若缺乏统一规范,分散的团队容易产出略有差异且互不兼容的数据模型;有时还会生成过度聚焦单一用例的数据,导致复用困难与点对点接口膨胀。要化解这些问题,就必须以可复用性为目标来设计数据产品;而有效的数据建模是达成通用性的关键,却常常被忽视。

此外,“什么是数据产品”“什么叫可复用”也常被混淆。下表(表 7-1)给出了一些常见的实体化示例,并说明背后的考量。

表 7-1 数据产品的示例性表现形式

场景适用度最佳实践 / 设计考量
Bronze 层的原始系统数据不足会造成强耦合;应在 Silver 层处理以降低依赖。
Gold 层中的一张 Delta 表最优设计为可复用的数据集(如“一张大表”),便于高效、通用访问。
Gold 层中多个 Delta 表组成的维度模型典范优化为易消费、尽量少 JOIN 的模型以提升性能与可用性;确保文档完善、易理解;并与领域特定工作负载解耦。
报表勉强报表强依赖上下文;更适合本地共享
AI 模型勉强模型难以通用,优先共享其底层数据
语义模型最优面向广泛适用复用进行设计。
Kafka / Event Hub 主题够用使用承载状态的事件,并确保复用性与兼容性。
存放 PDF 的文件夹不足非结构化数据难复用且需追加处理;应提供带元数据的标记化成品(详见第 13 章)。

为避免问题与误解,必须为跨团队的数据产品创建明确的标准与指引。这些指引应聚焦互操作性、数据质量、数据建模、元数据与治理等关键方面。以下是这些指引应覆盖的重点:

数据产品指引引言

由于行业缺乏统一标准,更凸显建立公司内部标准的重要性。请清晰阐明:数据产品的定义、指引的目的、以及在实践中的用法。在 Medallion 架构语境下,可将数据产品定义为一种可复用的逻辑实体,通常由去范式化的 Delta 表语义模型引用,并易于被消费。

同时需要阐述数据产品管理的业务动因:为何需要健壮的数据产品设计,以及数据所有权数据质量的重要性。

数据产品的类型

可按成熟度与原则划分不同类型的数据产品。例如,将运营型数据产品(位于 Silver 层)与分析型数据产品(位于 Gold 层)区分管理。

数据建模指引

为确保数据在组织范围内的一致性、可靠性与可用性,需要强有力的建模标准。可考虑:

  • 采用关于粒度参考数据数据类型模式细节主外键分类的最佳实践。
  • 要求原子化数据,以便在数据目录中正确关联业务术语。
  • 妥善处理拼接字段(例如在销售系统以 Category_Code + Product_ID 拼接生成 SKU,如 “ELC-00123”)。
  • 提供域内 / 跨域主键管理指南,简化对接。
  • 制定应对缺失关键字段不完整记录的策略。
  • 给出本地到本地全企业参考映射的指导。
  • 为安全目的封装或提供必要元数据。
  • 谨慎使用保留列名;并对历史管理给出清晰策略(追加、覆盖、合并、结构变更)。
治理指引

数据治理对于质量、合规与安全至关重要,可考虑:

  • 规范正确编目(Cataloging)的方法。
  • 明确组织内的角色与职责,以及开发、接入与登记流程。
  • 识别唯一外部数据源的策略。
  • 制定数据质量指引,包括源系统问题修复流程
  • 设定多团队共享数据产品的管理规范。
  • 管理遗留仓库快照的指南。
  • 明确**主数据管理(MDM)**策略,包括在数据产品中纳入主标识。
  • 处理重投 / 覆盖交付的规则。
  • 规定升级路径与使用讨论板进行问题跟踪。
  • 描述数据产品从创建到退役的生命周期与版本管理
  • 规范消费流程:访问申请、审批与开通路径。

理解数据产品用例特定数据之间的差异,是实现有效数据管理的关键。在第 12 章中,我们将探讨数据产品在规模化运营中的核心作用,并深入阐述在 Medallion 架构下开展稳健数据治理的重要性,帮助你提升组织效率与数据价值。

在理解了数据产品在 Medallion 架构中的概念与实践之后,下一个关键议题是:这些数据资产如何治理。若没有稳健的数据治理框架,数据管理就不算完成——必须确保全局的质量、合规与安全。这正是诸如 Microsoft Purview 之类数据治理解决方案的用武之地。

使用 Microsoft Purview 实施数据治理

Microsoft Purview 提供了一套面向高效管理与监控数据的功能。它作为数据治理与数据安全解决方案,能在整个数据生命周期内简化管理、提升可见性与可控性。在数据治理方面,平台提供 Microsoft Purview 统一目录(Unified Catalog) ,内含血缘追踪数据产品关键数据要素(CDE)目标与关键结果(OKR)以及数据质量度量等能力,帮助组织从多个维度对数据资产进行监管。

澄清“域(Domain)”概念的歧义

“域”的概念因**领域驱动设计(DDD)**而广为人知。DDD 是一种面向大型组织复杂系统的方法论,对现代软件与应用开发(包括微服务与数据网格)影响深远。

在 DDD 语境中,是组织试图解决的特定问题空间,囊括知识、行为、规则与活动,并体现语义耦合——包括团队、系统或服务之间的组织或行为依赖。为便于管理与澄清边界,域常被细分为子域,以贴合组织的不同侧面。

然而,“域”的使用常因解读差异而引发歧义。例如:

  • 业务域:公司的核心活动、优先级、应用与数据。
  • 治理域:通用治理、所有权、以及对数据产品与关键业务概念(如术语表)的编目。
  • 数据域:采集、处理、融合与分发数据的边界。
  • 技术域 / 应用域:支撑特定业务职能的技术与应用。

尽管以 DDD 来划定边界十分流行,但我对其在数据网格中的应用持保留意见,原因在于其对软件方法的强依赖。相比之下,我建议基于业务能力来定义边界,这通常更务实、也更利于明确职责与角色。另一种可行路径是借鉴**国际知识组织学会(ISKO)**的原则来清晰界定这些领域。

需要注意的是,域之间可能重叠或合并,增加管理复杂度:一个业务域可能依赖多个数据域,而这些数据域又可能依赖多个技术域;反过来,多个业务域也可能被归入更大的治理域。因此,必须精准界定每个域的边界,避免重叠与意外合并,从而维持组织效率与一致性。

Microsoft Purview 中,“域”用于技术与业务层面的数据管理组织化。它在不同场景中被用作分组元数据与开展治理任务(如数据所有权、数据发现)的边界。下文将结合 Medallion 架构,重点说明 Purview 中的“域”。

Microsoft Purview 设计考量

先从基础开始:创建一个 Microsoft Purview 帐户。随后聚焦治理域集合(Collections)的设置——这是启用 Purview 的第一步,对管理数据产品业务概念至关重要。

你可以参考如下入门资源以获得良好起点:

  • Get Started with the New Data Governance Experience in Microsoft Purview
  • Set Up Your Governance Domains

提示
我与 Sarath Sasidharan 共同维护了 YouTube 频道 Data Pancakes,会讨论包括使用 Microsoft Purview 进行数据治理在内的各类数据话题。

在 Purview 中,两个概念尤为关键:治理域(Governance Domains)集合(Collections) 。治理域用于管理数据产品与业务概念;集合用于在技术扫描过程中聚合元数据。下文分别展开。

治理域(Governance Domains)

在一个“扁平”的数据目录中,每个数据资产都是独立条目,数据所有者与数据管家难以高效治理。而数据会随组织发展不断增长,管家需要可扩展的工具来管理激增的资产。

这正是治理域发挥作用之处。它本质上是组织级通用治理的边界,具有很强的灵活性,可按组织实际来设定。例如,你可以为财务部建立治理域,或围绕特定主题域(如客户数据产品信息)建立治理域。

如图 7-10 所示(Microsoft Purview 的治理域示例),页面展示了数据所有权数据产品业务概念(术语表、OKR、关键数据要素等)信息。治理域明确边界,提供业务语境与技术资产的全景视图;这些域与数据产品相连,而数据产品再关联到底层数据资产,这些资产则归集在集合之中。

image.png

图 7-10. Microsoft Purview 的治理域总览页

集合(Collections)

集合用于管理与组织技术域的元数据。在此层面,你可以分配数据源管理员集合管理员等角色来负责数据资产管理。该配置对扫描并管理源系统与应用的元数据至关重要。

7-11 展示了 Purview 中集合结构的组织方式。示例采用 Oceanic Airlines 项目:根集合下包含 Microsoft FabricAzure Databricks 等共享服务。将共享服务置于层级较高位置有两点原因:

  1. 同一 Purview 帐户内数据源不可重复注册,每个唯一服务在集合层级中只能注册一次
  2. Purview 的元数据仅能向“子集合”下发,不能横向分发给“兄弟集合”。因此,把共享服务(如 Microsoft Fabric)放在更高层级,有助于元数据面向下层集合有效分发。

示例中还有一个 “Operational systems(运营系统)” 集合,用于聚合如 AdventureWorks 的 Azure SQL 数据库等运营系统的元数据。大型组织通常会有多个运营系统,每个可建立独立集合。

最后, “Lines of Business(业务线)” 集合用于按业务域(如销售域)聚合元数据。截图中它看似为空,但这是为后续平台(如 Microsoft Fabric)扫描入库的元数据预留对齐位置

image.png

图 7-11. Microsoft Purview 的数据源总览页

将 Unity Catalog 与 Microsoft Purview 集成

Unity Catalog 是 Azure Databricks 的数据治理工具(第 12 章详述),更偏向 Azure Databricks 内部的运营级治理;而 Microsoft Purview 提供跨技术平台的更广义治理视角。若你的治理需覆盖多种技术与平台,Purview 更为合适。

两者也可集成以增强能力:在 Purview 中发现 Databricks Workspace 的数据并可视化血缘,并将其组织进集合中。其扫描流程与扫描 Microsoft Fabric 类似:注册 Unity Catalog → 选择要扫描的 Databricks Workspaces → 归类到目标集合。连接与扫描的详细说明可参见官方文档。

针对 Microsoft Fabric 的技术源扫描,可使用范围扫描(Scoped scanning) :不仅能挑选要扫描的 Workspace,还可指定扫描结果落入的集合。例如,销售域相关的 Fabric Workspace 元数据可直接放入 Sales 集合。图 7-12 展示了范围扫描的配置示例。

扫描完成后,元数据即可供目录用户使用,涵盖数据资产的技术细节(如管道、Lakehouse、表、列等)。你可以据此描述数据产品。下节将深入说明在 Microsoft Purview 中的数据产品及其与 Microsoft Fabric 数据资产的关系。

image.png

图 7-12. Microsoft Purview 的范围扫描配置示例

Microsoft Purview 中的数据产品

完成集合结构与技术域元数据的扫描后,下一步就是创建数据产品。实质上,这是把**技术元数据(集合结构)业务概念(治理域)**关联起来——这正是数据产品的作用所在。

Microsoft Purview 中,数据产品逻辑实体:将一个或多个数据资产按某一业务目的进行分组。它是治理域与集合之间的连接枢纽,帮助组织高效管理、治理与推广数据,并确保质量、血缘与合规。图 7-13 给出了示例截图。

image.png

在图 7-13 中,你可以看到数据产品所属域、产品所有者、健康动作、更新频率及所用数据资产。若权限允许,还会出现**“请求访问(Request access)”按钮,便于用户申请访问、确保安全与合规。截图中未展示的是血缘信息**,它对理解数据来源与变换至关重要。数据产品中还可能包含业务术语关键数据要素OKR,有助于理解数据语境与用途。

Purview 的数据产品具备很强的灵活性:可存放各种条目——表、文件、Power BI 报表、机器学习模型等,并不限于 Lakehouse 的 Delta 表。你还可以将数据产品按类型分类(如 datasetoperationalbusiness system/application 等),并可让多个数据产品共享同一数据资产,方便复用与管理。

但也正因灵活,清晰的标准与边界尤为重要。正如前文所述,你需要定义数据产品的范围关联方式统一定义,否则目录中可能充斥着装入技术系统表的数据产品,或同一数据资产却对应多个数据产品所有者。这再次凸显制定明确指引的重要性。

小结一下:在 Purview 中,数据产品是按业务目的对一个或多个数据资产进行分组的逻辑实体,是治理域的连接枢纽,帮助组织有效治理与管理数据,确保质量、血缘与合规。

既然已覆盖数据治理与 Purview 的基础概念,下面看看它如何有效管理 Medallion 架构

Medallion 架构实施指南

让我们看看在 Medallion 架构中,Microsoft Fabric 与 Microsoft Purview 如何在数据管理层面衔接。先从 Microsoft Fabric 说起。正如你在第 5 章所看到的,域(domain)是一种按逻辑将组织内与某一领域相关的所有数据归组的方法。为便于按域归组数据,会将 Workspace(工作区) 关联到域。在该语境下,域可以被视作技术域数据域。在同时使用 Microsoft Fabric 和 Microsoft Purview 等服务时,请务必统一域的术语,因为不同产品中的含义可能不同。

Microsoft Purview 的语境中,**治理域(governance domain)用于定义一个边界,将特定业务概念与数据资产纳入统一的所有权之下。Microsoft Purview 还引入了集合(collection)**的概念。

在集合界面里也会出现“domain”一词,但含义不同:这里的 domain 指在扫描源系统与应用时,用于组织技术元数据的方法。这种用法与 Microsoft Fabric 中的域更为接近,但不要将其与治理域混淆

关键问题是:Microsoft Purview 与 Microsoft Fabric 中不同类型的“域”如何与整体架构中的“业务域”协同

图 7-14 展示了不同业务域之间的交互。例如,本章实践中的 销售(Sales)业务域。还有另一个名为“航班运行管理(Airflight Operations Management)”的业务域,它使用另一套 Medallion 架构来处理自己的源系统。最后是 消费者服务(Consumer Services)业务域。与其他不同,它没有自己的源系统,而是依赖销售与航班运行管理两个业务域提供的数据,并将这些数据整合到它自己的 Medallion 架构中。因此总共有三套 Medallion 架构

为实现有效的数据治理,每个业务域都会在 Microsoft Purview 中对齐到自己的治理域。这些治理域位于图 7-14 的顶部。每个治理域负责数据产品与业务概念的治理。从治理视角看,治理域贯穿数据全生命周期,从运营系统直至数据产品。

再往下一层是集合,用于组织数据资产的元数据。保持集合与治理域之间的清晰关系非常重要,以避免边界重叠或合并。在该设计中,运营系统分析服务是分离的。这确保了运营系统的元数据与数据产品的元数据分开管理,更易治理。

从中部到底部可以看到,集合与源系统Microsoft Fabric 的 Workspaces 相连。这确保了元数据与架构中具体部分的数据准确关联。源系统与 Workspaces 共同构成通常所说的业务域——数据在其中被业务方实际使用与管理,例如销售业务域。

消费者服务 业务域是个例外:它不从自身源系统采集数据,而依赖销售与航班运行管理业务域的数据。因此,它只有数据域,没有应用域

此治理结构的要点是:在本例中,治理域覆盖整个数据生命周期。它既管理运营系统(即应用域)中的数据,也管理用于分析(即数据域)的数据。这通常涉及跨横向团队,从运营系统到数据产品一体化管理。唯一的例外是消费者服务业务域,它没有自己的源系统,而是复用其他域的数据。

image.png

图 7-14. Microsoft Purview 的业务域、治理域与集合,以及 Microsoft Fabric 的数据域与工作区之间的关系(高亮显示同时管理应用域与数据域的销售业务域)

当然,你也可以用不同方式来组织。例如,你可以为运营系统与分析服务分别创建独立的治理域。这样便于分别管理运营数据与数据产品。该策略在源系统由应用团队管理、数据产品由数据工程团队管理的组织中尤为有用。

另一种方式,是将所有与应用域相关的集合进行归组,并将它们关联到一个或更大的治理域。如果一个中央 IT 或大型团队统一管理所有源系统与应用,这种安排会更顺畅。

这些洞见对 Medallion 架构意味着什么?要落地有效的数据治理,必须让架构各层与团队角色与责任对齐

先定义你的目录目标并制定数据架构落地计划。这一步有助于有效组织与管理域,并为粒度设定清晰标准,避免混淆与重叠。接着评估 Medallion 架构中的工作区、分层与数据产品结构。思考如下关键问题:Bronze、Silver、Gold 三层是否总在同一工作区?这些工作区是否与特定集合与治理域对齐?是否需要为特定用例或业务单元设置额外层或独立工作区? 还要评估这些调整是否需要新建治理域。思考这些问题将有助于建立稳健的治理框架。可参考以下场景:

去中心化管理
每个业务域管理自己的源系统,并维护一套独立的 Medallion 架构(含开发、测试、生产多个工作区)。建议为每个业务单元或部门建立独立治理域以确保良好对齐。

集中工程、分布式使用
单套 Medallion 架构包含多个工作区与 Lakehouse 实体。例如,一个工程团队统一负责采集与清洗;同时为不同团队设置附加工作区,针对各自业务用例进行数据融合。在该场景下,多个治理域可能关联到同一套集合结构

集中式管理
中央 IT 团队管理所有源系统、应用与数据集成。此时可将所有与应用域相关的集合统一组织,并连接到若干治理域。

混合方式
结合去中心化与集中式管理。例如,对关键数据域实施集中管理以加强对核心数据资产的控制与合规;而对不那么关键的数据域,允许业务单元各自管理以提升敏捷性。

通过权衡这些场景,你可以选择最契合组织需求与运作方式的结构,确保架构顺利有效落地。无论哪种方式,清晰的指引都至关重要,以维持一致性并避免混乱。

务必制定一个与组织数据战略对齐的治理框架。首先明确数据所有权——清楚界定数据管理中的内部角色。评估你是走向去中心化,还是需要一个中心化的数据治理团队来监管关键数据。回答这些问题有助于细化治理框架,并可能为你的架构引入新的要求。

提示
如果你希望严格限定 Microsoft Fabric 中的数据资产与数据产品的对应关系,可以考虑使用我开发的 Purview-Bulk-Collection-Mover 应用。这个小型 Web 应用支持在集合之间批量移动元数据,从而确保某集合中的元数据仅指向 Medallion 架构中某一特定层的数据。

最后,考虑需要建立哪些流程与访问控制策略,以确保只有授权用户才能访问数据。比如,你可以预配空工作区并通过快捷方式(shortcuts)指向数据,只允许用户通过只读接口访问。这样用户无法直接接触数据,只能在访问控制策略的约束下通过快捷方式获取。关于如何有效管理与扩展的更多策略,请参见第 11 章。本章将帮助你做出提升架构功能性、效率与安全性的明智决策。

结论

在贯穿 Medallion 架构的旅程中,你从 Bronze 层的数据引入开始,进入更精炼的 Silver 层,最终抵达复杂的 Gold 层。核心目标在于提升数据质量、增强性能,并让数据架构与业务目标对齐。

Gold 层在有效决策与洞察产出中至关重要。通过构建该层,你确保数据准确、可用,并为高性能查询与分析做好优化。你还学习了使用 Power BI 构建报表,并考察了 Microsoft Purview 的数据治理能力。以下是本章的要点回顾:

  • Gold 层需要实现复杂业务逻辑,以准确反映业务需求,并确保数据模型既有意义又易使用。在此背景下,基于业务影响、可行性与资源可用性为用例设定优先级,并定期审查、集中管理可复用的重叠业务逻辑。
  • 在构建报表与语义模型时,要在湖仓的数据获取模式之间做好平衡:使用 Import 模式以获得最佳性能与复杂计算支持;使用 DirectLake / DirectQuery 获取近实时数据。对直接查询而言,你可以使用预定义视图或在报表中直接应用筛选。不同模式各有影响,请谨慎选择。
  • 数据治理方面,审慎评估治理域与集合的数量及粒度,制定与组织数据战略对齐的治理框架,明确角色分工、数据接入/登记流程,以及数据产品与业务概念的管理责任。
  • 数据产品行业尚无统一标准。请制定自有标准:为数据产品设计提供清晰指引,确保全公司范围的一致性与清晰度。指引应涵盖如何组织结构、如何让业务用户易获取等。也可加入具体技术实践,例如在创建 Delta 表时启用 optimizeWrite 以支持 V-ordering
  • 可考虑将特定用例数据数据产品数据****分离(不同工作区与 lakehouse 实体),或在数据目录中进行标识区分
  • 制定明确的访问控制策略,仅允许授权用户访问敏感数据。例如,预配空工作区并通过快捷方式提供只读访问。本书第四部分会继续讨论这些主题。
  • 搭建 Gold 层不仅是技术任务,更是将技术与业务愿景与数据战略相匹配的战略工程

至此,第二部分告一段落——Gold 层标志着 Medallion 架构构建旅程的终点。我们走过了很长的一段路:你从 Bronze 层开始,使用 Data Factory 进行数据引入与设置;随后进入 Silver 层,通过 Airflow 管理流程,打磨 SQL 与 PySpark 能力;最后在 Gold 层构建维度模型并用 Power BI 使能报表。

本教程只是设计 Medallion 架构的一种方式。根据具体需求与细节,会有多种变体(第 11 章将进一步探讨)。

最后补充一句:在完成本书前,我曾重做 Bronze 与 Silver 的练习部分。由于某些服务无法继续使用,我迅速调整并在几天内替换了相关指导。这段经历凸显了在瞬息万变的技术世界中,保持适应性、拥抱可移植性与采用开放架构的重要性。

让我们把这一部分的经验带入来自一线的实践洞见。在第三部分中,我们将学习真实企业如何落地他们的 Medallion 架构,为你的实践提供可直接借鉴的宝贵经验。