行筛选器和列掩码策略的性能注意事项

注释

这些注意事项适用于在查询时执行 UDF 的行筛选器和列掩码策略。

行筛选器和列掩码策略引入了在查询时运行的逻辑,因此性能取决于策略的设计方式。 对于每个工作负荷,没有一种正确的方法。 最佳方法取决于数据量、查询模式、用户如何与受保护的表交互以及所需的掩码或筛选行为。 以下部分介绍最常见的性能注意事项。 在设计策略时将它们用作清单,并在部署到生产环境之前 使用具有代表性的查询进行测试

性能概述

注意事项 Description
降低 UDF 复杂性 复杂的 UDF 逻辑可能会抑制查询性能;简单函数的性能更好。
目标主体的方法 决定是在策略的TO/EXCEPT子句中实现基于主体的逻辑,还是在UDF中使用标识函数。
使用确定性、错误安全的表达式 可引发错误的非确定性函数和表达式可减少优化器缓存结果和重新排序操作的能力。
避免使用Python UDFs 尽可能使用 SQL UDF 而不是Python UDF。
保持查找表小 当这些表足够小且足以广播时,引用外部表的 UDF 性能最佳。
了解受保护表的谓词下推 如果谓词具有副作用,则针对受保护表的查询可能无法受益于分区修剪或液体聚类分析。
尽可能重用列掩码 表上的每个不同掩码都会增加开销;跨列重用同一函数可以减少它。
避免在大型文本字段上屏蔽正则表达式 基于正则表达式的文档的掩码强制引擎扫描和重写每一行的整个有效负载。

降低 UDF 复杂性

ABAC 策略中的 UDF 在查询执行过程中对每一行(行筛选器)或每个匹配的列值(列掩码)执行。 UDF 的复杂性直接影响查询性能。

应执行的操作:

  • 保持 UDF 简单。 支持基本 CASE 语句和简单的布尔表达式。
  • 尽可能多地引用 UDF 中的目标表列。 这将启用 谓词下推
  • 如果您的 UDF 必须引用外部表,请确保任何外部引用都足够小,以便可以广播。 确保对引用的表进行了优化和分区,以匹配策略的访问模式。 例如,按用户名对策略查找表进行分区。
  • 避免多级嵌套和不必要的函数调用。 尽可能多地使用内置 SQL 函数。

避免:

  • 外部 API 调用或查找 UDF 中的其他数据库。 网络调用可能会引入额外的延迟和超时。
  • 针对大型表的复杂子查询或联接。 这些机制阻止广播哈希连接并强制使用嵌套循环连接。
  • 在大型文本字段上执行繁重的正则表达式操作。 请查看 大型文本字段中的正则表达式
  • 每行元数据查找,例如查询 information_schema

针对主体的方法

编写 ABAC 策略时,您需要决定在何处实现基于主体的逻辑:是在策略的TO/EXCEPT子句中,还是在 UDF 中使用标识函数(例如 current_user()is_account_group_member())。

通常,使用策略的 TO/EXCEPT 子句定义策略适用的主体。 这样,策略定义更简单,UDF 侧重于数据转换、筛选或掩码。 该 EXCEPT 子句完全消除了免除用户的策略,这意味着这些用户不会执行 UDF。

如果条件逻辑对于策略的主体子句过于复杂,则 UDF 中的标识函数是可能的替代方法。 这些函数在查询分析期间解析一次,而不是每行解析一次。 对标识函数的多次调用(例如 is_account_group_member() 使用不同的组参数)会导致单个 UC API 调用,因此性能影响通常最小。

以下 UDF 非常有效,因为它仅依赖于在查询分析期间解析一次的标识函数:

CREATE OR REPLACE FUNCTION rowfilter()
RETURNS BOOLEAN
RETURN
  CASE
    WHEN is_account_group_member('auditors') OR is_account_group_member('external-auditors') THEN true
    WHEN is_account_group_member('low-privileged') THEN false
    WHEN session_user() = 'admin@organization.com' THEN true
    ELSE false
  END;

相比之下,以下 UDF 速度较慢,因为它对辅助表中的权限进行编码,这需要额外的表查找:

CREATE OR REPLACE FUNCTION rowfilter()
RETURNS BOOLEAN
RETURN
  CASE WHEN EXISTS(SELECT 1 FROM access_lease WHERE user = session_user()) THEN true
  ELSE false END;

