Aller au contenu principal

Considérations relatives aux performances pour les politiques de filtrage de lignes et de masquage de colonnes

remarque

Ces considérations s’appliquent aux politiques de filtrage de lignes et de masquage de colonnes, qui exécutent des UDF au moment de la query. Les politiques GRANT (Bêta) ne leur sont pas applicables. Voir les politiques ABAC GRANT pour les modèles (bêta).

Les politiques de filtrage de lignes et de masquage de colonnes introduisent une logique qui s'exécute au moment de la query, donc les performances dépendent de la façon dont vous concevez vos politiques. Il n'y a pas une seule bonne approche pour chaque charge de travail. La meilleure approche dépend de votre volume de données, des modèles de requêtes, de la façon dont vos utilisateurs interagissent avec les tables protégées, et de votre comportement de masquage ou de filtrage souhaité. Les sections suivantes couvrent les considérations les plus courantes en matière de performances. Utilisez-les comme une liste de contrôle lors de la conception de vos politiques, et testez avec des requêtes représentatives avant de déployer en production.

Présentation des performances

Considération

Description

Réduire la complexité des UDF

La logique UDF complexe peut nuire aux performances des requêtes ; les fonctions simples sont plus performantes.

Approche pour cibler les principaux

Décidez d'implémenter la logique basée sur les principes dans les clauses TO/EXCEPT de la politique ou au sein de l'UDF à l'aide de fonctions d'identité.

Utilisez des expressions déterministes et sans erreur

Les fonctions et expressions non déterministes qui peuvent générer des erreurs réduisent la capacité de l'optimiseur à mettre en cache les résultats et à réorganiser les opérations.

Évitez les UDF Python

Utilisez les UDF SQL plutôt que les UDF Python chaque fois que possible.

Maintenez les tables de correspondance de petite taille

Les fonctions UDF qui référencent des tables externes offrent les meilleures performances lorsque ces tables sont suffisamment petites pour être diffusées.

Comprendre le report de prédicat sur les tables protégées

Les requêtes sur les tables protégées peuvent ne pas bénéficier de l'élagage des partitions ou du liquid clustering si les prédicats ont des effets secondaires.

Réutilisez les masques de colonne lorsque cela est possible.

Chaque masque distinct sur une table ajoute une surcharge ; la réutilisation de la même fonction sur plusieurs colonnes peut la réduire.

Évitez le masquage regex sur les grands champs de texte

Le masquage basé sur des expressions régulières des documents sérialisés force le moteur à analyser et à réécrire l'intégralité de la charge utile pour chaque ligne.

Considération

Description

Réduire la complexité des UDF

La logique UDF complexe peut nuire aux performances des requêtes ; les fonctions simples sont plus performantes.

Approche pour cibler les principaux

Décidez d'implémenter la logique basée sur les principes dans les clauses TO/EXCEPT de la politique ou au sein de l'UDF à l'aide de fonctions d'identité.

Utilisez des expressions déterministes et sans erreur

Les fonctions et expressions non déterministes qui peuvent générer des erreurs réduisent la capacité de l'optimiseur à mettre en cache les résultats et à réorganiser les opérations.

Évitez les UDF Python

Utilisez les UDF SQL plutôt que les UDF Python chaque fois que possible.

Maintenez les tables de correspondance de petite taille

Les fonctions UDF qui référencent des tables externes offrent les meilleures performances lorsque ces tables sont suffisamment petites pour être diffusées.

Comprendre le report de prédicat sur les tables protégées

Les requêtes sur les tables protégées peuvent ne pas bénéficier de l'élagage des partitions ou du liquid clustering si les prédicats ont des effets secondaires.

Réutilisez les masques de colonne lorsque cela est possible.

Chaque masque distinct sur une table ajoute une surcharge ; la réutilisation de la même fonction sur plusieurs colonnes peut la réduire.

Évitez le masquage regex sur les grands champs de texte

