メインコンテンツまでスキップ

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

行フィルタと列マスクのポリシーは、クエリ実行時に実行されるロジックを導入するため、パフォーマンスはポリシーの設計方法に依存します。あらゆるワークロードに対して、唯一の正解となるアプローチは存在しない。最適なアプローチは、データ量、クエリパターン、ユーザーが保護されたテーブルとどのようにやり取りするか、そして希望するマスキングまたはフィルタリングの動作によって異なります。以下のセクションでは、最も一般的なパフォーマンスに関する考慮事項について説明します。ポリシーを設計するときにこれらをチェックリストとして使用し、本番運用に展開する前に代表的なクエリでテストしてください

パフォーマンス概要

考慮

説明

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句内、またはcurrent_user()is_account_group_member()のようなアイデンティティ関数を使用して UDF 内で実装します。

一般的に、ポリシーのTO / EXCEPT条項を使用して、ポリシーが適用される主体を定義します。これにより、ポリシー定義が簡素化され、UDFはデータ変換、フィルタリング、またはマスキングに集中できるようになります。EXCEPT条項は、免除対象ユーザーに対してポリシーを完全に削除するため、これらのユーザーに対しては UDF が実行されません。

条件ロジックが複雑すぎてポリシーの主要条項で処理できない場合、UDF内のアイデンティティ関数が代替手段として考えられます。これらの関数は、クエリ分析中に一度だけ解決され、行ごとに解決されるわけではありません。is_account_group_member()のような識別関数を異なるグループ引数で複数回呼び出しても、結果としてUC API呼び出しは1回に抑えられるため、パフォーマンスへの影響は通常最小限です。

以下のUDFは、クエリ解析中に一度だけ解決されるID関数のみに依存するため、効率的です。

SQL
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は権限をセカンダリテーブルにエンコードするため、追加のテーブル検索が必要となり、処理速度が遅くなります。

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

入力値が有効でない場合、例えば分母がゼロの場合のANSI除算のように、一部の式はエラーを発生させます。SQLコンパイラがこの可能性を検出すると、クエリプラン内でフィルタなどの操作を下位に押し下げることができなくなります。そうすると、フィルタリングやマスキングが効き始める前に値に関する情報が漏洩するエラーが発生する可能性があります。/の代わりに try_divideCASTの代わりにtry_casttry_to_number to_numberエラーのない代替手段を使用してください。これらは失敗した場合に例外をスローする代わりにNULL返すため、オプティマイザは式を自由に再配置および折り畳むことができます。

Python UDFを避ける

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

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

ルックアップテーブルは小さく保つ

一般的な方法としては、アクセス権限を小さなルックアップテーブル(例えば、ユーザーを許可された優先度レベルにマッピングするテーブル)と照合して確認する方法があります。ルックアップテーブルがターゲットテーブルよりも大幅に小さい場合、オプティマイザはサブクエリをブロードキャストハッシュ結合に変換します。ルックアップテーブルは各エグゼキューターにコピーされ、ハッシュマップとしてメモリに格納されるため、テーブルスキャン中に高速なフィルタリングが可能になります。 コード例については、 「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を使用してフィルタリングするクエリを考えてみましょう。

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

ポリシーがない場合、 date_format述語はPhotonScanノード内のPartitionFiltersに現れます。これはパーティションプルーニングが有効になっていることを意味します。

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

ポリシー(常にtrueを返すポリシーであっても)では、 SecureViewバリアが述語をブロックします。スキャン対象からPartitionFiltersに留まるのではなく、スキャン対象の上にあるPhotonFilterに移動するため、テーブル全体のスキャンが発生します。

+- 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への削除や固定文字列への置換など)が必要な場合は、列ごとに個別の関数を作成するのではなく、同じマスキング関数を再利用してください。

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

大きなテキストフィールドでは正規表現によるマスキングを避ける

シリアル化されたドキュメント(文字列列として格納された XML または JSON)内の要素を墨消しするために列マスク内でregexp_replace使用すると、コストがかかります。regexp_replace 、各行に対して文字列全体を走査します。オプティマイザは 列を不透明な値として扱い、ドキュメントの未使用部分を削除できません。 クエリに必要なフィールドがごく少数であっても、エンジンはペイロード全体を読み込み、書き換えます。

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

代わりに、機密性の高いフィールドを別のテーブルの型付き列として具体化し、それらのスカラー列に列マスクを適用します。マスク関数は、シリアル化されたドキュメント全体ではなく、各行の小さな単一の値に対して動作します。

SQL
-- 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パフォーマンスをテストします。 合成スケールテストに加えて、保護対象テーブルで想定される実際のワークロードを再現するクエリを実行してください。ポリシー機能には段階的に変更を加え、最終バージョンだけをテストするのではなく、それぞれの変更がもたらす影響を測定してください。

SQL
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 に置き換えてください。ポリシーを適用した場合と適用しない場合の結果を比較することで、ポリシーによるオーバーヘッドを特定します。