Como posso gerar todas as subseqüências à direita seguindo um delímetro?

8

Dada uma sequência que pode conter várias instâncias de um delimitador, desejo gerar todas as substrings iniciadas após esse caractere.

Por exemplo, dada uma sequência como 'a.b.c.d.e'(ou matriz {a,b,c,d,e}, suponho), quero gerar uma matriz como:

{a.b.c.d.e, b.c.d.e, c.d.e, d.e, e}

O uso pretendido é como um gatilho para preencher uma coluna para facilitar a consulta de partes de nomes de domínio (ou seja, encontrar tudo q.x.t.compara consulta t.com) sempre que outra coluna for gravada.

Parece uma maneira incômoda de resolver isso (e pode muito bem ser), mas agora estou curioso para saber como uma função como essa pode ser escrita no SQL (do Postgres).

Como são nomes de domínio de email, é difícil dizer qual é o número máximo possível de elementos, mas certamente a grande maioria seria <5.

Bo Jeanes
fonte
@ErwinBrandstetter yes. Desculpe pelo atraso (feriados etc.). Eu escolhi a resposta do índice trigrama porque ela realmente resolveu o meu problema real da melhor maneira. No entanto , sou sensível ao fato de que minha pergunta foi especificamente sobre como eu poderia separar uma string dessa maneira (por curiosidade), então não tenho certeza se usei a melhor métrica para escolher a resposta aceita.
precisa saber é
A melhor resposta deve ser a melhor para responder à pergunta dada. Em última análise, a escolha é sua. E o escolhido parece um candidato válido para mim.
Erwin Brandstetter

Respostas:

3

Eu não acho que você precise de uma coluna separada aqui; este é um problema XY. Você está apenas tentando fazer uma pesquisa por sufixo. Existem duas maneiras principais de otimizar isso.

Transforme a consulta de sufixo em uma consulta de prefixo

Você basicamente faz isso revertendo tudo.

Primeiro, crie um índice no verso da sua coluna:

CREATE INDEX ON yourtable (reverse(yourcolumn) text_pattern_ops);

Em seguida, consulte usando o mesmo:

SELECT * FROM yourtable WHERE reverse(yourcolumn) LIKE reverse('%t.com');

Você pode fazer uma UPPERchamada se quiser torná-la sem distinção entre maiúsculas e minúsculas:

CREATE INDEX ON yourtable (reverse(UPPER(yourcolumn)) text_pattern_ops);
SELECT * FROM yourtable WHERE reverse(UPPER(yourcolumn)) LIKE reverse(UPPER('%t.com'));

Índices de Trigrama

A outra opção é índices de trigrama. Você definitivamente deve usar isso se precisar de consultas infix ( LIKE 'something%something'ou LIKE '%something%'consultas de tipo).

Primeiro, ative a extensão do índice trigrama:

CREATE EXTENSION pg_trgm;

(Isso deve vir com o PostgreSQL pronto para uso, sem nenhuma instalação extra.)

Em seguida, crie um índice trigrama na sua coluna:

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

Depois, selecione:

SELECT * FROM yourtable WHERE yourcolumn LIKE '%t.com';

Novamente, você pode inserir um UPPERpara torná-lo sem distinção entre maiúsculas e minúsculas, se desejar:

CREATE INDEX ON yourtable USING GIST(UPPER(yourcolumn) gist_trgm_ops);
SELECT * FROM yourtable WHERE UPPER(yourcolumn) LIKE UPPER('%t.com');

Sua pergunta como está escrita

Os índices de trigrama realmente funcionam usando uma forma um pouco mais geral do que você está pedindo sob o capô. Ele divide a cadeia em pedaços (trigramas) e cria um índice com base neles. O índice pode então ser usado para procurar correspondências muito mais rapidamente do que uma varredura seqüencial, mas para consultas de infixo, bem como sufixos e prefixos. Sempre tente evitar reinventar o que outra pessoa desenvolveu quando puder.

Créditos

As duas soluções são praticamente verbais na escolha de um método de pesquisa de texto do PostgreSQL . Eu recomendo ler para uma análise detalhada das opções de pesquisa de texto disponíveis no PotsgreSQL.

jpmc26
fonte
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Paul White 9
Não voltei a isso até depois do Natal, então peço desculpas pelo atraso na escolha de uma resposta. Os índices de trigrama acabaram sendo a coisa mais fácil no meu caso e me ajudaram ao máximo, embora seja a resposta menos literal à pergunta, por isso não tenho certeza de qual é a política da SE para escolher respostas adequadas. De qualquer forma, obrigado a todos por sua ajuda.
precisa saber é
5

