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) {/*...*/},
}
Copy

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) ...
Copy

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: Se isConst (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>)) ...;
Copy

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>)) ...;
Copy

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 argumento row. Observe que se uma determinada invocação processRow() 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) )
Copy

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 |
+----+----+
Copy

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)) ;
Copy

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));
Copy

Particionamento explícito com uma cláusula OVER vazia

SELECT * FROM tab1, TABLE(js_udtf(tab1.c1, tab1.c2) OVER ());
Copy

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());
Copy

Saída:

+------------+
| OUTPUT_COL |
+============+
| Hello      |
+------------+
| World      |
+------------+
Copy

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'));
Copy

Saída:

+------------+
|  GREETING  |
+============+
| Hello      |
+------------+
| James      |
+------------+
| Kirk       |
+------------+
Copy

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;
    }}';
Copy

A consulta a seguir ilustra a chamada da UDTF CHAR_SUM na tabela parts sem particionamento:

SELECT * FROM parts, TABLE(char_sum(s));
Copy

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  |
+--------+---------+-----+
Copy

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));
Copy

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  |
+--------+---------+-----+
Copy

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));
Copy

Saída:

+--------------+
| IP_ADDRESS   |
+==============+
| 192.168.1.42 |
+--------------+
| 192.168.1.43 |
+--------------+
| 192.168.1.44 |
+--------------+
| 192.168.1.45 |
+--------------+
Copy

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

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 |
+--------------+
Copy

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)
Copy

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'));
Copy

Saída:

+-------------------------------+
| IPV6                          |
+===============================+
| 192.168.3.100.000.000.000.000 |
+-------------------------------+
| 192.168.3.100....             |
+-------------------------------+
| 192.168.3.100.FFF.FFF.FFF.FFF |
+-------------------------------+
Copy

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

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 |
+------------------------------+
Copy

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

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

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 |
+--------------+-------------------+
Copy

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.