UDFs tabulares de JavaScript (UDTFs)¶
Você pode escrever o manipulador para uma função de tabela definida pelo usuário (UDTF) no JavaScript.
Seu código do manipulador processa as linhas recebidas na chamada de UDTF e retorna um resultado tabular. As linhas recebidas são divididas, seja implicitamente pelo Snowflake ou explicitamente na sintaxe da chamada de função. Você usa funções de retorno de chamada escritas para processar linhas individuais, bem como as partições nas quais elas estão agrupadas.
O código JavaScript deve atender aos seguintes requisitos para que a UDTF seja válida:
O código deve definir um único objeto JavaScript literal.
O objeto definido deve incluir uma função de retorno chamada
processRow(). Para obter mais informações, consulte Funções de retorno de chamada de objeto.
Importante
Se o código em JavaScript não atender a esses requisitos, a UDTF ainda será criada; contudo, ela falhará quando for chamada em uma consulta.
Nota
Funções tabulares (UDTFs) têm um limite de 500 argumentos de entrada e 500 colunas de saída.
Funções de retorno de chamada de objeto¶
Através do código em JavaScript, o Snowflake interage com a UDTF invocando funções de retorno de chamada durante a execução da consulta. O esqueleto a seguir descreve todas as funções de retorno de chamada disponíveis e suas assinaturas esperadas:
Note que somente processRow() é necessária; as outras funções são opcionais.
processRow()¶
Essa função de chamada de retorno é invocada uma vez para cada linha na relação de entrada. Os argumentos para processRow() são passados no objeto row. Para cada um dos argumentos definidos na instrução CREATE FUNCTION usada para criar a UDTF, há uma propriedade no objeto row com o mesmo nome com todas as letras maiúsculas. O valor dessa propriedade é o valor do argumento para a linha atual. (O valor é convertido para um valor de JavaScript).
O argumento rowWriter é usado pelo código fornecido pelo usuário para produzir linhas de saída. O objeto rowWriter define uma única função, writeRow(). A função writeRow() leva um argumento, o objeto linha, que é uma única linha na tabela de saída representada como um objeto de JavaScript. Para cada coluna definida na cláusula RETURNS do comando CREATE FUNCTION, uma propriedade correspondente pode ser definida no objeto da linha. O valor dessa propriedade no objeto da linha torna-se o valor para a coluna correspondente na relação de saída. Qualquer coluna de saída sem uma propriedade correspondente no objeto da linha terá o valor NULL na tabela de resultados.
finalize()¶
A função de retorno de chamada finalize() é invocada uma vez, após todas as linhas terem sido passadas para processRow(). (Se os dados forem agrupados em partições, finalize() é invocado uma vez para cada partição, depois que todas as linhas nessa partição tiverem sido passadas para processRow()).
Essa função de retorno pode ser usada para produzir qualquer estado que possa ter sido agregado em processRow() usando a mesma linha rowWriter que é passada para processRow().
Nota
Enquanto o Snowflake oferece suporte a grandes partições com tempos limite ajustados para processá-las com sucesso, as grandes partições em especial podem causar um tempo limite no processamento (como quando finalize leva muito tempo para ser concluído). Entre em contato com o suporte Snowflake se você precisar ajustar o tempo limite para cenários específicos de uso.
initialize()¶
Essa função de retorno é invocada uma vez para cada partição antes de qualquer invocação de processRow().
Use initialize() para configurar qualquer estado necessário durante a computação do resultado.
O parâmetro initialize() da função argumentInfo contém metadados sobre os argumentos para a função definida pelo usuário. Por exemplo, se a UDF for definida como:
argumentInfo conterá informações sobre argument_1 e argument_2.
argumentInfo tem uma propriedade para cada um desses argumentos. Cada propriedade é um objeto com os seguintes valores:
type: String. O tipo desse argumento.isConst: Boolean. Se for verdadeiro, o valor desse argumento é constante (ou seja, é o mesmo para cada linha).constValue: SeisConst(como definido acima) for verdadeiro, essa entrada conterá o valor constante do argumento; caso contrário, esse campo seráundefined.
A função initialize() não pode produzir linhas de saída.
Notas de uso geral para funções de retorno de chamada¶
Todas as três funções de retorno de chamada recebem um objeto
context; esse objeto está reservado para uso futuro e atualmente está vazio.Cuidado
A modificação do objeto
contextpode produzir um comportamento indefinido.Funções e propriedades adicionais podem ser definidas, conforme necessário, para o objeto para uso na UDTF.
Os argumentos para as funções de retorno de chamada são posicionais e podem ter qualquer nome; entretanto, para os fins deste tópico, os nomes acima são usados para a discussão e exemplos restantes.
Partições¶
Em muitas situações, você pode querer agrupar linhas em partições. A criação de partições tem dois benefícios principais:
Ela permite agrupar linhas com base em uma característica comum. Isso permite processar todas as linhas dentro do grupo em conjunto, e processar cada grupo independentemente.
Isso permite que o Snowflake divida a carga de trabalho para melhorar a paralelização e, com isso, o desempenho.
Por exemplo, você pode particionar dados de preços de ações em um grupo por ação. Todos os preços de ações de uma empresa individual podem ser processados em conjunto, e os grupos para diferentes empresas são processados independentemente.
A instrução a seguir chama a UDTF chamada js_udtf() em partições individuais. Cada partição contém todas as linhas para as quais a expressão PARTITION BY é avaliada como o mesmo valor (por exemplo, o mesmo símbolo de ação).
Quando você especifica uma expressão de partição a ser usada com uma UDTF, o Snowflake chama:
initialize()uma vez para cada partição.processRow()uma vez para cada linha individual nessa partição.finalize()uma vez para cada partição (após processar a última linha nessa partição).
Você também pode querer processar as linhas de cada partição em uma ordem específica. Por exemplo, se você quiser calcular a média móvel do preço de uma ação ao longo do tempo, ordene os preços das ações por carimbo de data/hora (e particione por símbolo de ação ou empresa). O exemplo a seguir mostra como fazer isso:
Quando você especifica uma cláusula ORDER BY, as linhas são processadas na ordem definida pela expressão ORDER BY. Especificamente, as linhas são passadas para processRow() na ordem definida pela expressão ORDER BY.
Na maioria dos casos, a partição de dados melhora quase automaticamente as oportunidades de paralelização e, portanto, de maior desempenho. O Snowflake geralmente executa várias instâncias da UDTF em paralelo. (Para esta discussão, uma instância de uma UDTF de JavaScript é definida como uma instância do objeto de JavaScript usado para representar a função no Snowflake). Cada partição de linhas é passada para uma única instância da UDTF.
Observe, entretanto, que não há necessariamente uma relação de um para um entre as partições e as instâncias da UDTF. Embora cada partição seja processada por apenas uma instância da UDTF, o inverso não é necessariamente verdadeiro — uma única instância da UDTF pode processar várias partições. Portanto, é importante usar initialize() e finalize() para configurar e desmontar especificamente cada partição, por exemplo para evitar “transportar” valores acumulados do processamento de uma partição para o processamento de outra partição.
Colunas de resultados¶
Quando uma tabela é unida a uma função de tabela, como nos exemplos de particionamento acima, o conjunto de resultados pode conter o seguinte, dependendo do que for selecionado:
As colunas definidas na cláusula RETURNS do comando CREATE FUNCTION.
As colunas de tabela, incluindo as colunas usadas para partição dos dados e outras colunas, sejam elas usadas ou não como parâmetros de entrada para a UDTF.
Observe que as linhas produzidas no retorno de chamada processRow e as linhas produzidas por finalize diferem das seguintes maneiras:
Quando uma linha é produzida em
processRow, o Snowflake pode correlacioná-la a uma linha de entrada, ou seja, aquela passada para a função como o argumentorow. Observe que se uma determinada invocaçãoprocessRowproduz mais de uma linha, os atributos de entrada são correlacionados com cada linha de saída.Para linhas produzidas em
processRow, todas as colunas de entrada podem ser unidas à relação de saída.
Na função de retorno de chamada finalize, o Snowflake é incapaz de correlacioná-la a qualquer linha porque não há uma linha atual a ser correlacionada.
Para linhas produzidas no retorno de chamada
finalize, apenas as colunas usadas na cláusula PARTITION BY estão disponíveis (pois são as mesmas para qualquer linha na partição atual); todos os outros atributos são NULL. Se nenhuma cláusula PARTITION BY for especificada, todos esses atributos serão NULL.
Como chamar UDTFs de JavaScript em consultas¶
Ao chamar uma UDTF na cláusula FROM de uma consulta, especifique o nome e os argumentos da UDTF dentro dos parênteses que seguem a palavra-chave TABLE.
Em outras palavras, use um formulário como o seguinte para a palavra-chave TABLE quando chamar uma UDTF:
Nota
Para obter mais informações sobre como chamar UDFs e UDTFs, consulte Execução de uma UDF.
Sem particionamento¶
Este exemplo simples mostra como chamar uma UDTF. Este exemplo passa valores literais. A UDTF apenas retorna os parâmetros na ordem inversa da ordem em que foram passados. Este exemplo não usa particionamento.
Este exemplo chama uma UDTF e passa-lhe valores de outra tabela. Neste exemplo, a UDTF com o nome js_udtf é chamada uma vez para cada linha da tabela chamada tab1. Cada vez que a função é chamada, ela recebe valores das colunas c1 e c2 da linha atual. Como acima, a UDTF é chamada sem uma cláusula PARTITION BY.
Quando nenhum particionamento é usado, o mecanismo de execução do Snowflake particiona a entrada em si conforme vários fatores, tais como o tamanho do warehouse que processa a função e a cardinalidade da relação de entrada. Quando executado nesse modo, o código do usuário não pode fazer suposições sobre as partições. Isso é útil principalmente quando a função só precisa olhar as linhas isoladamente para produzir sua saída e nenhum estado é agregado entre linhas diferentes.
Particionamento explícito¶
UDTFs de JavaScript também podem ser chamado usando uma partição. Por exemplo:
Particionamento explícito com uma cláusula OVER vazia¶
Uma cláusula OVER vazia significa que cada linha pertence à mesma partição (ou seja, toda a relação de entrada é uma partição).
Nota
Você deve ter cuidado ao chamar uma UDTF de JavaScript com uma cláusula OVER vazia, porque isso limita o Snowflake à criação de uma instância da função e, portanto, o Snowflake é incapaz de paralelizar a computação.
Exemplos de UDTFs de JavaScript¶
Esta seção contém vários exemplos de UDTFs de JavaScript.
Exemplos básicos de Hello World¶
A UDTF de JavaScript a seguir não recebe nenhum parâmetro e sempre retorna os mesmos valores. Ela é fornecida principalmente para fins ilustrativos:
Saída:
A UDTF de JavaScript a seguir também é para fins de ilustração, mas utiliza um parâmetro de entrada. Observe que o JavaScript faz distinção entre maiúsculas e minúsculas, mas o SQL força os identificadores a usar maiúsculas; portanto, quando o código de JavaScript faz referência a um nome de parâmetro de SQL, o código de JavaScript deve usar maiúsculas.
Observe também que os parâmetros da função são acessados através do parâmetro chamado row na função get_params():
Saída:
Exemplos básicos ilustrando as funções de retorno de chamada¶
A UDTF de JavaScript a seguir ilustra todas as funções de retorno de chamada de API e várias colunas de saída. Ela simplesmente retorna todas as linhas como estão e fornece uma contagem do número de caracteres vistos em cada partição. Ela também ilustra como compartilhar o estado em uma partição usando uma referência THIS. Note que o exemplo usa uma função de retorno de chamada initialize() para inicializar o contador a zero; isso é necessário porque uma determinada instância de função pode ser usada para processar várias partições:
A consulta a seguir ilustra a chamada da UDTF CHAR_SUM na tabela parts sem particionamento:
Saída:
Quando nenhuma partição é especificada, o Snowflake define partições automaticamente. Neste exemplo, devido ao pequeno número de linhas, apenas uma partição é criada (ou seja, apenas uma invocação de finalize() é executada). Observe que a última linha tem valores NULL nas colunas de entrada.
A mesma consulta, mas com particionamento explícito:
Saída:
Este exemplo particiona sobre a coluna p, produzindo duas partições. Para cada partição, uma única linha é retornada na função retorno de chamada finalize(), produzindo um total de duas linhas, distinguidas pelo valor NULL na coluna s. Como p é a coluna PARTITION BY, as linhas criadas em finalize() têm o valor p que define a partição atual.
Exemplos estendidos usando valores de tabela e outras UDTFs como entrada¶
Esta UDTF básica converte um “intervalo” de endereços IP para uma lista completa de endereços IP. A entrada consiste nos primeiros 3 segmentos do endereço IP (por exemplo, '192.168.1') e, em seguida, no início e fim do intervalo usado para gerar o último segmento (por exemplo, 42 e 45):
Saída:
Com base no exemplo anterior, talvez você queira calcular endereços IP individuais para mais de um intervalo. Esta próxima instrução cria uma tabela de intervalos que pode ser usada para expandir para endereços IP individuais. A consulta então insere as linhas da tabela na UDTF range_to_values() para retornar os endereços IP individuais:
Saída:
Atenção
Neste exemplo, a sintaxe utilizada na cláusula FROM é idêntica à sintaxe de uma junção interna (ou seja, FROM t1, t2); entretanto, a operação realizada não é uma junção interna verdadeira. O comportamento real da função range_to_values() é chamada com os valores de cada linha da tabela ip_address changes. Em outras palavras, isso seria equivalente a escrever:
O conceito de passar valores a uma UDTF pode ser estendido a várias UDTFs. O próximo exemplo cria uma UDTF chamada fake_ipv4_to_ipv6() que “converte” endereços IPV4 para endereços IPV6. A consulta então chama a função como parte de uma instrução mais complexa envolvendo outra UDTF:
Saída:
A consulta a seguir usa as UDTFs fake_ipv4_to_ipv6 e range_to_values() criadas anteriormente com a entrada da tabela ip_address changes. Em outras palavras, ela começa com um conjunto de intervalos de endereços IP, converte-os em endereços IPV4 individuais e então pega cada endereço IPV4 e o “converte” para um intervalo de endereços IPV6:
Saída:
Observe que este exemplo usou uma sintaxe de junção duas vezes, mas nenhuma das operações foi uma junção verdadeira; ambas foram chamadas a uma UDTF usando a saída de uma tabela ou outra UDTF como entrada.
Uma junção interna verdadeira não leva em conta a ordem. Por exemplo, as instruções a seguir são idênticas:
table1 INNER JOIN table2 ON ...table2 INNER JOIN table1 ON ...A entrada de valores em uma UDTF não é uma verdadeira junção, e as operações levam em conta a ordem. Por exemplo, a consulta a seguir é idêntica ao exemplo anterior, com a diferença que ela inverte a ordem das UDTFs na cláusula FROM:
A consulta falha com a seguinte mensagem de erro:
SQL compilation error: error line 3 at position 35 invalid identifier 'RTV.IP_ADDRESS'
O identificador rtv.ip_address é inválido porque não foi definido antes de ser usado. Isso não aconteceria em uma junção verdadeira, mas esse erro pode ocorrer ao processar UDTFs usando uma sintaxe de junção.
Em seguida, tente uma instrução que mistura a entrada a uma UDTF com uma junção verdadeira; no entanto, lembre a inserção de uma entrada em uma UDTF e a realização de uma junção interna usam a mesma sintaxe, o que pode ser confuso:
Saída:
Atenção
O exemplo anterior funciona como descrito; no entanto, você deve tomar cuidado ao combinar UDTFs com junções verdadeiras porque isso pode resultar em um comportamento não-determinístico e/ou inesperado.
Observe também que esse comportamento pode mudar no futuro.