Eu acho que esse é o meu favorito.


create table t (id int,str varchar(100));
insert into t (id,str) values (1,'a.b.c.d.e'),(2,'xxx.yyy.zzz');

ROWS

select      id
           ,array_to_string((string_to_array(str,'.'))[i:],'.')

from        t,unnest(string_to_array(str,'.')) with ordinality u(token,i)
;

+----+-----------------+
| id | array_to_string |
+----+-----------------+
|  1 | a.b.c.d.e       |
|  1 | b.c.d.e         |
|  1 | c.d.e           |
|  1 | d.e             |
|  1 | e               |
|  2 | xxx.yyy.zzz     |
|  2 | yyy.zzz         |
|  2 | zzz             |
+----+-----------------+

ARRAYS

select      id
           ,array_agg(array_to_string((string_to_array(str,'.'))[i:],'.'))

from        t,unnest(string_to_array(str,'.')) with ordinality u(token,i)

group by    id
;

+----+-------------------------------------------+
| id |                 array_agg                 |
+----+-------------------------------------------+
|  1 | {"a.b.c.d.e","b.c.d.e","c.d.e","d.e","e"} |
|  2 | {"xxx.yyy.zzz","yyy.zzz","zzz"}           |
+----+-------------------------------------------+
David Markovitz
fonte
4
create table t (id int,str varchar(100));
insert into t (id,str) values (1,'a.b.c.d.e'),(2,'xxx.yyy.zzz');

ROWS

select  id
       ,regexp_replace(str,'^([^\.]+\.?){' || gs.i || '}','') as suffix

from    t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)
;

OU

select  id
       ,substring(str from '(([^.]*?\.?){' || gs.i+1 || '})$') as suffix

from    t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)
;

+----+-------------+
| id | suffix      |
+----+-------------+
| 1  | a.b.c.d.e   |
+----+-------------+
| 1  | b.c.d.e     |
+----+-------------+
| 1  | c.d.e       |
+----+-------------+
| 1  | d.e         |
+----+-------------+
| 1  | e           |
+----+-------------+
| 2  | xxx.yyy.zzz |
+----+-------------+
| 2  | yyy.zzz     |
+----+-------------+
| 2  | zzz         |
+----+-------------+

ARRAYS

select      id
           ,array_agg(regexp_replace(str,'^([^\.]+\.?){' || gs.i || '}','')) as suffixes

from        t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)

group by    id
;

OU

select      id
           ,array_agg(substring(str from '(([^.]*?\.?){' || gs.i+1 || '})$')) as suffixes

from        t,generate_series(0,cardinality(string_to_array(str,'.'))-1) gs(i)

group by    id
;

+----+-------------------------------------------+
| id |                 suffixes                  |
+----+-------------------------------------------+
|  1 | {"a.b.c.d.e","b.c.d.e","c.d.e","d.e","e"} |
|  2 | {"xxx.yyy.zzz","yyy.zzz","zzz"}           |
+----+-------------------------------------------+
David Markovitz
fonte
3

Pergunta feita

Tabela de teste:

CREATE TABLE tbl (id int, str text);
INSERT INTO tbl VALUES
  (1, 'a.b.c.d.e')
, (2, 'x1.yy2.zzz3')     -- different number & length of elements for testing
, (3, '')                -- empty string
, (4, NULL);             -- NULL

CTE recursiva em uma subconsulta LATERAL

SELECT *
FROM   tbl, LATERAL (
   WITH RECURSIVE cte AS (
      SELECT str
      UNION ALL
      SELECT right(str, strpos(str, '.') * -1)  -- trim leading name
      FROM   cte
      WHERE  str LIKE '%.%'  -- stop after last dot removed
      )
   SELECT ARRAY(TABLE cte) AS result
   ) r;

O CROSS JOIN LATERAL( , LATERALabreviado) é seguro, porque o resultado agregado da subconsulta sempre retorna uma linha. Você consegue ...

  • ... uma matriz com um elemento de sequência vazio para str = ''na tabela base
  • ... uma matriz com um elemento NULL para str IS NULLna tabela base

Embrulhado com um construtor de matriz barato na subconsulta, portanto, sem agregação na consulta externa.