使用确定性、错误安全的表达式

使用确定性表达式,这些表达式不能在策略 UDF 和针对受保护表的查询中引发错误。

非确定性函数(返回相同输入的不同结果的函数,如 rand()now())阻止优化器缓存结果或应用常量折叠。 SQL 和 Python UDF 都支持 DETERMINISTIC 语句中的 CREATE FUNCTION 关键字。 对于 SQL UDF,优化器会自动从函数体派生确定性,但你也可以显式设置它。 对于Python UDF,优化器无法检查函数体,因此显式将Python UDF 标记为确定性对于为具有相同参数的调用启用结果缓存非常重要。

如果输入无效,某些表达式将引发错误,例如零分母上的 ANSI 除法。 当 SQL 编译器检测到这种可能性时,它无法推送查询计划中的筛选器等操作。 这样做可能会触发在筛选或屏蔽生效之前显示有关值的信息的错误。 使用错误安全的替代方法,例如try_divide/try_cast而不是CASTtry_to_number而不是to_number。 这些在失败时返回 NULL ,而不是引发,这允许优化器自由排列和折叠表达式。

避免使用 Python UDF

尽可能避免在 ABAC 策略中的 Python UDF。 Python UDF 必须封装在 SQL UDF 中以用于策略。 它们通常也比 SQL UDF 慢,因为优化器无法内联或优化它们,并且Python函数针对目标表中的每一行执行。

如果Python UDF 不可避免,请参阅 不确定的错误安全表达式了解如何将其标记为 DETERMINISTIC 以启用结果缓存。

保持查找表较小

一种常见模式是针对小型查阅表格检查访问权限(例如,将用户映射到允许的优先级的表)。 如果查阅表格明显小于目标表,优化器会将子查询转换为广播哈希联接。 查阅表格将复制到每个执行程序,并作为哈希映射存储在内存中,从而在表扫描期间实现快速筛选。 有关代码示例,请参阅 ABAC 策略 UDF 中的查找表

  • 如果查找表很大,优化器会退回到较慢的 shuffle join。
  • 如果查找谓词很复杂(不是简单的相等性检查),广播联接也可能无法使用。
  • 即使使用广播哈希联接,执行期间每一行仍会产生哈希表查找的成本。

了解受保护表上的谓词下推

谓词下推是性能优化,引擎将筛选器条件推送到存储层。 这允许引擎跳过与查询不匹配的整个数据分区,显著减少 I/O 并加快执行速度。

对于受行筛选器和列掩码保护的表,此优化更为复杂。 这是受保护表性能问题的最常见来源,也是最难解决的源,因为策略作者无法控制用户针对受保护表运行的查询。

SecureView屏障如何影响谓词下推

ABAC 和 表级行筛选器和列掩码 都会使用一种屏障,以防止具有副作用的谓词被推过策略边界。 这可以防止侧通道数据泄漏,但它还可以阻止分区修剪和液体聚类分析优化,从而强制执行全表扫描。 即使策略 UDF 解析为常量 true (这意味着实际上没有筛选任何行),也是如此。 在表上存在的策略会引入SecureView障碍。

受屏障影响的筛选器

通常,优化器只能将无副作用的谓词穿透 SecureView 屏障。

  • 向下推(快速):简单的相等比较(WHERE col = 'value')和基本范围比较(WHERE col > 100)。 这些无副作用,不会有泄露数据的风险。
  • 阻塞(较慢):调用函数(WHERE date_format(col, 'yyyy-MM-dd') = '1995-07-29')或引入隐式类型转换的谓词。 这些内容保存在 SecureView 屏障之上,这意味着引擎必须在应用筛选器之前扫描表。

以下示例显示了差异。 请考虑具有分区键 o_orderdate 的表,以及使用 date_format 作为筛选条件进行查询。

EXPLAIN SELECT * FROM orders
WHERE date_format(o_orderdate, 'yyyy-MM-dd') = '1995-07-29'

如果没有策略, date_format 谓词会显示在 PartitionFilters 节点内 PhotonScan ,这意味着分区修剪处于活动状态:

+- PhotonScan parquet orders[...]
   PartitionFilters: [isnotnull(o_orderdate),
   (date_format(cast(o_orderdate as timestamp), yyyy-MM-dd, ...))]

