Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
I criteri di filtro di riga e maschera di colonna introducono la logica eseguita in fase di query, quindi le prestazioni dipendono dalla progettazione dei criteri. Non esiste un unico approccio corretto per ogni carico di lavoro. L'approccio migliore dipende dal volume di dati, dai modelli di query, dal modo in cui gli utenti interagiscono con le tabelle protette e dal comportamento desiderato di mascheramento o filtro. Le sezioni seguenti illustrano le considerazioni più comuni sulle prestazioni. Usarli come elenco di controllo durante la progettazione dei criteri e testarli con query rappresentative prima della distribuzione nell'ambiente di produzione.
Informazioni generali sulle prestazioni
| Considerazione | Description |
|---|---|
| Ridurre la complessità delle UDF | La logica complessa delle UDF può inibire le prestazioni delle query; funzioni semplici garantiscono migliori prestazioni. |
| Approccio per indirizzare i principali | Decidere se implementare la logica basata su principale nelle clausole dei criteri TO/EXCEPT o all'interno della funzione definita dall'utente usando funzioni di identità. |
| Usare espressioni deterministiche e sicure per gli errori | Funzioni ed espressioni non deterministiche che possono generare errori riducono la capacità di Optimizer di memorizzare nella cache i risultati e riordinare le operazioni. |
| Evita le UDF di Python | Preferisci utilizzare le funzioni definite dall'utente SQL anziché le funzioni definite dall'utente Python, quando possibile. |
| Mantenere le tabelle di ricerca di piccole dimensioni | Le funzioni definite dall'utente che fanno riferimento a tabelle esterne offrono prestazioni ottimali quando tali tabelle sono sufficientemente piccole da trasmettere. |
| Comprendere il predicate pushdown nelle tabelle protette | Le interrogazioni su tabelle protette potrebbero non trarre vantaggio dall'eliminazione delle partizioni o dal clustering liquido se i predicati hanno effetti collaterali. |
| Riutilizzare le maschere di colonna laddove possibile | Ogni maschera distinta in una tabella comporta un sovraccarico; riutilizzare la stessa funzione tra le colonne può ridurla. |
| Evitare la maschera regex in campi di testo di grandi dimensioni | La maschera basata su regex nei documenti serializzati impone al motore di analizzare e riscrivere l'intero payload per ogni riga. |
Ridurre la complessità delle funzioni definite dall'utente
La funzione definita dall'utente in un criterio di controllo degli accessi basato su attributi (ABAC) viene eseguita per ogni riga (filtri di riga) o per ogni valore di colonna abbinante (maschere di colonna) durante l'esecuzione della query. La complessità della UDF influisce direttamente sulle prestazioni delle query.
Do:
- Mantenere semplici le funzioni definite dall'utente. Favorire istruzioni di base
CASEed espressioni booleane semplici. - Fare riferimento alle sole colonne della tabella di destinazione nelle funzioni definite dall'utente quanto più possibile. In questo modo si abilita il pushdown dei predicati.
- Se la tua funzione UDF deve fare riferimento a tabelle esterne, mantieni il riferimento esterno abbastanza piccolo da poter essere trasmesso. Assicurarsi che le tabelle a cui si fa riferimento siano ottimizzate e partizionate in modo da corrispondere al modello di accesso delle policy. Ad esempio, partizionare una tabella di ricerca dei criteri in base al nome utente.
- Evitare l'annidamento a più livelli e le chiamate di funzione non necessarie. Usare le funzioni SQL predefinite il più possibile.
Evitare:
- Chiamate API esterne o ricerche su altri database nelle funzioni definite dall'utente. Le chiamate di rete possono introdurre latenza e timeout aggiuntivi.
- Sottoquery o join complessi su tabelle di grandi dimensioni. In questo modo si evitano i join hash trasmessi e si forzano i join di cicli annidati.
- Regex pesante su campi di testo ampi. Vedere Regex in campi di testo di grandi dimensioni.
- Ricerche di metadati per riga, ad esempio ricerca su
information_schema.
Approccio per i principali
Quando si scrivono criteri ABAC, si decide dove implementare la logica basata sui principali: nelle clausole del criterio TO/EXCEPT o all'interno della funzione definita dall'utente usando funzioni di identità come current_user() e is_account_group_member().
In generale, usare le clausole della politica TO/EXCEPT per definire le entità a cui si applica una politica. In questo modo si semplifica la definizione dei criteri e la funzione definita dall'utente (UDF) si concentra sulla trasformazione, il filtraggio o il mascheramento dei dati. La EXCEPT clausola elimina completamente la policy per gli utenti esentati, il che significa che nessuna UDF viene eseguita per tali utenti.
Quando la logica condizionale è troppo complessa per le clausole principali dei criteri, le funzioni identity all'interno di una UDF (funzione definita dall'utente) sono una possibile alternativa. Queste funzioni vengono risolte una volta durante l'analisi delle query, non per riga. Più chiamate a funzioni di identità come is_account_group_member() con argomenti di gruppo diversi generano una singola chiamata API UC, quindi l'impatto sulle prestazioni è in genere minimo.
La seguente funzione definita dall'utente è efficiente perché si basa unicamente su funzioni identitarie, che vengono risolte una sola volta durante l'analisi delle query.
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;
Al contrario, la seguente UDF è più lenta perché codifica i privilegi in una tabella secondaria, il che richiede una consultazione aggiuntiva della tabella.
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;
Usare espressioni deterministiche e sicure per gli errori
Usare espressioni deterministiche che non possono generare errori nelle funzioni definite dall'utente nelle politiche e nelle query su tabelle protette.
Funzioni non deterministiche (funzioni che restituiscono risultati diversi per lo stesso input, ad esempio rand() o now()) impediscono a Optimizer di memorizzare nella cache i risultati o di applicare la riduzione costante. Sia le UDF SQL che Python supportano la parola chiave DETERMINISTIC nell'istruzione CREATE FUNCTION. Per le UDF SQL, l'ottimizzatore deriva automaticamente il determinismo dal corpo della funzione, ma è anche possibile impostarlo in modo esplicito. Per le UDF in Python, l'ottimizzatore non può esaminare il corpo della funzione, quindi è importante contrassegnare esplicitamente una UDF Python come deterministica per abilitare la memorizzazione nella cache dei risultati per le chiamate con argomenti identici.
Alcune espressioni generano errori se gli input non sono validi, ad esempio la divisione ANSI su un denominatore zero. Quando il compilatore SQL rileva questa possibilità, non può eseguire il push di operazioni come i filtri nel piano di query. In questo modo è possibile attivare errori che rivelano informazioni sui valori prima che il filtro o la maschera venga applicato. Usare alternative sicure per gli errori, ad try_divide esempio anziché /, try_cast anziché CAST, e try_to_number invece di to_number. Questi restituiscono NULL in caso di errore invece di generare, che consente all'ottimizzatore di riorganizzare e piegare liberamente le espressioni.
Evita le UDF Python
Evitare le funzioni definite dall'utente (UDF) in Python nei criteri di controllo degli accessi basati sugli attributi quando possibile. Le funzioni definite dall'utente in Python devono essere incapsulate in una UDF SQL per essere usate nelle policy. In genere, sono più lente rispetto alle funzioni SQL definite dall'utente perché l'ottimizzatore non può inserirle direttamente o ottimizzarle, e la funzione Python viene eseguita su ogni riga della tabella di destinazione.
Se una funzione definita dall'utente Python è inevitabile, vedere Espressioni deterministiche, sicure dagli errori per come contrassegnarla come DETERMINISTIC per abilitare la memorizzazione nella cache dei risultati.
Mantenere le tabelle di ricerca di piccole dimensioni
Un modello comune consiste nel controllare i diritti di accesso rispetto a una tabella di ricerca di piccole dimensioni, ad esempio una tabella che esegue il mapping degli utenti ai livelli di priorità consentiti. Se la tabella di ricerca è significativamente più piccola della tabella di destinazione, l'ottimizzatore converte la sottoquery in un hash join broadcast. La tabella di ricerca viene copiata in ogni executor e archiviata in memoria come hashmap, che consente un filtro rapido durante l'analisi della tabella. Per un esempio di codice, vedere tabelle di ricerca nei criteri delle funzioni definite dall'utente ABAC.
- Se la tabella di ricerca è grande, l'ottimizzatore torna a un join di mescolamento, che è più lento.
- Se il predicato di ricerca è complesso (non un semplice controllo di uguaglianza), anche il join in modalità broadcast potrebbe diventare non idoneo.
- Anche con il broadcast hash join, ogni riga comporta comunque il costo della ricerca in una tabella hash durante l'esecuzione.
Comprendere il pushdown del predicato nelle tabelle protette
Il predicate pushdown è un'ottimizzazione delle prestazioni in cui il motore spinge le condizioni di filtro al livello di archiviazione. Ciò consente al motore di ignorare intere partizioni di dati che non corrispondono alla query, riducendo significativamente l'I/O e accelerando l'esecuzione.
Per le tabelle protette da filtri di riga e maschere di colonna, questa ottimizzazione è più complessa. Questa è l'origine più comune dei problemi di prestazioni con le tabelle protette e la più difficile da risolvere, perché gli autori di criteri non possono controllare quali query gli utenti eseguono su tabelle protette.
L'effetto della barriera SecureView sul pushdown dei predicati
Sia ABAC che i filtri di riga a livello di tabella e le maschere di colonna usano una SecureView barriera per impedire il push dei predicati con effetti collaterali oltre la barriera della policy. Ciò protegge dalla perdita di dati sul canale secondario, ma può anche bloccare l'eliminazione delle partizioni e le ottimizzazioni del clustering liquido, che possono forzare una scansione completa della tabella. Questo si applica anche quando l'UDF dei criteri si risolve in una costante true (ovvero, nessuna riga viene effettivamente filtrata). La presenza di un criterio in una tabella introduce la SecureView barriera.
Filtri interessati dalla barriera
In genere, l'ottimizzatore può propagare solo i predicati privi di effetti collaterali attraverso la SecureView barriera.
-
Spinto verso il basso (veloce): confronti di uguaglianza semplici (
WHERE col = 'value') e confronti di intervalli basilari (WHERE col > 100). Questi sono privi di effetti collaterali e non rischiano la perdita di dati. -
Bloccato (più lento): Predicati che fanno chiamate di funzioni (
WHERE date_format(col, 'yyyy-MM-dd') = '1995-07-29') o introducono cast di tipi impliciti. Queste vengono mantenute sopra laSecureViewbarriera, il che significa che il motore deve analizzare la tabella prima di applicare il filtro.
L'esempio seguente mostra la differenza. Si consideri una tabella con una chiave di partizione in o_orderdate e una query che filtra usando date_format:
EXPLAIN SELECT * FROM orders
WHERE date_format(o_orderdate, 'yyyy-MM-dd') = '1995-07-29'
Senza una regola, il date_format predicato appare in PartitionFilters dentro il nodo PhotonScan, il che significa che l'eliminazione della partizione è attiva:
+- PhotonScan parquet orders[...]
PartitionFilters: [isnotnull(o_orderdate),
(date_format(cast(o_orderdate as timestamp), yyyy-MM-dd, ...))]
Con una politica (anche una che restituisce sempre true), la barriera SecureView blocca il predicato. Passa a un PhotonFilter sopra la scansione invece di rimanere in PartitionFilters, che comporta un'analisi completa della tabella:
+- PhotonFilter (date_format(cast(o_orderdate as timestamp),
yyyy-MM-dd, ...) = 1995-07-29)
+- PhotonSecureView orders
+- PhotonScan parquet orders[...]
PartitionFilters: [isnotnull(o_orderdate)]
Un predicato più semplice come WHERE o_orderdate = '1995-07-29' non ha effetti collaterali e può ancora essere spinto giù anche con la SecureView barriera in atto:
+- PhotonSecureView orders
+- PhotonScan parquet orders[...]
PartitionFilters: [isnotnull(o_orderdate),
(o_orderdate = 1995-07-29)]
Usare predicati di uguaglianza semplici nelle tabelle protette, quando possibile. Per gli utenti esentati, usare la clausola EXCEPT nella policy per eliminare la barriera SecureView completamente, il che ripristina il full predicate pushdown.
Riutilizzare le maschere di colonna laddove possibile
L'applicazione di molte maschere di colonna distinte a una singola tabella aumenta il costo per colonna. Mascherare solo le colonne che contengono dati realmente sensibili.
Quando più colonne richiedono la stessa trasformazione, ad esempio l'oscuramento a NULL o la sostituzione con una stringa fissa, riutilizzare la stessa funzione di mascheramento anziché creare una funzione separata per ogni colonna.
Azure Databricks riconosce i criteri che fanno riferimento alla stessa UDF (funzione definita dall'utente) con gli stessi argomenti come la stessa maschera applicabile, quindi riutilizzare le funzioni evita un sovraccarico non necessario.
Evitare la maschera regex in campi di testo di grandi dimensioni
L'uso di regexp_replace all'interno di una maschera di colonna per redigere gli elementi all'interno di un documento serializzato (XML o JSON archiviato come tipo STRINGA) è oneroso.
regexp_replace percorre interamente la stringa per ogni riga. Optimizer considera la colonna STRING come valore opaco e non può eliminare parti inutilizzate del documento. Il motore legge e riscrive l'intero payload anche quando la query richiede solo alcuni campi.
-- 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;
Materializzare invece i campi sensibili in colonne tipizzate in una tabella separata, quindi applicare maschere di colonna a tali colonne scalari. La funzione mask opera quindi su un singolo valore piccolo per riga anziché sull'intero documento serializzato.
-- 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;
Se è possibile archiviare i dati come colonna di struct invece di XML, usare il modello di maschera flessibile VARIANT per redigire singoli campi all'interno dello struct. Vedere Mascherare le colonne struct con VARIANT.
Testare le prestazioni della UDF
Test su larga scala
Testare le prestazioni della funzione definita dall'utente su almeno 1 milione di righe prima della distribuzione in ambiente di produzione. Oltre ai test di scalabilità sintetica, eseguire query che rappresentano il carico di lavoro effettivo previsto nella tabella protetta. Apportare modifiche incrementali alle funzioni dei criteri e misurare l'effetto di ogni modifica anziché testare solo la versione 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;
Sostituire your_mask_function con l'UDF che si sta testando. Confrontare i risultati con e senza la politica applicata per isolare il sovraccarico della politica.