Le masquage basé sur des expressions régulières des documents sérialisés force le moteur à analyser et à réécrire l'intégralité de la charge utile pour chaque ligne.

Réduire la complexité des UDF

La UDF dans une politique ABAC s’exécute pour chaque ligne (filtres de lignes) ou pour chaque valeur de colonne correspondante (masques de colonnes) lors de l’exécution de la query. La complexité de la UDF a une incidence directe sur les performances de la query.

Faire :

  • Gardez les UDFs simples. Privilégiez les instructions CASE de base et les expressions booléennes simples.
  • Ne référencez que les colonnes de la table cible dans les UDF autant que possible. Cela permet le report de prédicat.
  • Si votre UDF doit référencer des tables externes, gardez toute référence externe suffisamment petite pour être diffusée. Assurez-vous que les tables référencées sont optimisées et partitionnées pour correspondre au modèle d'accès de la politique. Par exemple, partitionnez une table de recherche de politique par nom d'utilisateur.
  • Évitez l'imbrication à plusieurs niveaux et les appels de fonctions inutiles. Utilisez autant que possible les fonctions SQL intégrées.

Éviter :

  • Appels d'API externes ou recherches dans d'autres bases de données dans les UDF. Les appels réseau peuvent introduire une latence et des délais d'expiration supplémentaires.
  • Sous-requêtes ou jointures complexes sur de grandes tables. Ces éléments empêchent les jointures de hachage de diffusion et forcent les jointures de boucles imbriquées.
  • Expression rationnelle lourde sur les champs de texte volumineux. Voir Regex sur les champs de texte volumineux.
  • Recherches de métadonnées par ligne, par exemple l'interrogation de information_schema.

Approche pour cibler les Principals

Lorsque vous rédigez une politique ABAC, vous décidez où implémenter la logique basée sur les principes : dans les clauses TO/EXCEPT de la politique, ou à l'intérieur de l'UDF en utilisant des fonctions d'identité comme current_user() et is_account_group_member().

En général, utilisez les clauses TO/EXCEPT de la politique pour définir les principaux auxquels une politique s'applique. Cela simplifie la définition de la stratégie et permet de concentrer la UDF sur la transformation des données, le filtrage ou le masquage. La clause EXCEPT élimine entièrement la politique pour les utilisateurs exemptés, ce qui signifie aucune exécution d'UDF pour ces utilisateurs.

Lorsque la logique conditionnelle est trop complexe pour les clauses principales de la politique, les fonctions d'identité à l'intérieur de l'UDF sont une alternative possible. Ces fonctions sont résolues une seule fois pendant l'analyse des query, et non pas par ligne. Plusieurs appels aux fonctions d'identité, tels que is_account_group_member(), avec différents arguments de groupe, entraînent un seul appel d'API UC, de sorte que l'impact sur les performances est généralement minimal.

L'UDF suivant est efficace, car il repose uniquement sur des fonctions d'identité, qui sont résolues une fois pendant l'analyse de la query :

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;

En revanche, l’UDF suivante est plus lente car elle encode les privilèges dans une table secondaire, ce qui nécessite une recherche de table supplémentaire :

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;

Utilisez des expressions déterministes et sans erreur

Utilisez des expressions déterministes qui ne peuvent pas générer d'erreurs dans les UDF de politique et dans les queries sur les tables protégées.

Les fonctions non déterministes (fonctions qui renvoient des résultats différents pour la même entrée, comme rand() ou now()) empêchent l'optimiseur de mettre en cache les résultats ou d'appliquer la réduction des constantes. Les UDF SQL et Python prennent en charge le mot-clé DETERMINISTIC dans l'instruction CREATE FUNCTION. Pour les UDF SQL, l'optimiseur dérive automatiquement le déterminisme du corps de la fonction, mais vous pouvez également le définir explicitement. Pour les UDF Python, l'optimiseur ne peut pas inspecter le corps de la fonction, il est donc important de marquer explicitement une UDF Python comme déterministe pour activer la mise en cache des résultats pour les appels avec des arguments identiques.

