Melhores práticas de políticas de UDFs para ABAC
Esta página mostra como escrever funções definidas pelo usuário (UDFs) de alto desempenho para uso em políticas de filtro de linha e máscara de coluna ABAC no Unity Catalog.
Por que o desempenho do UDF é importante
As políticas ABAC são executadas em cada execução de consulta para cada linha ou coluna aplicável. UDFs ineficientes podem:
- Bloqueie a execução paralela e a redução de predicados
- Forçar junções ou pesquisas caras por linha
- Aumente o tempo de consulta de milissegundos para segundos (ou mais)
- Quebre o armazenamento em cache e a vetorização
- Tornar a análise e os relatórios não confiáveis
Em ambientes de produção, isso pode tornar a governança de dados um gargalo em vez de um facilitador.
Regras de ouro para UDFs ABAC
- Mantenha a simplicidade : dê preferência a declarações
CASE
básicas e expressões booleanas claras. - Mantenha-se determinista : a mesma entrada deve sempre produzir a mesma saída para armazenamento em cache e junção consistente.
- Evite chamadas externas : não há chamadas de API ou pesquisas em outros bancos de dados.
- Use apenas funções integradas dentro do seu UDF : não chame outros UDFs de dentro de um UDF.
- Teste em escala : valide o desempenho em pelo menos 1 milhão de linhas.
- Minimize a complexidade : evite aninhamento em vários níveis e chamadas de funções desnecessárias.
- Referências somente de coluna: faça referência somente às colunas da tabela de destino para ativar o pushdown.
Antipadrões comuns a serem evitados
- Chamadas de API externas dentro de UDFs : causam latência de rede, timeouts e pontos únicos de falha.
- Subconsultas ou junções complexas : força a junção de loops aninhados e impede a otimização.
- Regex pesado em texto grande : uso intenso de CPU e memória por linha.
- Pesquisas de metadados : evite verificações por linha que atinjam metadados ou fontes de identidade, como consultas
information_schema
, pesquisas de perfil de usuário,is_member()
ouis_account_group_member()
. Adiciona digitalizações extras para cada registro. - Geração de SQL dinâmico : não há otimização de consultas e impede o armazenamento em cache.
- Lógica não determinística : impede o armazenamento em cache e a união consistente, consulte Impacto da lógica não determinística.
Mantenha as verificações de acesso nas políticas, não nas UDFs
Um erro comum é chamar is_account_group_member
() ou is_member()
diretamente dentro de um UDF. Isso torna a função mais lenta e dificulta a reutilização da UDF.
Em vez disso, siga esse padrão:
- Função UDF : Concentrar-se apenas em como transformar, mascarar ou filtrar os dados. Use somente as colunas e os parâmetros passados para ele.
- Função da política : Defina quem (diretores, grupos) e quando (tags) o UDF deve ser aplicado, fazendo referência aos diretores na política ABAC.
Impacto da lógica não determinística
Alguns cenários (como o mascaramento aleatório para pesquisa) exigem uma saída diferente a cada vez.
Se você precisar usar funções não determinísticas:
- Espere um desempenho mais lento devido à ausência de cache.
- pode falhar ou retornar resultados inconsistentes.
- Os relatórios podem mostrar dados diferentes entre as execuções.
- A solução de problemas e a validação podem ser mais difíceis.
Exemplos de UDF
abaixo são padrões de fácil produção para mascaramento de colunas e filtragem de linhas. Todos os exemplos seguem as práticas recomendadas de desempenho do ABAC: lógica simples, comportamento determinístico, nenhuma chamada externa e uso apenas de funções integradas.
Máscara de coluna: rápida e determinística
-- Deterministically pseudonymize patient_id with a version tag for rotation.
CREATE OR REPLACE FUNCTION mask_patient_id_fast(patient_id STRING)
RETURNS STRING
DETERMINISTIC
RETURN CONCAT('REF_', SHA2(CONCAT(patient_id, ':v1'), 256));
Por que isso funciona:
- Declaração CASE simples
- Sem dependências externas
- Resultados determinísticos para junção consistente
- Manter a lógica principal fora do UDF
Máscara de coluna: revelação parcial sem pontos ativos de regex
-- Reveal only the last 4 digits of an SSN, masking the rest.
CREATE OR REPLACE FUNCTION mask_ssn_last4(ssn STRING)
RETURNS STRING
DETERMINISTIC
RETURN CASE
WHEN ssn IS NULL THEN NULL
WHEN LENGTH(ssn) >= 4 THEN CONCAT('XXX-XX-', RIGHT(REGEXP_REPLACE(ssn, '[^0-9]', ''), 4))
ELSE 'MASKED'
END;
Por que isso funciona:
- Usa um único regex leve para remover números que não sejam dígitos
- Evita várias passagens de regex em campos de texto grandes
Máscara de coluna: pseudonimização determinística com controle de versão
-- Create a consistent pseudonymized reference ID.
CREATE OR REPLACE FUNCTION mask_id_deterministic(id STRING)
RETURNS STRING
DETERMINISTIC
RETURN CONCAT('REF_', SHA2(CONCAT(id, ':v1'), 256));
Por que isso funciona:
- Preserva a união em todo o conjunto de dados mascarados
- Inclui uma tag de versão () para suportar a rotação do key sem quebrar os dados históricos intencionalmente
Filtro de linha: fácil de pressionar por região
-- Returns TRUE if the row's state is in the allowed set.
CREATE OR REPLACE FUNCTION filter_by_region(state STRING, allowed ARRAY<STRING>)
RETURNS BOOLEAN
DETERMINISTIC
RETURN array_contains(TRANSFORM(allowed, x -> lower(x)), lower(state));
Por que isso funciona:
- Lógica booleana simples
- Referencia somente colunas da tabela
- Permite a inserção e vetorização de predicados
Filtro de linha: multicondição determinística
-- Returns TRUE if the row's region is in the allowed set.
CREATE OR REPLACE FUNCTION filter_region_in(region STRING, allowed_regions ARRAY<STRING>)
RETURNS BOOLEAN
DETERMINISTIC
RETURN array_contains(TRANSFORM(allowed_regions, x -> lower(x)), lower(region));
Por que isso funciona:
- Suporta várias funções e regiões geográficas em uma função
- Mantém a lógica plana para uma melhor otimização
Teste de desempenho do UDF
Use testes de escala sintética para validar o comportamento e o desempenho antes da produção. Por exemplo:
WITH test_data AS (
SELECT
patient_id,
your_mask_function(patient_id) as masked_id,
current_timestamp() as start_time
FROM (
SELECT CONCAT('PAT', LPAD(seq, 6, '0')) as patient_id
FROM range(1000000) -- 1 million test rows
)
)
SELECT
COUNT(*) as rows_processed,
MAX(start_time) - MIN(start_time) as total_duration,
COUNT(*) / EXTRACT(EPOCH FROM (MAX(start_time) - MIN(start_time))) as rows_per_second
FROM test_data;