Uma demonstração dos recursos SQL, mas a sobrecarga do rCTE pode impedir o desempenho superior.

Força bruta para número trivial de elementos

Para o seu caso com um número trivialmente pequeno de elementos , uma abordagem simples sem subconsulta pode ser mais rápida:

SELECT id, array_remove(ARRAY[substring(str, '(?:[^.]+\.){4}[^.]+$')
                            , substring(str, '(?:[^.]+\.){3}[^.]+$')
                            , substring(str, '(?:[^.]+\.){2}[^.]+$')
                            , substring(str,        '[^.]+\.[^.]+$')
                            , substring(str,               '[^.]+$')], NULL)
FROM   tbl;

Supondo um máximo de 5 elementos como você comentou. Você pode facilmente expandir para mais.

Se um determinado domínio tiver menos elementos, substring()expressões em excesso retornarão NULL e serão removidas por array_remove().

Na verdade, a expressão acima ( right(str, strpos(str, '.')), aninhada várias vezes, pode ser mais rápida (embora de difícil leitura), pois as funções de expressão regular são mais caras.

Uma bifurcação da consulta de @ Dudu

A consulta inteligente do @ Dudu pode ser melhorada com generate_subscripts():

SELECT id, array_agg(array_to_string(arr[i:], '.')) AS result
FROM  (SELECT id, string_to_array(str,'.') AS arr FROM tbl) t
LEFT   JOIN LATERAL generate_subscripts(arr, 1) i ON true
GROUP  BY id;

Também usando LEFT JOIN LATERAL ... ON truepara preservar possíveis linhas com valores NULL.

Função PL / pgSQL

Lógica semelhante à do rCTE. Substancialmente mais simples e mais rápido do que você tem:

CREATE OR REPLACE FUNCTION string_part_seq(input text, OUT result text[]) AS
$func$
BEGIN
   LOOP
      result := result || input;  -- text[] || text array concatenation
      input  := right(input, strpos(input, '.') * -1);
      EXIT WHEN input = '';
   END LOOP;
END
$func$  LANGUAGE plpgsql IMMUTABLE STRICT;

O OUTparâmetro é retornado no final da função automaticamente.

Não há necessidade de inicializar result, porque NULL::text[] || text 'a' = '{a}'::text[].
Isso funciona apenas com a 'a'digitação correta. NULL::text[] || 'a'(string literal) geraria um erro porque o Postgres escolhe o array || arrayoperador.

strpos()retorna 0se nenhum ponto for encontrado, então right()retorna uma string vazia e o loop termina.

Esta é provavelmente a mais rápida de todas as soluções aqui.

Todos eles funcionam no Postgres 9.3+ (exceto pela notação de fatia de matriz curta . Adicionei um limite superior no violino para fazê-lo funcionar na página 9.3:. )
arr[3:]arr[3:999]

SQL Fiddle.

Abordagem diferente para otimizar a pesquisa

Estou com @ jpmc26 (e com você): uma abordagem completamente diferente será preferível. Eu gosto da combinação de jpmc26 de reverse()e a text_pattern_ops.

Um índice de trigrama seria superior para correspondências parciais ou difusas. Mas como você só está interessado em palavras inteiras , a Pesquisa de texto completo é outra opção. Espero um tamanho de índice substancialmente menor e, portanto, melhor desempenho.

O pg_trgm e o FTS oferecem suporte a consultas sem distinção entre maiúsculas e minúsculas , btw.

Nomes de host como q.x.t.comou t.com(palavras com pontos embutidos) são identificados como tipo "host" e tratados como uma palavra. Mas também há correspondência de prefixo no STF (que às vezes parece ser esquecido). O manual:

Além disso, *pode ser anexado a um lexeme para especificar a correspondência de prefixo:

Usando a idéia inteligente do @ jpmc26 reverse(), podemos fazer este trabalho:

SELECT *
FROM   tbl
WHERE  to_tsvector('simple', reverse(str))
    @@ to_tsquery ('simple', reverse('c.d.e') || ':*');
-- or with reversed prefix:  reverse('*:c.d.e')

O que é suportado por um índice:

CREATE INDEX tbl_host_idx ON tbl USING GIN (to_tsvector('simple', reverse(str)));

Observe a 'simple'configuração: não queremos que o stemming ou o thesaurus sejam usados ​​com a 'english'configuração padrão .

Como alternativa (com uma variedade maior de consultas possíveis), poderíamos usar o novo recurso de pesquisa de frase da pesquisa de texto no Postgres 9.6. As notas de versão:

Uma consulta de pesquisa de frase pode ser especificada na entrada tsquery usando os novos operadores <->e . O primeiro significa que os lexemas antes e depois devem aparecer adjacentes um ao outro nessa ordem. O último significa que eles devem estar exatamente com lexemes separados.<N>N

Inquerir:

SELECT *
FROM   tbl
WHERE  to_tsvector     ('simple', replace(str, '.', ' '))
    @@ phraseto_tsquery('simple', 'c d e');

Substitua dot ( '.') por space ( ' ') para impedir que o analisador classifique 't.com' como nome do host e, em vez disso, use cada palavra como léxico máximo.

E um índice correspondente para acompanhar:

CREATE INDEX tbl_phrase_idx ON tbl USING GIN (to_tsvector('simple', replace(str, '.', ' ')));
Erwin Brandstetter
fonte
2

Eu criei algo semi-viável, mas eu adoraria comentários sobre a abordagem. Eu escrevi muito pouco PL / pgSQL, então sinto que tudo o que faço é bastante hacky e fico surpreso quando funciona.

No entanto, é aqui que eu cheguei:

CREATE OR REPLACE FUNCTION string_part_sequences(input text, separator text)
RETURNS text[]
LANGUAGE plpgsql
AS $$
  DECLARE
    parts text[] := string_to_array(input, separator);
    result text[] := '{}';
    i int;
  BEGIN
    FOR i IN SELECT generate_subscripts(parts, 1) - 1
    LOOP
      SELECT array_append(result, (
          SELECT array_to_string(array_agg(x), separator)
          FROM (
            SELECT *
            FROM unnest(parts)
            OFFSET i
          ) p(x)
        )
      )
      INTO result;
    END LOOP;
    RETURN result;
  END;
$$
STRICT IMMUTABLE;

Isso funciona assim:

# SELECT string_part_sequences('mymail.unisa.edu.au', '.');
┌──────────────────────────────────────────────┐
            string_part_sequences             
├──────────────────────────────────────────────┤
 {mymail.unisa.edu.au,unisa.edu.au,edu.au,au} 
└──────────────────────────────────────────────┘
(1 row)

Time: 1.168 ms
Bo Jeanes
fonte
Eu adicionei uma função mais simples do plpgsql à minha resposta.
Erwin Brandstetter
1

Eu uso a função de janela:

with t1 as (select regexp_split_to_table('ab.ac.xy.yx.md','\.') as str),
     t2 as (select string_agg(str,'.') over ( rows between current row and unbounded following) as str from t1 ),
     t3 as (select array_agg(str) from t2)
     select * from t3 ;

Resultado:

postgres=# with t1 as (select regexp_split_to_table('ab.ac.xy.yx.md','\.') as str),
postgres-#      t2 as (select string_agg(str,'.') over ( rows between current row and unbounded following) as str from t1 ),
postgres-#      t3 as (select array_agg(str) from t2)
postgres-#      select * from t3 ;
                   array_agg
------------------------------------------------
 {ab.ac.xy.yx.md,ac.xy.yx.md,xy.yx.md,yx.md,md}
(1 row)

Time: 0.422 ms
postgres=# with t1 as (select regexp_split_to_table('mymail.unisa.edu.au','\.') as str),
postgres-#      t2 as (select string_agg(str,'.') over ( rows between current row and unbounded following) as str from t1 ),
postgres-#      t3 as (select array_agg(str) from t2)
postgres-#      select * from t3 ;
                  array_agg
----------------------------------------------
 {mymail.unisa.edu.au,unisa.edu.au,edu.au,au}
(1 row)

Time: 0.328 ms
Luan Huynh
fonte
1

Uma variante da solução de @Dudu Markovitz, que também funciona com versões do PostgreSQL que ainda não reconhecem [i:]:

create table t (id int,str varchar(100));
insert into t (id,str) values (1,'a.b.c.d.e'),(2,'xxx.yyy.zzz');

SELECT    
    id, array_to_string(the_array[i:upper_bound], '.')
FROM     
    (
    SELECT
        id, 
        string_to_array(str, '.') the_array, 
        array_upper(string_to_array(str, '.'), 1) AS upper_bound
    FROM
        t
    ) AS s0, 
    generate_series(1, upper_bound) AS s1(i)
joanolo
fonte