即使该策略始终返回 trueSecureView 屏障也会阻止谓词。 它移动到 PhotonFilter 扫描上方,而不是停留 PartitionFilters,这会导致完整表扫描:

+- PhotonFilter (date_format(cast(o_orderdate as timestamp),
   yyyy-MM-dd, ...) = 1995-07-29)
    +- PhotonSecureView orders
        +- PhotonScan parquet orders[...]
           PartitionFilters: [isnotnull(o_orderdate)]

一个更简单的谓词,如 WHERE o_orderdate = '1995-07-29',没有副作用,即使屏障 SecureView 在位,仍然可以下推。

+- PhotonSecureView orders
    +- PhotonScan parquet orders[...]
       PartitionFilters: [isnotnull(o_orderdate),
       (o_orderdate = 1995-07-29)]

在可能的情况下,对受保护的表尽量使用简单的相等谓词。 对于豁免用户,请使用 EXCEPT 策略中的子句完全消除 SecureView 限制,从而还原完全谓词下推。

尽可能重用列掩码

将多个不同的列掩码应用于同一个表会增加每列的成本。 仅屏蔽包含真正敏感数据的列。

如果多个列需要相同的转换(例如,对 NULL 固定字符串进行修订或替换),请重复使用相同的掩码函数,而不是为每个列创建单独的函数。

Azure Databricks识别引用具有相同参数的同一 UDF 的策略为具有相同功能掩码的策略,因此重用函数可避免不必要的开销。

避免在大型文本字段上屏蔽正则表达式

在列掩码中使用 regexp_replace 对序列化文档(XML 或作为 STRING 列存储的 JSON)中的元素进行编辑或遮蔽,代价高昂。 regexp_replace 遍历每行的完整字符串。 优化器将 STRING 列视为不透明值,并且无法修剪文档的未使用部分。 即使查询只需要几个字段,引擎也会读取和重写整个有效负载。

-- Expensive: regex masking on serialized XML
CREATE FUNCTION mask_xml_pii(raw_xml STRING)
RETURNS STRING
RETURN CASE
  WHEN is_account_group_member('sensitive_data_viewers') THEN raw_xml
  ELSE regexp_replace(raw_xml, '<SSN>[^<]*</SSN>', '<SSN>***</SSN>')
END;

相反,将敏感字段具体化为单独的表中的类型化列,然后将列掩码应用于这些标量列。 然后,掩码函数对每行的单个小值而不是整个序列化文档进行操作。

-- Source table stores raw XML as STRING
-- Example XML: <person><SSN>123-45-6789</SSN><name>Alice</name><dob>1990-01-01</dob></person>

-- Recommended: extract fields into a table, then mask scalar values
CREATE TABLE person_data AS
SELECT
  id,
  xpath_string(raw_xml, 'person/SSN') AS ssn,
  xpath_string(raw_xml, 'person/name') AS name,
  xpath_string(raw_xml, 'person/dob') AS date_of_birth,
  raw_xml
FROM raw_records;

-- Simple scalar mask, applied to each extracted column
CREATE FUNCTION redact(val STRING) RETURNS STRING
RETURN CASE
  WHEN is_account_group_member('sensitive_data_viewers') THEN val
  ELSE '***'
END;

如果可以将数据存储为结构列而不是 XML,请使用 VARIANT 灵活掩码模式来编辑结构中的单个字段。 请参阅 使用 VARIANT 掩码的结构列

测试 UDF 性能

大规模测试

在部署到生产环境之前,在至少 100 万行上测试 UDF 性能。 除了模拟规模测试之外,还运行代表受保护表实际预期工作负载的查询。 对策略函数进行增量更改并衡量每个更改的效果,而不是仅测试最终版本。

WITH test_data AS (
  SELECT
    id,
    your_mask_function(id) AS masked_id,
    current_timestamp() AS ts
  FROM (
    SELECT CONCAT('ID', LPAD(CAST(id AS STRING), 6, '0')) AS id
    FROM range(1000000)
  )
)
SELECT
  COUNT(*) AS rows_processed,
  MAX(ts) - MIN(ts) AS total_duration
FROM test_data;

请将 your_mask_function 替换为您要测试的 UDF。 比较应用策略与不应用策略的结果,以识别策略的开销。