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:
{
processRow: function (row, rowWriter, context) {/*...*/},
finalize: function (rowWriter, context) {/*...*/},
initialize: function (argumentInfo, context) {/*...*/},
}
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:
CREATE FUNCTION f(argument_1 INTEGER, argument_2 VARCHAR) ...
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
context
pode 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).
SELECT * FROM tab1, TABLE(js_udtf(tab1.c1, tab1.c2) OVER (PARTITION BY <expression>)) ...;
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:
SELECT * FROM tab1, TABLE(js_udtf(tab1.c1, tab1.c2) OVER (PARTITION BY <expression> ORDER BY <expression>)) ...;
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¶
Esta seção descreve as colunas que são retornadas por uma UDTF de JavaScript:
Todas as colunas definidas na cláusula RETURNS do comando CREATE FUNCTION são retornadas na relação de saída.
Todas as colunas passadas para a UDTF também são retornadas.
Há uma distinção entre as linhas produzidas na função de retorno de chamada processRow()
e as linhas produzidas pela função finalize()
:
Quando uma linha é produzida em
processRow()
, o Snowflake pode correlacioná-la a uma entrada, ou seja, aquela que passou para a função como o argumentorow
. Observe que se uma determinada invocaçãoprocessRow()
produz mais de uma linha, os atributos de entrada são duplicados em cada linha de saída.Para linhas produzidas em
processRow()
, todas as colunas de entrada são duplicadas na 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 as linhas produzidas na função de retorno de chamada
finalize()
, somente as colunas usadas na cláusula PARTITION BY são duplicadas (pois elas 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 os atributos de entrada 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:
SELECT ...
FROM TABLE ( udtf_name (udtf_arguments) )
Nota
Para obter mais informações sobre como chamar UDFs e UDTFs, consulte Como chamar 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.
SELECT * FROM TABLE(js_udtf(10.0::FLOAT, 20.0::FLOAT));
+----+----+
| Y | X |
|----+----|
| 20 | 10 |
+----+----+
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
.
SELECT * FROM tab1, TABLE(js_udtf(tab1.c1, tab1.c2)) ;
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:
SELECT * FROM tab1, TABLE(js_udtf(tab1.c1, tab1.c2) OVER (PARTITION BY tab1.c3 ORDER BY tab1.c1));
Particionamento explícito com uma cláusula OVER
vazia¶
SELECT * FROM tab1, TABLE(js_udtf(tab1.c1, tab1.c2) OVER ());
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:
CREATE OR REPLACE FUNCTION HelloWorld0()
RETURNS TABLE (OUTPUT_COL VARCHAR)
LANGUAGE JAVASCRIPT
AS '{
processRow: function f(row, rowWriter, context){
rowWriter.writeRow({OUTPUT_COL: "Hello"});
rowWriter.writeRow({OUTPUT_COL: "World"});
}
}';
SELECT output_col FROM TABLE(HelloWorld0());
Saída:
+------------+
| OUTPUT_COL |
+============+
| Hello |
+------------+
| World |
+------------+
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()
:
CREATE OR REPLACE FUNCTION HelloHuman(First_Name VARCHAR, Last_Name VARCHAR)
RETURNS TABLE (V VARCHAR)
LANGUAGE JAVASCRIPT
AS '{
processRow: function get_params(row, rowWriter, context){
rowWriter.writeRow({V: "Hello"});
rowWriter.writeRow({V: row.FIRST_NAME}); // Note the capitalization and the use of "row."!
rowWriter.writeRow({V: row.LAST_NAME}); // Note the capitalization and the use of "row."!
}
}';
SELECT V AS Greeting FROM TABLE(HelloHuman('James', 'Kirk'));
Saída:
+------------+
| GREETING |
+============+
| Hello |
+------------+
| James |
+------------+
| Kirk |
+------------+
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:
-- set up for the sample
CREATE TABLE parts (p FLOAT, s STRING);
INSERT INTO parts VALUES (1, 'michael'), (1, 'kelly'), (1, 'brian');
INSERT INTO parts VALUES (2, 'clara'), (2, 'maggie'), (2, 'reagan');
-- creation of the UDTF
CREATE OR REPLACE FUNCTION "CHAR_SUM"(INS STRING)
RETURNS TABLE (NUM FLOAT)
LANGUAGE JAVASCRIPT
AS '{
processRow: function (row, rowWriter, context) {
this.ccount = this.ccount + 1;
this.csum = this.csum + row.INS.length;
rowWriter.writeRow({NUM: row.INS.length});
},
finalize: function (rowWriter, context) {
rowWriter.writeRow({NUM: this.csum});
},
initialize: function(argumentInfo, context) {
this.ccount = 0;
this.csum = 0;
}}';
A consulta a seguir ilustra a chamada da UDTF CHAR_SUM
na tabela parts
sem particionamento:
SELECT * FROM parts, TABLE(char_sum(s));
Saída:
+--------+---------+-----+
| P | S | NUM |
+--------+---------+-----+
| 1 | michael | 7 |
| 1 | kelly | 5 |
| 1 | brian | 5 |
| 2 | clara | 5 |
| 2 | maggie | 6 |
| 2 | reagan | 6 |
| [NULL] | [NULL] | 34 |
+--------+---------+-----+
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:
SELECT * FROM parts, TABLE(char_sum(s) OVER (PARTITION BY p));
Saída:
+--------+---------+-----+
| P | S | NUM |
+--------+---------+-----+
| 1 | michael | 7 |
| 1 | kelly | 5 |
| 1 | brian | 5 |
| 1 | [NULL] | 17 |
| 2 | clara | 5 |
| 2 | maggie | 6 |
| 2 | reagan | 6 |
| 2 | [NULL] | 17 |
+--------+---------+-----+
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
):
CREATE OR REPLACE FUNCTION range_to_values(PREFIX VARCHAR, RANGE_START FLOAT, RANGE_END FLOAT)
RETURNS TABLE (IP_ADDRESS VARCHAR)
LANGUAGE JAVASCRIPT
AS $$
{
processRow: function f(row, rowWriter, context) {
var suffix = row.RANGE_START;
while (suffix <= row.RANGE_END) {
rowWriter.writeRow( {IP_ADDRESS: row.PREFIX + "." + suffix} );
suffix = suffix + 1;
}
}
}
$$;
SELECT * FROM TABLE(range_to_values('192.168.1', 42::FLOAT, 45::FLOAT));
Saída:
+--------------+
| IP_ADDRESS |
+==============+
| 192.168.1.42 |
+--------------+
| 192.168.1.43 |
+--------------+
| 192.168.1.44 |
+--------------+
| 192.168.1.45 |
+--------------+
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:
CREATE TABLE ip_address_ranges(prefix VARCHAR, range_start INTEGER, range_end INTEGER);
INSERT INTO ip_address_ranges (prefix, range_start, range_end) VALUES
('192.168.1', 42, 44),
('192.168.2', 10, 12),
('192.168.2', 40, 40)
;
SELECT rtv.ip_address
FROM ip_address_ranges AS r, TABLE(range_to_values(r.prefix, r.range_start::FLOAT, r.range_end::FLOAT)) AS rtv;
Saída:
+--------------+
| IP_ADDRESS |
+==============+
| 192.168.1.42 |
+--------------+
| 192.168.1.43 |
+--------------+
| 192.168.1.44 |
+--------------+
| 192.168.2.10 |
+--------------+
| 192.168.2.11 |
+--------------+
| 192.168.2.12 |
+--------------+
| 192.168.2.40 |
+--------------+
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:
for input_row in ip_address_ranges: output_row = range_to_values(input_row.prefix, input_row.range_start, input_row.range_end)
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:
-- Example UDTF that "converts" an IPV4 address to a range of IPV6 addresses.
-- (for illustration purposes only and is not intended for actual use)
CREATE OR REPLACE FUNCTION fake_ipv4_to_ipv6(ipv4 VARCHAR)
RETURNS TABLE (IPV6 VARCHAR)
LANGUAGE JAVASCRIPT
AS $$
{
processRow: function f(row, rowWriter, context) {
rowWriter.writeRow( {IPV6: row.IPV4 + "." + "000.000.000.000"} );
rowWriter.writeRow( {IPV6: row.IPV4 + "." + "..."} );
rowWriter.writeRow( {IPV6: row.IPV4 + "." + "FFF.FFF.FFF.FFF"} );
}
}
$$;
SELECT ipv6 FROM TABLE(fake_ipv4_to_ipv6('192.168.3.100'));
Saída:
+-------------------------------+
| IPV6 |
+===============================+
| 192.168.3.100.000.000.000.000 |
+-------------------------------+
| 192.168.3.100.... |
+-------------------------------+
| 192.168.3.100.FFF.FFF.FFF.FFF |
+-------------------------------+
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:
SELECT rtv6.ipv6
FROM ip_address_ranges AS r,
TABLE(range_to_values(r.prefix, r.range_start::FLOAT, r.range_end::FLOAT)) AS rtv,
TABLE(fake_ipv4_to_ipv6(rtv.ip_address)) AS rtv6
WHERE r.prefix = '192.168.2' -- limits the output for this example
;
Saída:
+------------------------------+
| IPV6 |
+==============================+
| 192.168.2.10.000.000.000.000 |
+------------------------------+
| 192.168.2.10.... |
+------------------------------+
| 192.168.2.10.FFF.FFF.FFF.FFF |
+------------------------------+
| 192.168.2.11.000.000.000.000 |
+------------------------------+
| 192.168.2.11.... |
+------------------------------+
| 192.168.2.11.FFF.FFF.FFF.FFF |
+------------------------------+
| 192.168.2.12.000.000.000.000 |
+------------------------------+
| 192.168.2.12.... |
+------------------------------+
| 192.168.2.12.FFF.FFF.FFF.FFF |
+------------------------------+
| 192.168.2.40.000.000.000.000 |
+------------------------------+
| 192.168.2.40.... |
+------------------------------+
| 192.168.2.40.FFF.FFF.FFF.FFF |
+------------------------------+
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:
SELECT rtv6.ipv6
FROM ip_address_ranges AS r,
TABLE(fake_ipv4_to_ipv6(rtv.ip_address)) AS rtv6,
TABLE(range_to_values(r.prefix, r.range_start::FLOAT, r.range_end::FLOAT)) AS rtv
WHERE r.prefix = '192.168.2' -- limits the output for this example
;
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:
-- First, create a small table of IP address owners.
-- This table uses only IPv4 addresses for simplicity.
DROP TABLE ip_address_owners;
CREATE TABLE ip_address_owners (ip_address VARCHAR, owner_name VARCHAR);
INSERT INTO ip_address_owners (ip_address, owner_name) VALUES
('192.168.2.10', 'Barbara Hart'),
('192.168.2.11', 'David Saugus'),
('192.168.2.12', 'Diego King'),
('192.168.2.40', 'Victoria Valencia')
;
-- Now join the IP address owner table to the IPv4 addresses.
SELECT rtv.ip_address, ipo.owner_name
FROM ip_address_ranges AS r,
TABLE(range_to_values(r.prefix, r.range_start::FLOAT, r.range_end::FLOAT)) AS rtv,
ip_address_owners AS ipo
WHERE ipo.ip_address = rtv.ip_address AND
r.prefix = '192.168.2' -- limits the output for this example
;
Saída:
+--------------+-------------------+
| IP_ADDRESS | OWNER_NAME |
+==============+===================+
| 192.168.2.10 | Barbara Hart |
+--------------+-------------------+
| 192.168.2.11 | David Saugus |
+--------------+-------------------+
| 192.168.2.12 | Diego King |
+--------------+-------------------+
| 192.168.2.40 | Victoria Valencia |
+--------------+-------------------+
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.