Come posso generare tutte le sottostringhe finali seguendo un delimitatore?


8

Data una stringa che può contenere più istanze di un delimitatore, voglio generare tutte le sottostringhe che iniziano dopo quel carattere.

Ad esempio, data una stringa come 'a.b.c.d.e'(o matrice {a,b,c,d,e}, suppongo), voglio generare una matrice come:

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

L'utilizzo previsto è come trigger per riempire una colonna per una più semplice interrogazione delle parti del nome di dominio (ovvero trovare tutto q.x.t.comper la query t.com) ogni volta che viene scritta un'altra colonna.

Sembra un modo imbarazzante per risolvere questo (e potrebbe benissimo essere), ma ora sono curioso di sapere come una funzione come questa possa essere scritta in (Postgres ') SQL.

Questi sono nomi di dominio e-mail, quindi è difficile dire quale sia il numero massimo possibile di elementi, ma certamente la stragrande maggioranza sarebbe <5.


@ErwinBrandstetter sì. Ci scusiamo per il ritardo (festività ecc.). Ho scelto la risposta all'indice del trigramma perché ha risolto il mio vero problema nel migliore dei modi. Tuttavia sono sensibile al fatto che la mia domanda era specificamente su come avrei potuto spezzare una stringa in questo modo (per motivi di curiosità), quindi non sono sicuro di aver usato la metrica migliore per scegliere la risposta accettata.
Bo Jeanes,

La migliore risposta dovrebbe essere quella che risponde meglio alla domanda data. In definitiva, è la tua scelta. E il prescelto sembra un candidato valido per me.
Erwin Brandstetter,

Risposte:


3

Non penso che tu abbia bisogno di una colonna separata qui; questo è un problema XY. Stai solo cercando di fare una ricerca suffisso. Esistono due modi principali per ottimizzarlo.

Trasforma la query suffisso in una query prefisso

Fondamentalmente lo fai invertendo tutto.

Innanzitutto crea un indice sul retro della colonna:

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

Quindi eseguire una query utilizzando lo stesso:

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

Puoi lanciare una UPPERchiamata se vuoi renderla insensibile alle maiuscole:

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

Indici Trigram

L'altra opzione sono gli indici trigram. Dovresti assolutamente usarlo se hai bisogno di query infix ( LIKE 'something%something'o LIKE '%something%'digita query).

Per prima cosa abilita l'estensione dell'indice trigram:

CREATE EXTENSION pg_trgm;

(Questo dovrebbe venire con PostgreSQL fuori dalla scatola senza alcuna installazione aggiuntiva.)

Quindi crea un indice trigramma sulla tua colonna:

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

Quindi basta selezionare:

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

Ancora una volta, puoi inserire un UPPERper renderlo insensibile alle maiuscole se ti piace:

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

La tua domanda come scritta

Gli indici Trigram funzionano effettivamente usando una forma un po 'più generale di ciò che stai chiedendo sotto il cofano. Suddivide la stringa in pezzi (trigrammi) e crea un indice basato su quelli. L'indice può quindi essere utilizzato per cercare corrispondenze molto più rapidamente di una scansione sequenziale, ma per query infix, nonché suffissi e prefissi. Cerca sempre di evitare di reinventare ciò che qualcun altro ha sviluppato quando puoi.

Crediti

Le due soluzioni sono praticamente testimoni della scelta del metodo di ricerca del testo PostgreSQL . Consiglio vivamente di dargli una lettura per un'analisi dettagliata delle opzioni di ricerca di testo disponibili in PotsgreSQL.


I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Paul White 9

Non sono tornato su questo fino a dopo Natale, quindi mi scuso per il ritardo nella scelta di una risposta. Gli indici Trigram sono stati la cosa più semplice nel mio caso e mi hanno aiutato di più, anche se è la risposta meno letterale alla domanda posta, quindi non sono sicuro di quale politica di SE ci sia per scegliere le risposte appropriate. Ad ogni modo, grazie a tutti per il vostro aiuto.
Bo Jeanes,

5

Penso che questo sia il mio preferito.


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

RIGHE

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"}           |
+----+-------------------------------------------+

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

RIGHE

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

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

O

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
;

O

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"}           |
+----+-------------------------------------------+

3

Domanda posta

Tabella di prova:

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 ricorsivo in una sottoquery LATERALE

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;

Il CROSS JOIN LATERAL( , LATERALin breve) è sicuro, perché il risultato aggregato della sottoquery restituisce sempre una riga. Ottieni ...

  • ... un array con un elemento stringa vuoto per str = ''nella tabella di base
  • ... un array con un elemento NULL str IS NULLnella tabella di base

Confezionato con un costruttore di array economico nella sottoquery, quindi nessuna aggregazione nella query esterna.

Un punto di forza delle funzionalità SQL, ma il sovraccarico di rCTE potrebbe impedire le massime prestazioni.

Forza bruta per numero banale di elementi

Per il tuo caso con un numero banalmente piccolo di elementi , un approccio semplice senza subquery potrebbe essere più veloce:

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

Supponendo un massimo di 5 elementi come hai commentato. Puoi espanderti facilmente per ulteriori informazioni.

Se un determinato dominio ha meno elementi, le substring()espressioni in eccesso restituiscono NULL e vengono rimosse da array_remove().

In realtà, l'espressione dall'alto ( right(str, strpos(str, '.')), nidificata più volte potrebbe essere più veloce (anche se scomoda da leggere) poiché le funzioni delle espressioni regolari sono più costose.

Un fork della domanda di @ Dudu

La query intelligente di @ Dudu potrebbe essere migliorata con 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;

Usando anche LEFT JOIN LATERAL ... ON trueper preservare possibili righe con valori NULL.

Funzione PL / pgSQL

Logica simile alla rCTE. Sostanzialmente più semplice e veloce di quello che hai:

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;

Il OUTparametro viene restituito automaticamente alla fine della funzione.

Non è necessario inizializzare result, perché NULL::text[] || text 'a' = '{a}'::text[].
Questo funziona solo con la 'a'corretta digitazione. NULL::text[] || 'a'(string letteral) genererebbe un errore perché Postgres seleziona l' array || arrayoperatore.

strpos()ritorna 0se non viene trovato alcun punto, quindi right()restituisce una stringa vuota e il ciclo termina.

Questa è probabilmente la più veloce di tutte le soluzioni qui.

Funzionano tutti in Postgres 9.3+
(ad eccezione della breve notazione di slice dell'array arr[3:]. Ho aggiunto un limite superiore nel violino per farlo funzionare in pag 9.3:. arr[3:999])

SQL Fiddle.

Approccio diverso per ottimizzare la ricerca

Sono con @ jpmc26 (e te stesso): sarà preferibile un approccio completamente diverso. Mi piace la combinazione di jpmc26 di reverse()e a text_pattern_ops.

Un indice di trigramma sarebbe superiore per le partite parziali o sfocate. Ma poiché sei interessato solo a parole intere , la ricerca full-text è un'altra opzione. Mi aspetto una dimensione dell'indice sostanzialmente inferiore e quindi prestazioni migliori.

pg_trgm e query insensibili al caso di supporto FTS , tra l'altro.

I nomi host come q.x.t.como t.com(parole con punti incorporati) sono identificati come tipo "host" e trattati come una parola. Ma c'è anche la corrispondenza del prefisso in FTS (che a volte sembra essere trascurato). Il manuale:

Inoltre, *può essere collegato a un lessico per specificare la corrispondenza del prefisso:

Usando l'idea intelligente di @ jpmc26 con reverse(), possiamo far funzionare questo:

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

Quale è supportato da un indice:

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

Nota la 'simple'configurazione: non vogliamo che il stemming o il thesaurus vengano usati con la 'english'configurazione predefinita .

In alternativa (con una maggiore varietà di possibili query) potremmo usare la nuova funzionalità di ricerca di frasi della ricerca di testo in Postgres 9.6. Le note di rilascio:

È possibile specificare una query di ricerca di frasi nell'input tsquery utilizzando i nuovi operatori <->e . Il primo significa che i lessemi prima e dopo devono apparire adiacenti l'uno all'altro in quell'ordine. Quest'ultimo significa che devono essere esattamente distanti lexemi.<N>N

Query:

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

Sostituisci punto ( '.') con spazio ( ' ') per impedire al parser di classificare "t.com" come nome host e utilizzare invece ogni parola come lessema separato.

E un indice corrispondente da abbinare:

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

2

Ho pensato a qualcosa di semi-lavorabile, ma mi piacerebbe un feedback sull'approccio. Ho scritto pochissimo PL / pgSQL, quindi sento che tutto ciò che faccio è piuttosto confuso e sono sorpreso quando funziona.

Tuttavia, è qui che sono arrivato a:

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;

Funziona così:

# 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

Ho aggiunto una funzione plpgsql più semplice alla mia risposta.
Erwin Brandstetter,

1

Uso la funzione finestra:

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 ;

Risultato:

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

1

Una variante della soluzione di @Dudu Markovitz, che funziona anche con le versioni di PostgreSQL che non (ancora) riconoscono [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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.