UDFs tabulares (UDTFs) de Java¶
Este documento explica como escrever uma UDTF (função de tabela definida pelo usuário) em Java.
Neste tópico:
Introdução¶
Uma UDTF pode ser escrito em qualquer uma das linguagens a seguir:
SQL
JavaScript
Java
Cada UDTF é criada ao executar a instrução CREATE FUNCTION, que especifica:
Os tipos de dados dos argumentos passados para a UDTF.
Os tipos de dados das colunas nas linhas retornadas da UDTF.
O código a ser executado quando a UDTF é chamada. Por exemplo, se a UDTF é implementada como uma UDTF JavaScript, a instrução
CREATE FUNCTION
especifica a função de JavaScript a ser chamada.
Um desenvolvedor de UDTF cria uma função (ou método) por linha, que é chamada uma vez para cada linha de entrada. O método por linha aceita zero ou mais parâmetros. Para cada linha de entrada, o método por linha retorna um conjunto de zero, uma ou mais linhas de saída.
Atualmente, UDTFs escritas em todas as linguagens suportadas, exceto SQL, têm as seguintes características adicionais:
Opcionalmente, as linhas podem ser agrupadas em partições. Todas as linhas dentro de uma partição são processadas juntas (ou seja, são passadas sequencialmente para a mesma instância do método por linha). Opcionalmente, a saída pode conter uma ou mais linhas que são baseadas nas características comuns de linhas dentro da partição.
Para dados particionados, UDTFs oferecem suporte para um método opcional inicializador que é chamado uma vez por partição, antes que o método por linha comece a processar a primeira linha da partição. O método inicializador pode inicializar variáveis a serem utilizadas para toda a partição.
Para dados particionados, UDTFs oferecem suporte para um método opcional finalizador que é chamado uma vez por partição, depois que o método por linha termina de processar a última linha da partição. O método finalizador permite que o código retorne zero, uma, ou várias linhas de saída que não estejam ligadas a uma linha de entrada específica. As linhas de saída podem ser:
Linhas que são adicionadas à saída já gerada para a partição.
Uma ou mais linhas que resumem a partição. Neste caso, o método por linha pode não ter gerado nenhuma linha de entrada; ele pode simplesmente ter realizado cálculos que são usados pelo método finalizador quando o finalizador gera suas linhas de saída.
A coluna na qual as linhas serão particionadas está especificada na instrução SQL que chama a UDTF.
Para obter mais informações sobre partições, consulte Funções de tabela e partições.
Para UDTFs de Java, o método inicializador, o método por linha e o método finalizador são implementados da forma descrita abaixo:
O inicializador é implementado através da escrita de um construtor sem argumentos.
O método por linha é denominado
process
.O finalizador é implementado como um método sem argumentos chamado
endPartition
.
Cada UDTF Java requer uma classe de manipulador que define o construtor, process
e endPartition
. Você encontra detalhes em Classes para UDTFs de Java (neste tópico).
Cada UDTF de Java também requer uma classe de linha de saída, que especifica os tipos de dados de Java das colunas da(s) linha(s) de saída que são geradas pela classe do manipulador. Você encontra detalhes em A classe da linha de saída (neste tópico).
Nota
Funções tabulares (UDTFs) têm um limite de 500 argumentos de entrada e 500 colunas de saída.
Classes para UDTFs de Java¶
Os componentes primários da UDTF são a classe do manipulador e a classe da linha de saída.
A classe do manipulador¶
O Snowflake interage com a UDTF principalmente invocando os seguintes métodos da classe do manipulador:
O inicializador (o construtor).
O método por linha (
process
).O método finalizador (
endPartition
).
A classe do manipulador pode conter métodos adicionais necessários para apoiar esses três métodos.
A classe do manipulador também contém um método getOutputClass
, que é descrito mais adiante.
Gerar uma exceção de qualquer método na classe do manipulador (ou na classe da linha de saída) faz com que o processamento seja interrompido. A consulta que chamou a UDTF falha com uma mensagem de erro.
O construtor¶
Uma classe de manipulador pode ter um construtor, que deve aceitar zero argumentos.
O construtor é invocado uma vez para cada partição antes de qualquer invocação de process
.
O construtor não pode produzir linhas de saída.
Use o construtor para inicializar o estado para a partição; esse estado pode ser usado pelos métodos process
e endPartition
. O construtor também é o lugar adequado para colocar qualquer inicialização de longo prazo que precisa ser feita apenas uma vez por partição e não uma vez por linha.
O construtor é opcional.
O método process
¶
O método process
é invocado uma vez para cada linha na partição de entrada.
Os argumentos passados à UDTF são passados ao process
. Os valores dos argumentos são convertidos de tipos de dados de SQL para tipos de dados de Java. (Para obter mais informações sobre mapeamento de tipos de dados de SQL e Java, consulte Mapeamentos de tipos de dados SQL-Java para tipos de parâmetros e retorno).
Os nomes dos parâmetros do método process
podem ser quaisquer identificadores de Java válidos; os nomes não precisam corresponder aos nomes especificados na instrução CREATE FUNCTION
.
Cada vez que process
é chamado, ele pode retornar zero, uma ou várias linhas.
O tipo de dados retornado pelo método process
deve ser Stream<OutputRow>
, onde Stream é definido em java.util.stream.Stream, e OutputRow
é o nome da classe da linha de saída. O exemplo abaixo mostra um método simples process
que simplesmente retorna sua entrada através de um Stream:
import java.util.stream.Stream;
...
public Stream<OutputRow> process(String v) {
return Stream.of(new OutputRow(v));
}
...
Se o método process
não mantiver ou usar nenhum estado no objeto (por exemplo, se o método for projetado para apenas excluir linhas de entrada selecionadas da saída), você pode declarar o método static
. Se o método process
for static
e a classe do manipulador não tiver um construtor ou método endPartition
não estático, o Snowflake passará cada linha diretamente para o método process
estático sem construir uma instância da classe do manipulador.
Se você precisar pular uma linha de entrada e processar a próxima linha (por exemplo, se você estiver validando as linhas de entrada), retorne um objeto Stream
vazio. Por exemplo, o método process
abaixo só retorna as linhas para as quais number
é um número inteiro positivo. Se number
não for positivo, o método retorna um objeto Stream
vazio para pular a linha atual e continuar processando a próxima linha.
public Stream<OutputRow> process(int number) {
if (inputNumber < 1) {
return Stream.empty();
}
return Stream.of(new OutputRow(number));
}
Se process
retornar um Stream nulo, o processamento é interrompido. (O método endPartition
ainda é chamado mesmo que um Stream nulo seja retornado).
Esse método é exigido.
O método endPartition
¶
Esse método pode ser usado para gerar linhas de saída baseadas em qualquer informação de estado agregada em process
.
Esse método é invocado uma vez para cada partição depois que todas as linhas nessa partição tiverem sido passadas para process
.
Nota
Se o usuário não particionar os dados explicitamente, o Snowflake particionará os dados implicitamente. Para obter mais detalhes, consulte: partições.
Esse método pode produzir zero, uma ou várias linhas.
Esse método é opcional.
O método getOutputClass
¶
Esse método retorna informações sobre a classe da linha de saída. A classe da linha de saída contém informações sobre os tipos de dados da linha retornada.
A classe da linha de saída¶
O Snowflake usa a classe da linha de saída para ajudar a especificar as conversões entre tipos de dados de Java e de SQL.
Quando uma UDTF de Java retorna uma linha, o valor em cada coluna da linha deve ser convertido do tipo de dados de Java para o tipo de dados de SQL correspondente. Os tipos de dados de SQL são especificados na cláusula RETURNS
da instrução CREATE FUNCTION
. Entretanto, o mapeamento entre os tipos de dados de Java e de SQL não tem uma correspondência exata; portanto, o Snowflake precisa saber o tipo de dados de Java para cada coluna retornada. (Para obter mais informações sobre o mapeamento de tipos de dados de SQL e Java, consulte Mapeamentos de tipos de dados SQL-Java para tipos de parâmetros e retorno).
Uma UDTF de Java especifica os tipos de dados de Java das colunas de saída definindo uma classe de linha de saída. Cada linha retornada da UDTF é retornada como uma instância da classe da linha de saída. Cada instância da classe da linha de saída contém um campo público para cada coluna de saída. O Snowflake lê os valores dos campos públicos de cada instância da classe da linha de saída, converte os valores de Java para valores de SQL e cria uma linha de saída de SQL contendo esses valores.
Os valores em cada instância da classe da linha de saída são definidos chamando o construtor da classe da linha de saída. O construtor aceita parâmetros que correspondem às colunas de saída e depois define os campos públicos para esses parâmetros.
O código abaixo define uma classe de linha de saída de exemplo:
class OutputRow {
public String name;
public int id;
public OutputRow(String pName, int pId) {
this.name = pName;
this.id = pId
}
}
As variáveis públicas especificadas por essa classe devem corresponder às colunas especificadas na cláusula RETURNS TABLE (...)
da instrução CREATE FUNCTION
. Por exemplo, a classe OutputRow
acima corresponde à cláusula RETURNS
abaixo:
CREATE FUNCTION F(...)
RETURNS TABLE(NAME VARCHAR, ID INTEGER)
...
Importante
A correspondência entre os nomes das colunas de SQL e os nomes dos campos públicos de Java na classe da linha de saída não diferencia maiúsculas e minúsculas. Por exemplo, no código de Java e de SQL mostrado acima, o campo de Java chamado id
corresponde à coluna de SQL chamada ID
.
A classe da linha de saída é utilizada da seguinte forma:
A classe do manipulador usa a classe da linha de saída para especificar o tipo de retorno do método
process
e do métodoendPartition
. A classe do manipulador também usa a classe da linha de saída para construir valores retornados. Por exemplo:public Stream<OutputRow> process(String v) { ... return Stream.of(new OutputRow(...)); } public Stream<OutputRow> endPartition() { ... return Stream.of(new OutputRow(...)); }A classe de linha de saída também é usada no método
getOutputClass
da classe do manipulador, que é um método estático que o Snowflake chama para aprender os tipos de dados de Java a partir das saídas:public static Class getOutputClass() { return OutputRow.class; }
A geração de uma exceção de qualquer método na classe de linha de saída (ou na classe do manipulador) faz com que o processamento pare. A consulta que chamou a UDTF falha com uma mensagem de erro.
Resumo dos requisitos¶
O código da UDTF de Java deve atender aos seguintes requisitos:
O código deve definir uma classe de linha de saída.
A classe do manipulador da UDTF deve incluir um método público chamado
process
que retorna um Stream de<output_row_class>
, onde Stream é definido em java.util.stream.stream.Stream.A classe do manipulador da UDTF deve definir um método estático público chamado
getOutputClass
, que deve retornar<output_row_class>.class
.
Se o de código Java não atender a esses requisitos, a criação ou execução da UDTF falha:
Se a sessão tiver um warehouse ativo no momento em que a instrução CREATE FUNCTION for executada, o Snowflake detectará violações quando a função for criada.
Se a sessão não tiver um warehouse ativo no momento em que a instrução CREATE FUNCTION for executada, o Snowflake detectará violações quando a função for chamada.
Como chamar uma UDTF de Java¶
Chame uma UDTF da mesma forma como você chamaria qualquer função de tabela. 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:
SELECT ...
FROM TABLE ( udtf_name (udtf_arguments) )
Por exemplo, a instrução a seguir chama uma função de tabela e a passa um literal DATE:
select ...
from table(my_java_udtf('2021-01-16'::DATE));
O argumento para uma função de tabela pode ser uma expressão e não apenas um literal. Por exemplo, uma função de tabela pode ser chamada usando uma coluna de uma tabela. Alguns exemplos estão abaixo, inclusive na seção Exemplos.
Para obter mais informações sobre as funções de tabela em geral, consulte função de tabela.
Funções de tabela e partições¶
Antes que linhas sejam passadas para funções de tabela, elas podem ser agrupadas em partições. A criação de partições tem dois benefícios principais:
A criação de partições permite que o Snowflake divida a carga de trabalho para melhorar a paralelização e, assim, o desempenho.
A criação de partições permite que o Snowflake processe todas as linhas com uma característica comum como um grupo. Você pode retornar resultados que são baseados em todas as linhas do grupo e não apenas em linhas individuais.
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 analisados em conjunto, enquanto os preços das ações de cada empresa podem ser analisados independentemente de qualquer outra empresa.
Dados podem ser particionados de forma explícita ou implícita.
Particionamento explícito¶
Particionamento explícito em vários grupos¶
A instrução a seguir chama a UDTF chamada my_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, a mesma empresa ou símbolo de ação).
SELECT *
FROM stocks_table AS st,
TABLE(my_udtf(st.symbol, st.transaction_date, st.price) OVER (PARTITION BY st.symbol))
Particionamento explícito em um único grupo¶
A instrução a seguir chama a UDTF chamada my_udtf
em uma partição. A cláusula PARTITION BY <constante>
(neste caso PARTITION BY 1
) coloca todas as linhas na mesma partição.
SELECT *
FROM stocks_table AS st,
TABLE(my_udtf(st.symbol, st.transaction_date, st.price) OVER (PARTITION BY 1))
Para obter um exemplo mais completo e realista, consulte Exemplos de como chamar UDTFs de Java em consultas, em particular a subseção intitulada Partição única.
Como ordenar linhas para partições¶
Para processar as linhas de cada partição em uma ordem especificada, inclua uma cláusula ORDER BY
. Isso diz ao Snowflake para passar as linhas para o método do manipulador por linha na ordem especificada.
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). O exemplo a seguir mostra como fazer isso:
SELECT *
FROM stocks_table AS st,
TABLE(my_udtf(st.symbol, st.transaction_date, st.price) OVER (PARTITION BY st.symbol ORDER BY st.transaction_date))
Uma cláusula OVER
pode conter uma cláusula ORDER BY
mesmo sem uma cláusula PARTITION BY
.
Lembre-se de que incluir uma cláusula ORDER BY
dentro de uma cláusula OVER ()
não é o mesmo que colocar uma cláusula ORDER BY
no nível mais externo da consulta. Se você quiser que todos os resultados da consulta sejam encomendados, você precisa de uma cláusula ORDER BY
separada. Por exemplo:
SELECT *
FROM stocks_table AS st,
TABLE(my_udtf(st.symbol, st.transaction_date, st.price) OVER (PARTITION BY st.symbol ORDER BY st.transaction_date))
ORDER BY st.symbol, st.transaction_date, st.transaction_time;
Notas de uso para particionamento explícito¶
Ao utilizar uma UDTF com uma cláusula PARTITION BY
, a cláusula PARTITION BY
deve utilizar uma referência de coluna ou uma expressão literal, não uma expressão geral. Por exemplo, o seguinte não é permitido:
SELECT * FROM udtf_table, TABLE(my_func(col1) OVER (PARTITION BY udtf_table.col2 * 2)); -- NO!
Particionamento implícito¶
Se uma função de tabela não particiona explicitamente as linhas usando uma cláusula PARTITION BY
, o Snowflake normalmente divide as linhas implicitamente para usar processamento paralelo para melhorar o desempenho.
O número de partições normalmente é baseado em fatores como o tamanho do warehouse que processa a função e a cardinalidade da relação de entrada. As linhas normalmente são atribuídas a partições específicas com base em fatores como a localização física das linhas (por exemplo, por micropartição), de modo que o agrupamento não tem significado.
Quando executado com particionamento implícito, o código do usuário não pode fazer suposições sobre partições. A execução com particionamento implícito é mais útil quando a UDTF só precisa olhar as linhas isoladamente para produzir sua saída e nenhum estado é agregado entre as linhas. Nesse caso, o código provavelmente não precisa de um construtor ou de um método endPartition
.
Se a UDF inclui um método finalizador (por exemplo, endPartition
para UDFs de Java), o finalizador é chamado em cada partição, independentemente de os dados terem sido particionados explícita ou implicitamente. Se os dados não forem particionados de forma significativa, a saída do finalizador pode não ser significativa.
Notas de uso para particionamento¶
Para melhorar o desempenho, o Snowflake normalmente executa várias instâncias do código do manipulador da UDTF em paralelo. Cada partição de linhas é passada para uma única instância da UDTF.
Embora cada partição seja processada por apenas uma instância de UDTF, o inverso não é necessariamente verdadeiro — uma única instância de UDTF pode processar várias partições sequencialmente. Portanto, é importante usar o inicializador e o finalizador para inicializar e limpar cada partição, de forma a evitar passar valores acumulados do processamento de uma partição para o processamento de outra partição.
Exemplos de como chamar UDTFs de Java em consultas¶
Chamada sem particionamento explícito¶
Este exemplo mostra como criar uma UDTF. Este exemplo retorna duas cópias de cada entrada e retorna uma linha adicional para cada partição.
create function return_two_copies(v varchar)
returns table(output_value varchar)
language java
handler='TestFunction'
target_path='@~/TestFunction.jar'
as
$$
import java.util.stream.Stream;
class OutputRow {
public String output_value;
public OutputRow(String outputValue) {
this.output_value = outputValue;
}
}
class TestFunction {
String myString;
public TestFunction() {
myString = "Created in constructor and output from endPartition()";
}
public static Class getOutputClass() {
return OutputRow.class;
}
public Stream<OutputRow> process(String inputValue) {
// Return two rows with the same value.
return Stream.of(new OutputRow(inputValue), new OutputRow(inputValue));
}
public Stream<OutputRow> endPartition() {
// Returns the value we initialized in the constructor.
return Stream.of(new OutputRow(myString));
}
}
$$;
Este exemplo mostra como chamar uma UDTF. Para manter este exemplo simples, a instrução passa um valor literal em vez de uma coluna e omite a cláusula OVER()
.
SELECT output_value
FROM TABLE(return_two_copies('Input string'));
+-------------------------------------------------------+
| OUTPUT_VALUE |
|-------------------------------------------------------|
| Input string |
| Input string |
| Created in constructor and output from endPartition() |
+-------------------------------------------------------+
Este exemplo chama a UDTF com valores lidos de outra tabela. Cada vez que o método process
é chamado, ele recebe um valor da coluna city_name
da linha atual da tabela cities_of_interest
. Como acima, a cláusula UDTF é chamada sem uma cláusula OVER()
explícita.
Crie uma tabela simples para ser usada como fonte de entradas:
CREATE TABLE cities_of_interest (city_name VARCHAR);
INSERT INTO cities_of_interest (city_name) VALUES
('Toronto'),
('Warsaw'),
('Kyoto');
Chame a UDTF de Java:
SELECT city_name, output_value
FROM cities_of_interest,
TABLE(return_two_copies(city_name))
ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE |
|-----------+-------------------------------------------------------|
| Kyoto | Kyoto |
| Kyoto | Kyoto |
| Toronto | Toronto |
| Toronto | Toronto |
| Warsaw | Warsaw |
| Warsaw | Warsaw |
| NULL | Created in constructor and output from endPartition() |
+-----------+-------------------------------------------------------+
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 é que a função é chamada com os valores de cada linha da tabela. Em outras palavras, dada a seguinte cláusula FROM:
from cities_of_interest, table(f(city_name))
o comportamento seria equivalente ao seguinte pseudocódigo:
for city_name in cities_of_interest: output_row = f(city_name)
A seção de exemplos na documentação para UDTFs de JavaScript contém exemplos mais complexos de consultas que chamam UDTFs com valores de tabelas.
Se a instrução não especificar explicitamente o particionamento, o mecanismo de execução do Snowflake usa particionamento implícito.
Se houver apenas uma partição, o método endPartition
é chamado apenas uma vez e a saída da consulta inclui apenas uma linha que contém o valor Created in constructor and output from endPartition()
. Se os dados forem agrupados em diferentes números de partições durante diferentes execuções da instrução, o método endPartition
é chamado em números de vezes diferentes, e a saída contém números diferentes de cópias dessa linha.
Para obter mais informações, consulte partição implícita.
Chamada com partição explícita¶
UDTFs de Java também podem ser chamadas usando partição explícita.
Partições múltiplas¶
O exemplo a seguir utiliza a mesma UDTF e tabela criadas anteriormente. O exemplo particiona os dados por city_name.
SELECT city_name, output_value
FROM cities_of_interest,
TABLE(return_two_copies(city_name) OVER (PARTITION BY city_name))
ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE |
|-----------+-------------------------------------------------------|
| Kyoto | Created in constructor and output from endPartition() |
| Kyoto | Kyoto |
| Kyoto | Kyoto |
| Toronto | Created in constructor and output from endPartition() |
| Toronto | Toronto |
| Toronto | Toronto |
| Warsaw | Created in constructor and output from endPartition() |
| Warsaw | Warsaw |
| Warsaw | Warsaw |
+-----------+-------------------------------------------------------+
Partição única¶
O exemplo a seguir utiliza a mesma UDTF e tabela criadas anteriormente e particiona os dados por uma constante, o que força o Snowflake a usar somente uma única partição:
SELECT city_name, output_value
FROM cities_of_interest,
TABLE(return_two_copies(city_name) OVER (PARTITION BY 1))
ORDER BY city_name, output_value;
+-----------+-------------------------------------------------------+
| CITY_NAME | OUTPUT_VALUE |
|-----------+-------------------------------------------------------|
| Kyoto | Kyoto |
| Kyoto | Kyoto |
| Toronto | Toronto |
| Toronto | Toronto |
| Warsaw | Warsaw |
| Warsaw | Warsaw |
| NULL | Created in constructor and output from endPartition() |
+-----------+-------------------------------------------------------+
Note que apenas uma cópia da mensagem Created in constructor and output from endPartition()
foi incluída na saída, o que indica que endPartition
foi chamada apenas uma vez.
Processamento de entradas muito grandes (por exemplo, arquivos grandes)¶
Em alguns casos, uma UDTF requer uma quantidade muito grande de memória para processar cada linha de entrada. Por exemplo, uma UDTF pode ler e processar um arquivo que é grande demais para caber na memória.
Para processar arquivos grandes em uma UDF ou UDTF, use a classe SnowflakeFile
ou InputStream
. Para obter mais informações, consulte Processamento de dados não estruturados usando UDFs ou UDTFs de Java.
Como usar uma tabela ou UDTF como entrada para uma UDTF¶
A entrada para uma função de tabela pode vir de uma tabela ou de outra UDTF, como documentado em Como usar uma tabela como entrada para uma função de tabela.
O exemplo abaixo mostra como utilizar uma tabela para fornecer entrada para a UDTF de Java split_file_into_words
:
create table file_names (file_name varchar);
insert into file_names (file_name) values ('sample.txt'),
('sample_2.txt');
select f.file_name, w.word
from file_names as f, table(split_file_into_words(f.file_name)) as w;
A saída se assemelha ao seguinte:
+-------------------+------------+
| FILE_NAME | WORD |
+-------------------+------------+
| sample_data.txt | some |
| sample_data.txt | words |
| sample_data_2.txt | additional |
| sample_data_2.txt | words |
+-------------------+------------+
A cláusula imports
da UDTF do Java deve especificar o nome e o caminho de cada arquivo passado para a UDTF. Por exemplo:
create function split_file_into_words(inputFileName string)
...
imports = ('@inline_jars/sample.txt', '@inline_jars/sample_2.txt')
...
Cada arquivo já deve ter sido copiado para um estágio (neste caso, o estágio denominado @inline_jars
) antes que a UDTF leia o arquivo.
Para obter um exemplo de uso de uma UDTF como entrada para outra UDTF, consulte Exemplos estendidos usando valores de tabela e outras UDTFs como entrada na documentação UDTF de JavaScript.