ABAC ポリシーのパフォーマンスに関する考慮事項

行フィルターポリシーと列マスク ポリシーでは、クエリ時に実行されるロジックが導入されるため、パフォーマンスはポリシーの設計方法によって異なります。 すべてのワークロードに対して適切なアプローチは 1 つもありません。 最適な方法は、データ ボリューム、クエリ パターン、ユーザーが保護されたテーブルを操作する方法、必要なマスクまたはフィルター処理の動作によって異なります。 次のセクションでは、パフォーマンスに関する最も一般的な考慮事項について説明します。 ポリシーを設計するときにチェックリストとして使用し、運用環境にデプロイする前に 代表的なクエリでテスト します。

パフォーマンスの概要

考慮事項 Description
UDF の複雑さを軽減する 複雑な UDF ロジックにより、クエリのパフォーマンスが低下する可能性があります。単純な関数のパフォーマンスが向上します。
プリンシパルをターゲットにするためのアプローチ プリンシパル ベースのロジックをポリシーの TO/EXCEPT 句に実装するか、ID 関数を使用して UDF 内に実装するかを決定します。
決定的でエラーセーフな式を使用する 非決定論的な関数や式がエラーを引き起こす可能性があると、オプティマイザーの結果をキャッシュし、操作を再順序付けする能力が低下します。
PythonのUDFを避ける 可能な限り、PYTHON UDF の代わりに SQL UDF を使用します。
参照テーブルを小さくする 外部テーブルを参照する UDF は、それらのテーブルがブロードキャストに十分な小さい場合に最適に動作します。
保護されたテーブルにおける述語プッシュダウンについて理解する 保護されたテーブルに対するクエリは、述語に副作用がある場合、パーティションプルーニングやリキッドクラスタリングの恩恵を受けられない可能性があります。
可能な場合は列マスクを再利用する テーブル上の個別のマスクごとにオーバーヘッドが追加されます。列間で同じ関数を再利用すると、それを減らすことができます。
大きなテキスト フィールドで正規表現マスクを回避する シリアル化されたドキュメントに対する正規表現ベースのマスクにより、エンジンは各行のペイロード全体をスキャンして書き換える必要があります。

UDF の複雑さを軽減する

ABAC ポリシーの UDF は、クエリの実行中に、すべての行 (行フィルター) または一致するすべての列値 (列マスク) に対して実行されます。 UDF の複雑さは、クエリのパフォーマンスに直接影響します。

するべきこと

  • UDF はシンプルにしておきましょう。 基本的な CASE ステートメントと単純なブール式を優先します。
  • UDF 内のターゲット テーブル列のみを可能な限り参照します。 これにより述語のプッシュダウンを有効化します。
  • UDF で外部テーブルを参照する必要がある場合は、ブロードキャストに十分な小さな外部参照を保持します。 参照先テーブルが、ポリシーのアクセス パターンに合わせて最適化およびパーティション分割されていることを確認します。 たとえば、ポリシー参照テーブルをユーザー名でパーティション分割します。
  • 複数レベルの入れ子と不要な関数呼び出しを避けます。 可能な限り組み込みの SQL 関数を使用してください。

以下は使いません:

  • UDF 内の他のデータベースに対する外部 API 呼び出しまたは参照。 ネットワーク呼び出しにより、待機時間とタイムアウトが増える可能性があります。
  • 大規模なテーブルに対する複雑なサブクエリまたは結合。 これにより、ブロードキャスト ハッシュ結合が禁止され、入れ子になったループ結合が強制されます。
  • 大きなテキスト フィールドに対する大量の正規表現。 大きなテキスト フィールドの正規表現を参照してください。
  • information_schemaのクエリなど、行ごとのメタデータ参照。

プリンシパルをターゲットにするためのアプローチ

ABAC ポリシーを記述する場合は、プリンシパル ベースのロジックを実装する場所を決定します。ポリシーの TO/EXCEPT 句で、または UDF 内で、 current_user()is_account_group_member()などの ID 関数を使用します。

一般に、ポリシーの TO/EXCEPT 句を使用して、ポリシーが適用されるプリンシパルを定義します。 これにより、ポリシー定義が簡単になり、UDF はデータ変換、フィルター処理、またはマスクに重点を置きます。 EXCEPT句は、除外ユーザーのポリシーを完全に排除します。つまり、それらのユーザーに対して UDF を実行しません。

条件付きロジックがポリシーのプリンシパル句に対して複雑すぎる場合は、UDF 内の ID 関数が代替として考えられます。 これらの関数は、行ごとではなく、クエリ分析中に 1 回解決されます。 グループ引数が異なる is_account_group_member() などの ID 関数を複数回呼び出すと、1 つの UC API 呼び出しが発生するため、通常、パフォーマンスへの影響は最小限です。

