列過濾器與欄位遮罩政策的效能考量

備註

這些考量適用於列過濾器與欄遮罩政策,這些策略會在查詢時執行 UDF。 GRANT 政策及 DENY 政策(Beta)不受這些規範約束。 請參閱ABACGRANT政策及ABACDENY政策(Beta版)。

列過濾器和欄遮罩政策會在查詢時引入邏輯,因此效能取決於你如何設計政策。 沒有一種適用於所有工作量的正確方法。 最佳方法取決於你的資料量、查詢模式、使用者與受保護資料表的互動方式,以及你想要的遮蔽或過濾行為。 以下章節涵蓋最常見的效能考量。 設計政策時請將它們當作檢查清單,並在部署到生產環境前 用代表性的查詢來測試 。

效能概觀

考量事項 Description
降低 UDF 複雜度 複雜的 UDF 邏輯可能會抑制查詢效能;簡單函式的表現會更好。
針對負責人的方法 決定是在策略的TO/EXCEPT子句中實作基於主體的邏輯,還是在 UDF 內部使用識別函式。
使用確定性且安全錯誤的表達式 非確定性函數和表達式會拋出錯誤,會降低優化器快取結果與重排序操作的能力。
避免使用Python UDF 盡可能使用 SQL UDF 代替 Python UDF。
保持查詢表小一點 參考外部資料表的 UDF 在資料表小到可以廣播時表現最佳。
了解受保護資料表上的謂詞下推 若謂詞有副作用,對受保護的資料表的查詢可能無法受益於分區修剪或液態聚類。
盡可能重用欄位遮罩 桌上每個不同的遮罩都會增加額外負擔;在欄位間重複使用相同函數可以減少這個問題。
避免在大型文字欄位上使用正則表達式遮蔽 基於正則表達式的遮罩需使引擎對序列化文件的每一行進行整個有效載荷的掃描和重寫。

降低 UDF 複雜度

ABAC 政策中的 UDF 會在查詢執行時對每一行(行篩選器)或每個相符的欄位值(欄位遮罩)執行操作。 UDF 的複雜度直接影響查詢效能。

Do:

  • 保持使用者自定義函數 (UDF) 簡單。 偏好基本 CASE 陳述和簡單的布林表達式。
  • 在 UDF 中盡量只參考目標資料表欄位。 這使得 預測下推成為可能。
  • 如果你的 UDF 必須參考外部資料表,請確保任何外部參考資料小到足以傳播。 確保參考資料表的優化與分割符合政策的存取模式。 例如,依使用者名稱劃分政策查找表。
  • 避免多層巢狀結構和不必要的函式呼叫。 盡量多使用內建的 SQL 函式。

避免:

  • 外部 API 呼叫或查詢 UDF 中其他資料庫。 網路通話可能會帶來額外的延遲和逾時。
  • 複雜的子查詢或與大型資料表的連接。 這些方法阻止廣播雜湊連接並強制巢狀迴圈連接。
  • 在大型文字欄位上使用重度正則表達式。 請參見 較大的文字欄位上的正則表達式。
  • 每列元資料查詢,例如查詢 information_schema。

針對負責人的策略

當你撰寫 ABAC 政策時,你需要決定要在哪裡實作以主體為基礎的邏輯:是在政策的 TO/EXCEPT 子句中、在政策的 WHEN 子句中使用 identity attributes,還是在 UDF 內使用例如 is_account_group_member() 之類的身分函數。

一般來說,利用政策條款 TO/EXCEPT 來定義政策適用於哪些主要客戶。 如果你需要基於身份屬性的額外彈性,也可以在政策條款 WHEN 中使用身份屬性。 這使得政策定義更為簡單,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 UDFs,優化器會自動從函式主體推導出決定性,但你也可以明確設定。 對於 Python UDF,優化器無法檢查函式主體,因此明確標記 Python UDF 為確定性非常重要,以啟用參數相同呼叫的結果快取。

有些表達式如果輸入不正確會產生錯誤,例如零分母上的 ANSI 除法。 當 SQL 編譯器偵測到這種可能性時,就無法在查詢計畫中推送像是篩選器這類操作。 這樣做可能會觸發錯誤,在篩選或遮蔽生效前就揭露數值資訊。 使用避免錯誤的替代方案,例如使用try_divide代替/,try_cast代替CAST,以及try_to_number代替to_number。 這些表達式在失敗時回傳 NULL 而非拋棄,使優化器能自由重組與摺疊表達式。

避免 Python UDF

盡量避免在 ABAC 政策中使用 Python UDF。 Python UDF 必須被包裹在一個 SQL UDF中才能用於政策。 它們通常比 SQL UDF 慢,因為優化器無法內嵌或優化它們,且 Python 函式會對目標資料表中的每一列執行。

若無法避免 Python UDF,請參閱 確定性和安全的表達式,以了解如何將其標記為 DETERMINISTIC,以啟用結果快取。

保持查詢表小一點

一個常見的模式是將存取權限與一個小型查詢表(例如將使用者對應到允許優先權等級的表)來檢查。 若查找表明顯小於目標表,優化器會將子查詢轉換為廣播哈希連接。 查找表會被複製到每個執行器,並以雜湊圖形式儲存在記憶體中,這讓在資料表掃描時能快速過濾。 關於程式碼範例,請參見 ABAC 政策 UDF 中的查詢表。

  • 如果查找表很大,優化器會退回到洗牌連接,速度較慢。
  • 若查詢謂詞複雜(非簡單的等式檢查),則廣播連線也可能變得不適用。
  • 即使使用廣播哈希聯接,每一列在執行時仍需要產生哈希表查詢的成本。

了解受保護資料表上的謂詞下推

謂詞下推是一種性能優化方法,資料引擎會將篩選條件下推到儲存層。 這讓引擎能跳過與查詢不符的資料分割,大幅減少輸入輸出並加快執行速度。

對於受列篩選與欄遮罩保護的資料表,此優化較為複雜。 這是受保護資料表最常出現效能問題的來源,也是最難解決的,因為政策制定者無法控制使用者對受保護資料表執行哪些查詢。

障礙如何 SecureView 影響謂詞下推

ABAC 與資料表層級的列過濾器及欄位遮罩皆使用SecureView障礙,防止帶有副作用的謂詞被推越政策邊界。 這可防止側通道資料外洩,但也可能阻擋分區修剪與流體化叢集優化,這些優化可能迫使全資料表掃描。 即使政策 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, ...))]

對於一個策略(即使是總是回傳 true的那種),SecureView 屏障會阻擋該謂詞。 它會移到 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 且使用相同參數的政策視為相同的有效遮罩,因此重複使用函數可避免不必要的負擔。

避免在大型文字欄位上使用正則表達式遮蔽

在序列化文件(以 STRING 欄位儲存的 XML 或 JSON 格式)中,使用 regexp_replace 欄位遮罩來遮蔽元素是昂貴的。 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 彈性遮罩模式來遮蔽結構體內的個別欄位。 請參見 遮罩 STRUCT、ARRAY 和 MAP 欄位。

測試 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。 比較有無套用政策的結果,以隔離政策的額外負擔。