Pular para o conteúdo principal

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() ou is_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

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

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

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

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

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

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