Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Les stratégies de filtre de lignes et de masque de colonne introduisent une logique qui s’exécute au moment de la requête, de sorte que les performances dépendent de la façon dont vous concevez vos stratégies. Il n’existe aucune approche appropriée pour chaque charge de travail. La meilleure approche dépend de votre volume de données, des modèles de requête, de la façon dont vos utilisateurs interagissent avec des tables protégées et de votre comportement de masquage ou de filtrage souhaité. Les sections suivantes couvrent les considérations relatives aux performances les plus courantes. Utilisez-les comme liste de contrôle lors de la conception de vos stratégies et testez avec des requêtes représentatives avant le déploiement en production.
Vue d’ensemble sur les performances
| Point à considérer | Description |
|---|---|
| Réduire la complexité des UDF | La logique UDF complexe peut empêcher les performances des requêtes ; les fonctions simples fonctionnent mieux. |
| Approche pour viser les responsables principaux | Déterminez s'il faut implémenter une logique basée sur le principal dans les clauses de la stratégie TO/EXCEPT ou à l'intérieur de la fonction UDF à l'aide de fonctions d'identité. |
| Utiliser des expressions déterministes et sans risque d’erreur | Les fonctions et expressions non déterministes qui peuvent lever des erreurs réduisent la capacité de l’optimiseur à mettre en cache les résultats et à réorganiser les opérations. |
| Évitez les UDFs Python | Utilisez des fonctions définies par l’utilisateur SQL au lieu de fonctions définies par l’utilisateur Python dans la mesure du possible. |
| Conserver les tables de recherche petites | Les fonctions définies par l'utilisateur qui font référence aux tables externes fonctionnent de manière optimale lorsque ces tables sont suffisamment petites pour être diffusées. |
| Comprendre le pushdown de prédicat sur des tables protégées | Les requêtes sur des tables protégées peuvent ne pas bénéficier de l'élagage de partitions ou du clustering dynamique si les prédicats ont des effets secondaires. |
| Réutiliser les masques de colonne autant que possible | Chaque masque distinct d’une table ajoute une surcharge ; la réutilisation de la même fonction sur plusieurs colonnes peut la réduire. |
| Éviter le masquage de regex sur les champs de texte volumineux | Le masquage basé sur des expressions régulières appliqué à des documents sérialisés contraint le moteur à analyser et à réécrire l'intégralité des données pour chaque ligne. |
Réduire la complexité des UDF
La fonction UDF dans une stratégie ABAC s’exécute pour chaque ligne (filtres de lignes) ou chaque valeur de colonne correspondante (masques de colonne) pendant l’exécution de la requête. La complexité de la fonction UDF affecte directement les performances des requêtes.
À faire :
- Gardez les UDF (fonctions définies par l’utilisateur) simples. Privilégiez les instructions de base
CASEet les expressions booléennes simples. - Référencez les colonnes de table cible dans les fonctions définies par l'utilisateur (UDF) dans la mesure du possible. Cela active le pushdown de prédicat.
- Si votre Fonction Définie par l'Utilisateur (UDF) doit référencer des tables externes, assurez-vous que toute référence externe soit suffisamment petite pour être transmise. Vérifiez que les tables référencées sont optimisées et partitionnées pour correspondre au modèle d’accès de la stratégie. Par exemple, partitionnez une table de recherche de stratégie par nom d’utilisateur.
- Évitez l’imbrication multiniveau et les appels de fonction inutiles. Utilisez les fonctions SQL intégrées autant que possible.
Éviter :
- Appels ou consultations d’API externes vers d’autres bases de données dans des fonctions définies par l’utilisateur. Les appels réseau peuvent introduire des délais d’attente et de latence supplémentaires.
- Sous-requêtes complexes ou jointures sur des tables volumineuses. Celles-ci empêchent les jointures de hachage de diffusion et forcent les jointures de boucle imbriquées.
- Regex lourd sur les champs de texte volumineux. Consultez Regex sur les champs de texte volumineux.
- Recherches de métadonnées par ligne, par exemple l’interrogation
information_schema.
Approche pour cibler les acteurs principaux
Lorsque vous écrivez une stratégie ABAC, vous décidez où implémenter une logique basée sur le TO : dans les clauses de la stratégie, ou à l’intérieur de la fonction UDF en utilisant des fonctions d'identité telles que current_user() et is_account_group_member().
En général, utilisez les clauses de TO/EXCEPT la stratégie pour définir les principaux auxquels une stratégie s’applique. Cela simplifie la définition de stratégie et l’UDF se concentre sur la transformation, le filtrage ou le masquage des données. La EXCEPT clause élimine entièrement la stratégie pour les utilisateurs exemptés, ce qui signifie qu’aucune exécution UDF n’est exécutée 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 fois pendant l’analyse des requêtes, et non par ligne. Plusieurs appels à des fonctions d’identité comme 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.
La fonction UDF suivante est efficace, car elle s’appuie uniquement sur les fonctions d’identité, qui sont résolues une seule fois lors de l’analyse des requêtes :
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, la fonction 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 :
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;
Utiliser des expressions déterministes et sans risque d’erreur
Utilisez des expressions déterministes qui ne peuvent pas lever erreurs dans les fonctions définies par l'utilisateur pour les politiques et dans les requêtes sur des tables protégées.
Les fonctions non déterministes (fonctions qui retournent des résultats différents pour la même entrée, par exemple rand()now()) empêchent l’optimiseur de mettre en cache les résultats ou d’appliquer un pliage constant. SQL et les fonctions définies par l'utilisateur en Python prennent en charge le mot clé DETERMINISTIC dans l'instruction CREATE FUNCTION. Pour les fonctions définies par l’utilisateur SQL, l’optimiseur dérive automatiquement le déterminisme du corps de la fonction, mais vous pouvez également le définir explicitement. Pour les fonctions Python définies par l'utilisateur, l'optimiseur ne peut pas inspecter le corps de la fonction. Il est donc important de marquer explicitement une fonction UDF Python comme déterministe pour activer la mise en cache des résultats pour les appels avec des arguments identiques.
Certaines expressions lèvent des erreurs si les entrées ne sont pas valides, telles que les divisions ANSI par un dénominateur nul. Lorsque le compilateur SQL détecte cette possibilité, il ne peut pas propager des opérations, telles que les filtres, dans le plan de requête. Cela peut déclencher des erreurs qui révèlent des informations sur les valeurs avant que le filtrage ou le masquage prenne effet. Utilisez des alternatives sans risque d’erreur comme try_divide au lieu de /, try_cast au lieu de CAST, et try_to_number au lieu de to_number. Ces fonctions renvoient NULL en cas d'échec au lieu de lever une exception, ce qui permet à l’optimiseur de réorganiser et d’optimiser librement les expressions.
Évitez les UDFs Python
Évitez les fonctions définies par l'utilisateur (UDF) en Python dans les stratégies ABAC dans la mesure du possible. Les fonctions Python définies par l’utilisateur doivent être enveloppées dans une fonction SQL UDF pour être utilisées dans les stratégies. Ils sont également généralement plus lents que les UDF SQL, car l'optimiseur ne peut pas les intégrer ou les optimiser, et comme la fonction Python s'exécute pour chaque ligne de la table cible.
Si une fonction UDF Python est inévitable, consultez Deterministic, error-safe expressions pour savoir comment la marquer comme DETERMINISTIC pour activer la mise en cache des résultats.
Conserver les tables de recherche petites
Un modèle courant consiste à vérifier les droits d’accès sur une petite table de choix (par exemple, une table qui mappe les utilisateurs aux niveaux de priorité autorisés). Si la table de recherche est beaucoup plus petite que la table cible, l’optimiseur convertit la sous-requête en jointure de hachage de diffusion. La table de recherche est copiée dans chaque exécuteur et stockée en mémoire sous forme de hachage, ce qui permet un filtrage rapide pendant l’analyse de la table. Pour obtenir un exemple de code, consultez les tables de recherche dans les UDF de la stratégie ABAC.
- Si la table de recherche est volumineuse, l’optimiseur revient à une jointure aléatoire, ce qui est plus lent.
- Si le prédicat de recherche est complexe (pas un simple contrôle d’égalité), la jointure par diffusion peut également ne pas être éligible.
- Même avec la jointure par hachage par diffusion, chaque ligne entraîne toujours le coût d'une recherche dans une table de hachage durant l'exécution.
Comprendre l'optimisation par prédicat sur les tables protégées
Le pushdown de prédicat est une optimisation des performances où le moteur envoie vos conditions de filtre à la couche de stockage. Cela permet au moteur d’ignorer des partitions entières de données qui ne correspondent pas à votre requête, réduisant considérablement les E/S et accélérant l’exécution.
Pour les tables protégées par les filtres de lignes et les masques de colonne, cette optimisation est plus complexe. Il s’agit de la source la plus courante des problèmes de performances liés aux tables protégées et la plus difficile à résoudre, car les auteurs de stratégie ne peuvent pas contrôler les requêtes que les utilisateurs exécutent sur des tables protégées.
Comment la barrière SecureView affecte l'optimisation des prédicats
Les filtres de lignes et les masques de colonnes au niveau de la table, ainsi que l'ABAC, utilisent une barrière pour empêcher les prédicats ayant des effets secondaires d'être poussés au-delà de la limite de stratégie. Cela protège contre les fuites de données de canal latéral, mais cela peut également bloquer les optimisations d'élagage de partition et de regroupement liquide, ce qui peut forcer les analyses complètes des tables. Cela s’applique même lorsque la fonction UDF de stratégie se résout en constante true (ce qui signifie qu’aucune ligne n’est réellement filtrée). La présence d’une stratégie sur une table introduit la SecureView barrière.
Filtres affectés par la barrière
En règle générale, l’optimiseur peut envoyer uniquement des prédicats sans effet secondaire à travers la SecureView barrière.
-
Réduction (rapide): comparaisons d’égalité simples (
WHERE col = 'value') et comparaisons de plages simples (WHERE col > 100). Ils sont exempts d’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 casts de types implicites. Celles-ci sont conservées au-dessus de laSecureViewbarrière, ce qui signifie que le moteur doit parcourir 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 requête qui filtre en utilisant date_format :
EXPLAIN SELECT * FROM orders
WHERE date_format(o_orderdate, 'yyyy-MM-dd') = '1995-07-29'
Sans stratégie, le prédicat apparaît dans le nœud PartitionFilters, ce qui signifie que l'élagage de partition 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 du balayage au lieu de rester dans PartitionFilters, ce qui entraîne un scan complet 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 aucun effet secondaire et peut toujours être poussé vers le bas même avec la SecureView barrière en place :
+- PhotonSecureView orders
+- PhotonScan parquet orders[...]
PartitionFilters: [isnotnull(o_orderdate),
(o_orderdate = 1995-07-29)]
Utilisez des prédicats d’égalité simples sur des 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 restaure le pushdown de prédicat complet.
Réutiliser les masques de colonne lorsque possible
L’application de nombreux masques de colonne différents à une seule table augmente le coût par colonne. Masquez uniquement les colonnes qui contiennent des données véritablement sensibles.
Lorsque plusieurs colonnes nécessitent la même transformation (par exemple, en masquant par NULL ou en remplaçant par une chaîne fixe), utilisez la même fonction de masquage plutôt que de créer une fonction distincte pour chaque colonne.
Azure Databricks reconnaît les stratégies qui font référence à la même fonction UDF avec les mêmes arguments que le même masque effectif, de sorte que les fonctions de réutilisation évitent une surcharge inutile.
Éviter le masquage de regex sur les champs de texte volumineux
L’utilisation regexp_replace à l’intérieur d’un masque de colonne pour réacter des éléments dans un document sérialisé (XML ou JSON stocké en tant que colonne STRING) est coûteuse.
regexp_replace parcourt la chaîne complète pour chaque ligne. L’optimiseur traite la colonne STRING comme une valeur opaque et ne peut pas découper les parties inutilisées du document. Le moteur lit et réécrit la charge utile entière, même si la requête ne nécessite que quelques champs.
-- 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 en colonnes typées dans une table distincte, puis appliquez des masques de colonne à ces colonnes scalaires. La fonction mask fonctionne ensuite sur une seule petite valeur par ligne plutôt que sur l’ensemble du document sérialisé.
-- 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 en tant que colonne de structure au lieu de XML, utilisez le modèle de masquage flexible VARIANT pour masquer des champs individuels au sein de la structure. Consultez les colonnes de struct Mask avec VARIANT.
Tester les performances UDF
Tester à grande échelle
Testez les performances UDF sur au moins 1 million de lignes avant le déploiement en production. Outre les tests de mise à l’échelle synthétique, exécutez des requêtes 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 stratégie et mesurez l’effet de chaque modification plutôt que de tester uniquement la version finale.
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 la fonction UDF que vous testez. Comparez les résultats avec et sans la stratégie appliquée pour isoler la surcharge de la stratégie.