Certaines expressions génèrent des erreurs si les entrées ne sont pas valides, comme la division ANSI par un dénominateur zéro. Lorsque le compilateur SQL détecte cette possibilité, il ne peut pas intégrer les opérations comme les filtres dans le plan de query. Cela pourrait **trigger** des erreurs qui révèlent des informations sur les valeurs avant que le filtrage ou le masquage ne prenne effet. Utilisez des alternatives résistant aux erreurs, telles que try_divide plutôt que /, try_cast plutôt que CAST, et try_to_number plutôt que to_number. Ceux-ci retournent NULL en cas d'échec au lieu de lever une exception, ce qui permet à l'optimiseur de réorganiser et de condenser les expressions librement.

Évitez les UDF Python

Évitez les UDF Python dans les politiques ABAC autant que possible. Les UDF Python doivent être encapsulées dans une UDF SQL pour être utilisées dans des politiques. Elles sont également généralement plus lentes que les UDF SQL, car l'optimiseur ne peut pas les intégrer en ligne ni les optimiser, et la fonction Python s'exécute pour chaque ligne de la table cible.

Si une UDF Python est inévitable, consultez Expressions déterministes et tolérantes aux erreurs pour savoir comment la marquer comme DETERMINISTIC afin d'activer la mise en cache des résultats.

Conserver les tables de recherche petites

Un modèle courant est de vérifier les droits d’accès par rapport à une petite table de recherche (par exemple, une table qui mappe les utilisateurs aux niveaux de priorité autorisés). Si la table de recherche est significativement plus petite que la table cible, l'optimiseur convertit la sous-requête en une jointure de hachage de diffusion. La table de recherche est copiée vers chaque exécuteur et stockée en mémoire sous forme de table de hachage, ce qui permet un filtrage rapide lors de l'analyse de la table. Pour un exemple de code, voir Tables de recherche dans les UDF de politique ABAC.

  • Si la table de recherche est grande, l'optimiseur se rabat sur une jointure de brassage, qui est plus lente.
  • Si le prédicat de recherche est complexe (pas une simple vérification d’égalité), la jointure de diffusion peut également devenir inéligible.
  • Même avec une jointure de hachage broadcast, chaque ligne entraîne toujours le coût d'une recherche dans une table de hachage pendant l'exécution.

Comprendre le report de prédicat sur les tables protégées

La poussée des prédicats est une optimisation des performances où le moteur pousse vos conditions de filtre vers la couche de stockage. Cela permet au moteur d’ignorer des partitions de données entières qui ne correspondent pas à votre query, ce qui réduit considérablement les E/S et accélère l’exécution.

Pour les tables protégées par des filtres de lignes et des masques de colonne, cette optimisation est plus complexe. C'est la source la plus courante de problèmes de performances avec les tables protégées, et la plus difficile à résoudre, car les auteurs de politiques ne peuvent pas contrôler les requêtes que les utilisateurs exécutent sur les tables protégées.

Comment la barrière SecureView affecte le report de prédicat

L'ABAC et les filtres de lignes et masques de colonnes au niveau de la table utilisent une barrière SecureView pour empêcher que les prédicats avec effets secondaires ne soient poussés au-delà de la limite de la politique. Cela protège contre les fuites de données par canal latéral, mais cela peut également bloquer l'élagage des partitions et les optimisations de clustering liquide, ce qui peut forcer des analyses de table complètes. Cela s'applique même lorsque l'UDF de la politique se résout en une constante true (ce qui signifie qu'aucune ligne n'est réellement filtrée). La présence d'une politique sur une table introduit la barrière SecureView.

Filtres affectés par la barrière