次の UDF は、クエリ分析中に 1 回解決される ID 関数にのみ依存するため、効率的です。

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 UDF と Python UDF の両方で、DETERMINISTIC ステートメントの CREATE FUNCTION キーワードがサポートされています。 SQL UDF の場合、オプティマイザーは関数本体から決定性を自動的に派生させますが、明示的に設定することもできます。 Python UDF の場合、オプティマイザーは関数本体を検査できないため、同じ引数を持つ呼び出しの結果キャッシュを有効にするには、Python UDF を決定論的として明示的にマークすることが重要です。

一部の式では、0 分母での ANSI 除算など、入力が有効でない場合にエラーがスローされます。 SQL コンパイラがこの可能性を検出すると、クエリ プランでフィルターなどの操作をプッシュすることはできません。 そうすると、フィルター処理またはマスクが有効になる前に値に関する情報を表示するエラーが発生する可能性があります。 try_divideではなく/try_castではなくCASTtry_to_numberではなくto_numberなどのエラーセーフな代替手段を使用します。 失敗した場合、これらはスローする代わりに NULL を返します。これにより、オプティマイザーは式を自由に再配置し、折りたたむことができます。

Python UDFを避ける

可能な限り、ABAC ポリシーでPythonのUDFを使用しないでください。 ポリシーで使用するには、Python UDFはSQL UDFにラップされている必要があります。 また、オプティマイザーはインライン化または最適化できず、Python関数はターゲット テーブル内のすべての行に対して実行されるため、一般に SQL UDF よりも低速です。

Python UDF が避けられない場合は、決定論的でエラーセーフな式としてマークして結果のキャッシュを有効にする方法を参照してください。

参照テーブルを小さくする

一般的なパターンは、小さな参照テーブル (たとえば、ユーザーを許可された優先度レベルにマップするテーブル) に対してアクセス権を確認することです。 ルックアップ テーブルがターゲット テーブルよりも大幅に小さい場合、オプティマイザーはサブクエリをブロードキャスト ハッシュ結合に変換します。 ルックアップ テーブルは各 Executor にコピーされ、ハッシュマップとしてメモリに格納されます。これにより、テーブル スキャン中に高速なフィルター処理が可能になります。 コード例については、「 ABAC ポリシー UDF の参照テーブル」を参照してください。

  • ルックアップテーブルが大きい場合、オプティマイザはシャッフル結合にフォールバックし、それによって処理が遅くなります。
  • ルックアップ述語が複雑な場合 (単純な等値チェックではない)、ブロードキャストジョインも不適格になる可能性があります。
  • ブロードキャスト ハッシュ結合を使用しても、各行は実行中にハッシュ テーブル参照のコストを引き続き発生します。

保護されたテーブルの述語プッシュダウンについて

述語プッシュダウンとは、エンジンがフィルター条件をストレージ層に移行させることによるパフォーマンスの最適化です。 これにより、エンジンはクエリに一致しないデータのパーティション全体をスキップでき、I/O が大幅に削減され、実行が高速化されます。

行フィルターと列マスクによって保護されるテーブルの場合、この最適化はより複雑になります。 これは、保護されたテーブルに関するパフォーマンスの問題の最も一般的な原因であり、最も対処が困難です。ポリシー作成者は、ユーザーが保護されたテーブルに対して実行するクエリを制御できないためです。

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'

ポリシーがない場合、PhotonScanノード内のPartitionFiltersdate_format述語が表示されます。これは、パーティションプルーニングがアクティブであることを示しています。

+- 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 バリアを完全に排除し、完全な述語プッシュダウンを復元します。

可能な場合は列マスクを再利用する

1 つのテーブルに多数の個別の列マスクを適用すると、列ごとのコストが発生します。 本当に機密性の高いデータを含む列のみをマスクします。

複数の列で同じ変換が必要な場合 (たとえば、 NULL に編集したり、固定文字列に置き換えたりする場合)、列ごとに個別の関数を作成するのではなく、同じマスク関数を再利用します。

Azure Databricksは、同じ有効なマスクと同じ引数を持つ同じ UDF を参照するポリシーを認識するため、関数を再利用すると不要なオーバーヘッドが回避されます。

大きなテキスト フィールドで正規表現マスクを回避する

列マスク内の regexp_replace を使用して、シリアル化されたドキュメント (XML または JSON が STRING 列として格納されている) 内の要素を編集すると、コストがかかります。 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;

代わりに、機密フィールドを別のテーブルの型指定された列に具体化し、それらのスカラー列に列マスクを適用します。 その後、mask 関数は、シリアル化されたドキュメント全体ではなく、行ごとに 1 つの小さな値で動作します。

-- 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;

テストする UDF に your_mask_function を置き換えます。 ポリシーのオーバーヘッドを分離するために、ポリシーを適用した結果と適用されていない結果を比較します。