注释
隐私级别目前在 Power Platform 数据流中不可用,但产品团队正在努力启用此功能。
如果长时间使用 Power Query,则可能遇到过它。 当你拼命查询时,突然收到一个错误,而无论是在线搜索、调整查询,还是不断敲击键盘,都无法解决这个问题。 错误示例:
Formula.Firewall: Query 'Query1' (step 'Source') references other queries or steps, so it may not directly access a data source. Please rebuild this data combination.
或者:
Formula.Firewall: Query 'Query1' (step 'Source') is accessing data sources that have privacy levels which cannot be used together. Please rebuild this data combination.
这些 Formula.Firewall 错误是 Power Query 数据隐私防火墙(也称为隐私防火墙)的结果,有时它似乎只是为了让全球的数据分析员感到挫败。 无论你信不信,防火墙确实具有重要的作用。 在本文中,我们深入探讨其工作原理。 通过更深入的了解,你希望将来能够更好地诊断和修复防火墙错误。
它是什么?
数据隐私防火墙的用途很简单:它的存在是为了防止 Power Query 意外地在源之间泄露数据。
为什么需要这样安排? 我的意思是,你当然可以创建一些将 SQL 值传递给 OData 流的 M。 但这将是有意的数据泄露。 混搭作者会(或至少应该)知道他们是在这样做。 为什么需要防止意外数据泄露?
答案? 折叠。
折叠?
折叠操作是指将 M 中的表达式(例如筛选器、重命名、联接等)转换为针对数据源(如 SQL、OData 等)执行操作的术语。 Power Query 的强大功能的重要组成部分在于,Power Query 可以通过用户界面将用户通过用户界面执行的作转换为复杂的 SQL 或其他后端数据源语言,而无需用户知道上述语言。 用户可以轻松使用用户界面,享受本地数据源操作的性能优势,而且所有数据源都可以使用一组通用命令进行转换。
作为折叠的一部分,Power Query 有时可能会确定执行给定混合的最有效方法是从一个源获取数据并将其传递给另一个源。 例如,如果要将小型 CSV 文件联接到大型 SQL 表,则可能不希望 Power Query 读取 CSV 文件,读取整个 SQL 表,然后将它们联接在本地计算机上。 你可能希望 Power Query 将 CSV 数据内联到 SQL 语句中,并要求 SQL 数据库执行联接。
这是意外数据泄露的发生方式。
假设你正在联接包含员工社会安全号码的 SQL 数据以及外部 OData 源的结果,并且突然发现 SQL 中的社会安全号码被发送到 OData 服务。 坏消息,对吧?
这种情景是防火墙旨在防止的。
它的工作原理是什么?
防火墙存在以防止从一个源意外发送到另一个源的数据。 足够简单。
那么,它如何完成此任务?
它通过将你的 M 个查询划分为称为分区的部分,并强制执行以下规则来实现:
- 分区可能访问兼容的数据源,或引用其他分区,但不能同时引用这两个分区。
简单。。。然而令人困惑。 什么是分区? 是什么使两个数据源“兼容”? 为什么防火墙应该关心分区是否想要访问数据源并引用分区?
让我们逐步分解并逐一查看上一条规则。
什么是分区?
在最基本的级别,分区只是一个或多个查询步骤的集合。 当前实现中最精细的分区是单步。 最大的分区有时可以包含多个查询。 稍后将对此进行详细讲解。
如果不熟悉步骤,可以在“ 已应用的步骤 ”窗格中选择查询后,在 Power Query 编辑器窗口右侧查看它们。 步骤记录您将数据转换为最终形状所做的一切。
引用其他分区的分区
使用防火墙评估查询时,防火墙会将查询及其所有依赖项划分为分区(即步骤组)。 每当一个分区引用另一个分区中的内容时,防火墙会将引用替换为对调用 Value.Firewall的特殊函数的调用。 换句话说,防火墙不允许分区直接访问对方。 已修改所有引用以确保它们能够通过防火墙。 将防火墙视为守门员。 引用另一个分区的分区必须获取防火墙执行此作的权限,防火墙控制是否允许引用的数据进入分区。
这一切都可能看起来相当抽象,所以让我们看看一个示例。
假设你有一个名为 Employees 的查询,该查询从 SQL 数据库拉取某些数据。 假设你还有一个查询(EmployeesReference),它只是简单地引用了 Employees。
shared Employees = let
Source = Sql.Database(…),
EmployeesTable = …
in
EmployeesTable;
shared EmployeesReference = let
Source = Employees
in
Source;
这些查询最终分为两个分区:一个用于 Employees 查询,一个用于 EmployeesReference 查询(它引用 Employees 分区)。 使用防火墙评估时,将重写这些查询,如下所示:
shared Employees = let
Source = Sql.Database(…),
EmployeesTable = …
in
EmployeesTable;
shared EmployeesReference = let
Source = Value.Firewall("Section1/Employees")
in
Source;
请注意,对 Employees 查询的简单引用被对Value.Firewall的调用所替代,该调用向Value.Firewall提供 Employees 查询的全名。
评估 EmployeesReference 时,防火墙会截获对Value.Firewall("Section1/Employees")的调用,并且现在有机会控制所请求的数据是否流入 EmployeesReference 分区,以及以何种方式流入。 它可以执行任意数量的操作:拒绝请求、缓冲请求的数据(这会阻止任何进一步将其折回到原始数据源的操作发生),等等。
这是防火墙如何保持对分区之间数据流的控制。
直接访问数据源的分区
假设使用一个步骤定义查询查询 1(请注意,此单步查询对应于一个防火墙分区),并且此单步访问两个数据源:一个 SQL 数据库表和一个 CSV 文件。 防火墙如何处理此问题,由于没有分区引用,因此没有调用 Value.Firewall 来拦截? 让我们查看前面所述的规则:
- 分区可以访问兼容的数据源,也可以引用其他分区,但不能同时引用这两个分区。
若要允许运行单分区双数据源的查询,其两个数据源必须是“兼容的”。换句话说,这两个数据源之间的数据需要能够进行双向共享。 这意味着,这两个源的隐私级别都需要是“公开级别”或“组织级别”,因为这两种组合是唯一允许在两个方向共享的组合。 如果两个源都标记为“专用”,或者一个标记为“公共”,另一个标记为“组织”,或者使用隐私级别的其他一些组合进行标记,则不允许双向共享。 在同一分区中评估两者是不安全的。 这样做意味着可能会发生不安全的数据泄漏(由于折叠),防火墙无法阻止它。
如果尝试访问同一分区中的不兼容数据源,会发生什么情况?
Formula.Firewall: Query 'Query1' (step 'Source') is accessing data sources that have privacy levels which cannot be used together. Please rebuild this data combination.
希望你现在更好地了解本文开头列出的错误消息之一。
此 兼容性 要求仅适用于给定分区。 如果分区引用其他分区,则引用的分区中的数据源不必彼此兼容。 这是因为防火墙可以缓冲数据,从而阻止对原始数据源进行进一步折叠。 数据被加载到内存中,并被视为没有来源。
为什么不同时这样做?
假设您定义了一个包含一个步骤(这同样也对应于一个分区)的查询,该查询访问另外两个查询(即另外两个分区)。 如果想要在同一步骤中直接访问 SQL 数据库,该怎么办? 为什么分区不能引用其他分区并直接访问兼容的数据源?
如前所述,当一个分区引用另一个分区时,防火墙充当流入分区的所有数据的守护程序。 为此,它必须能够控制允许的数据。 如果分区内有数据源被访问,并且有数据从其他分区流入,它将失去作为守护者的能力,因为流入的数据可能会泄漏到内部访问的数据源之一,而自身对此毫不知情。 因此,防火墙可以防止一个原本可访问其他分区的分区直接访问任何数据源。
那么,如果分区尝试引用其他分区并直接访问数据源,会发生什么情况?
Formula.Firewall: Query 'Query1' (step 'Source') references other queries or steps, so it may not directly access a data source. Please rebuild this data combination.
现在,你希望更好地了解本文开头列出的其他错误消息。
深入分区
正如你可能从以前的信息中猜到的那样,如何对查询进行分区最终变得非常重要。 如果你有一些引用其他查询的步骤,以及访问数据源的其他步骤,则现在希望能够识别到在某些地方绘制分区边界会导致防火墙错误,而在其他位置绘制分区边界时,查询可以正常运行。
那么,如何对查询进行分区?
本部分可能是了解为何看到防火墙错误以及了解如何解决防火墙错误(尽可能)的最重要部分。
下面是分区逻辑的高级摘要。
- 初始分区
- 为每个查询中的每个步骤创建分区
- 静态阶段
- 此阶段不依赖于评估结果。 相反,它依赖于查询的结构。
- 参数修整
- 剪裁类似于参数的分区,即以下任一分区:
- 不引用任何其他分区
- 不包含任何函数调用
- 它不是循环的(也就是说,它不指向自身)
- 请注意,“解除”分区会使其有效地包含在引用它的其他分区中。
- 剪裁参数分区允许数据源函数调用(例如)
Web.Contents(myUrl)中使用的参数引用正常工作,而不是引发“分区不能引用数据源和其他步骤”错误。
- 剪裁类似于参数的分区,即以下任一分区:
- 分组 (静态)
- 分区按自下而上依赖项顺序合并。 在生成的合并分区中,以下是独立的:
- 不同查询中的分区
- 不引用其他分区的分区(因此允许访问数据源)
- 引用其他分区的分区(因此禁止访问数据源)
- 分区按自下而上依赖项顺序合并。 在生成的合并分区中,以下是独立的:
- 参数修整
- 此阶段不依赖于评估结果。 相反,它依赖于查询的结构。
- 动态阶段
- 此阶段取决于评估结果,包括有关各种分区访问的数据源的信息。
- 修剪
- 修剪满足以下所有要求的分区:
- 无法访问任何数据源
- 不引用任何访问数据源的分区
- 不是循环的
- 修剪满足以下所有要求的分区:
- 分组 (动态)
- 剪裁不必要的分区后,请尝试创建尽可能大的源分区。 此创建是通过使用上一个静态分组阶段中所述的相同规则合并分区来完成的。
这是什么意思?
让我们通过一个示例来演示之前阐述的复杂逻辑的工作原理。
下面是一个示例方案。 这是一个相当简单的文本文件(联系人)与 SQL 数据库(Employees)合并,其中 SQL Server 是参数(DbServer)。
三个查询
下面是此示例中使用的三个查询的 M 代码。
shared DbServer = "MySqlServer" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true];
shared Contacts = let
Source = Csv.Document(File.Contents(
"C:\contacts.txt"),[Delimiter=" ", Columns=15, Encoding=1252, QuoteStyle=QuoteStyle.None]
),
#"Promoted Headers" = Table.PromoteHeaders(Source, [PromoteAllScalars=true]),
#"Changed Type" = Table.TransformColumnTypes(
#"Promoted Headers",
{
{"ContactID", Int64.Type},
{"NameStyle", type logical},
{"Title", type text},
{"FirstName", type text},
{"MiddleName", type text},
{"LastName", type text},
{"Suffix", type text},
{"EmailAddress", type text},
{"EmailPromotion", Int64.Type},
{"Phone", type text},
{"PasswordHash", type text},
{"PasswordSalt", type text},
{"AdditionalContactInfo", type text},
{"rowguid", type text},
{"ModifiedDate", type datetime}
}
)
in
#"Changed Type";
shared Employees = let
Source = Sql.Databases(DbServer),
AdventureWorks = Source{[Name="AdventureWorks"]}[Data],
HumanResources_Employee = AdventureWorks{[Schema="HumanResources",Item="Employee"]}[Data],
#"Removed Columns" = Table.RemoveColumns(
HumanResources_Employee,
{
"HumanResources.Employee(EmployeeID)",
"HumanResources.Employee(ManagerID)",
"HumanResources.EmployeeAddress",
"HumanResources.EmployeeDepartmentHistory",
"HumanResources.EmployeePayHistory",
"HumanResources.JobCandidate",
"Person.Contact",
"Purchasing.PurchaseOrderHeader",
"Sales.SalesPerson"
}
),
#"Merged Queries" = Table.NestedJoin(
#"Removed Columns",
{"ContactID"},
Contacts,
{"ContactID"},
"Contacts",
JoinKind.LeftOuter
),
#"Expanded Contacts" = Table.ExpandTableColumn(
#"Merged Queries",
"Contacts",
{"EmailAddress"},
{"EmailAddress"}
)
in
#"Expanded Contacts";
下面是一个更高级别的视图,其中显示了依赖项。
让我们分区
让我们稍微放大一下,并在图片中包含步骤,然后开始浏览分区逻辑。 下面是三个查询的示意图,其中以绿色显示初始防火墙分区。 请注意,每个步骤都在其独立的分区中启动。
接下来,我们将剪裁参数分区。 因此,DbServer 将隐式包含在源分区中。
现在,我们执行静态分组。 此分组在单独的查询中保持分区之间的分离(请注意,例如,员工的最后两个步骤不会与联系人的步骤合并),以及在引用其他分区的分区(例如员工的最后两个步骤)与不引用其他分区的分区(例如员工的前三个步骤)之间保持分离。
现在,我们进入动态阶段。 在此阶段中,将评估上述静态分区。 不访问任何数据源的分区将被删除。 然后,将分区分组以创建尽可能大的源分区。 但是,在此示例方案中,所有剩余分区都访问数据源,并且没有可以进行进一步的分组。 因此,示例中的分区在此阶段不会更改。
让我们假装
为了便于说明,我们来看看如果联系人查询不是来自文本文件,而是直接在 M 中硬编码(可能通过“输入数据”对话框),会发生什么。
在这种情况下,联系人查询将无法访问任何数据源。 因此,它会在动态流程阶段的第一部分被修剪。
删除“联系人”分区后,员工流程的最后两个步骤将不再引用任何其他分区,除了包含流程前三个步骤的员工分区外。 因此,这两个分区将被分组。
生成的分区如下所示。
示例:将数据从一个数据源传递到另一个数据源
好了,抽象的解释就到这里。 让我们看看一个常见场景,其中你可能会碰到防火墙错误,并了解解决此问题的步骤。
假设你想要从 Northwind OData 服务查找公司名称,然后使用公司名称执行必应搜索。
首先,创建 公司 查询以检索公司名称。
let
Source = OData.Feed(
"https://services.odata.org/V4/Northwind/Northwind.svc/",
null,
[Implementation="2.0"]
),
Customers_table = Source{[Name="Customers",Signature="table"]}[Data],
CHOPS = Customers_table{[CustomerID="CHOPS"]}[CompanyName]
in
CHOPS
接下来,创建一个 搜索 查询,该查询引用 公司 并将其传递给必应。
let
Source = Text.FromBinary(Web.Contents("https://www.bing.com/search?q=" & Company))
in
Source
此时,你遇到了麻烦。 评估 搜索 会产生防火墙错误。
Formula.Firewall: Query 'Search' (step 'Source') references other queries or steps, so it may not directly access a data source. Please rebuild this data combination.
发生此错误的原因是 搜索 的源步骤引用数据源(bing.com),并引用另一个查询/分区(公司)。 它违反了前面提到的规则(“分区可以访问兼容的数据源或引用其他分区,但不能同时引用这两个分区”)。
怎么办? 一个选项是完全禁用防火墙(通过标记为 “忽略隐私级别”的隐私选项,并可能提高性能)。 但是,如果想要使防火墙保持启用状态,该怎么办?
若要在不禁用防火墙的情况下解决此错误,可以将“公司”和“搜索”合并为单个查询,如下所示:
let
Source = OData.Feed(
"https://services.odata.org/V4/Northwind/Northwind.svc/",
null,
[Implementation="2.0"]
),
Customers_table = Source{[Name="Customers",Signature="table"]}[Data],
CHOPS = Customers_table{[CustomerID="CHOPS"]}[CompanyName],
Search = Text.FromBinary(Web.Contents("https://www.bing.com/search?q=" & CHOPS))
in
Search
现在,所有内容都发生在 单个 分区内。 假设两个数据源的隐私级别兼容,防火墙现在应该很高兴,并且你不再收到错误。
圆满完成
虽然可以在此主题上说更多,但此介绍性文章已经足够长了。 希望它能够更好地了解防火墙,并帮助你了解和修复将来遇到防火墙错误。