Généralement, l'optimiseur ne peut pousser que les prédicats sans effet secondaire à travers la barrière SecureView.

  • Poussé vers le bas (rapide) : Comparaisons d'égalité simples (WHERE col = 'value') et comparaisons de plage de base (WHERE col > 100). Ils sont sans effets secondaires et ne risquent pas de fuite de données.
  • Bloqué (plus lent) : Prédicats qui appellent des fonctions (WHERE date_format(col, 'yyyy-MM-dd') = '1995-07-29') ou introduisent des conversions de type implicites. Ceux-ci sont conservés au-dessus de la barrière SecureView, ce qui signifie que le moteur doit scanner la table avant d’appliquer le filtre.

L'exemple suivant montre la différence. Considérez une table avec une clé de partition sur o_orderdate et une query qui filtre à l'aide de date_format:

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

Sans politique, le prédicat date_format apparaît dans PartitionFilters dans le nœud PhotonScan, ce qui signifie que l'élagage des partitions est actif :

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

Avec une politique (même celle qui retourne toujours true), la barrière SecureView bloque le prédicat. Il passe à un PhotonFilter au-dessus de l'analyse au lieu de rester dans PartitionFilters, ce qui entraîne une analyse complète de la table :

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

Un prédicat plus simple comme WHERE o_orderdate = '1995-07-29' n’a pas d’effets secondaires et peut toujours être poussé vers le bas même avec la barrière SecureView en place :

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

Utilisez de simples prédicats d’égalité sur les tables protégées lorsque cela est possible. Pour les utilisateurs exemptés, utilisez la clause EXCEPT dans la politique pour éliminer entièrement la barrière SecureView, ce qui rétablit la poussée de prédicat complète.

Réutilisez les masques de colonne lorsque cela est possible.

L'application de nombreux masques de colonne distincts à une seule table augmente le coût par colonne. Masquez uniquement les colonnes qui contiennent des données réellement sensibles.

Lorsque plusieurs colonnes nécessitent la même transformation (par exemple, masquage à NULL ou remplacement par une chaîne fixe), réutilisez la même fonction de masquage plutôt que de créer une fonction distincte par colonne.

Databricks reconnaît les politiques qui référencent la même UDF avec les mêmes arguments comme étant le même masque effectif, de sorte que la réutilisation des fonctions évite une surcharge inutile.

Évitez le masquage regex sur les champs de texte volumineux

L'utilisation de regexp_replace à l'intérieur d'un masque de colonne pour masquer des éléments au sein d'un document sérialisé (XML ou JSON stocké en tant que colonne STRING) est coûteuse. regexp_replace parcourt l'intégralité de la chaîne pour chaque ligne. L'optimiseur traite la colonne STRING comme une valeur opaque et ne peut pas élaguer les parties inutilisées du document. Le moteur lit et réécrit l'intégralité de la charge utile même lorsque la query n'a besoin que de quelques champs.

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;

Au lieu de cela, matérialisez les champs sensibles dans des colonnes typées dans une table distincte, puis appliquez des masques de colonne à ces colonnes scalaires. La fonction de masque opère alors sur une seule petite valeur par ligne plutôt que sur l'ensemble du document sérialisé.

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;

Si vous pouvez stocker les données sous forme de colonne struct au lieu de XML, utilisez le modèle de masquage flexible VARIANT pour masquer les champs individuels au sein de la struct. Consultez Masquer les colonnes de structure avec VARIANT.

Tester les performances des UDF

Test à grande échelle

Testez les performances UDF sur au moins 1 million de lignes avant de déployer en production. En plus des tests de montée en charge synthétiques, exécutez des queries qui représentent la charge de travail réelle que vous attendez sur la table protégée. Apportez des modifications incrémentielles à vos fonctions de politique et mesurez l'effet de chaque changement plutôt que de tester uniquement la version finale.

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;

Remplacez your_mask_function par l’UDF que vous testez. Comparez les résultats avec et sans la politique appliquée pour isoler les frais généraux de la politique.