¿Cómo puedo generar todas las subcadenas finales después de un delímetro?

8

Dada una cadena que puede contener varias instancias de un delimitador, quiero generar todas las subcadenas que comiencen después de ese carácter.

Por ejemplo, dada una cadena como 'a.b.c.d.e'(o matriz {a,b,c,d,e}, supongo), quiero generar una matriz como:

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

El uso previsto es como un disparador para llenar una columna para una consulta más fácil de las partes del nombre de dominio (es decir, buscar todo q.x.t.compara consulta t.com) siempre que se escriba en otra columna.

Parece una forma incómoda de resolver esto (y muy bien puede ser), pero ahora tengo curiosidad por saber cómo se podría escribir una función como esta en SQL (Postgres).

Estos son nombres de dominio de correo electrónico, por lo que es difícil decir cuál es el número máximo posible de elementos, pero ciertamente la gran mayoría sería <5.

Bo Jeanes
fuente
@ErwinBrandstetter sí. Perdón por el retraso (vacaciones, etc.). Elegí la respuesta del índice de trigrama porque en realidad resolvió mi problema real de la mejor manera. Sin embargo , soy sensible al hecho de que mi pregunta era específicamente sobre cómo podría separar una cadena de esta manera (por curiosidad), así que no estoy seguro de si he usado la mejor métrica para elegir la respuesta aceptada.
Bo Jeanes
La mejor respuesta debería ser la que mejor responda la pregunta dada. En última instancia, es tu elección. Y el elegido me parece un candidato válido.
Erwin Brandstetter

Respuestas:

3

No creo que necesites una columna separada aquí; Este es un problema XY. Solo estás tratando de hacer una búsqueda de sufijos. Hay dos formas principales de optimizar eso.

Convierta la consulta de sufijo en una consulta de prefijo

Básicamente haces esto invirtiendo todo.

Primero cree un índice en el reverso de su columna:

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

Luego consulta usando lo mismo:

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

Puede UPPERenviar una llamada si desea que no distinga entre mayúsculas y minúsculas:

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

Trigram Indexes

La otra opción son los índices trigram. Definitivamente debe usar esto si necesita consultas infijadas ( LIKE 'something%something'o LIKE '%something%'consultas de tipo).

Primero habilite la extensión del índice de trigram:

CREATE EXTENSION pg_trgm;

(Esto debería venir con PostgreSQL listo para usar sin ninguna instalación adicional).

Luego cree un índice de trigrama en su columna:

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

Luego solo seleccione:

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

Una vez más, puede agregar un UPPERpara que no distinga entre mayúsculas y minúsculas si lo desea:

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

Su pregunta como esta escrita

Los índices Trigram realmente funcionan usando una forma algo más general de lo que estás pidiendo debajo del capó. Rompe la cadena en pedazos (trigramas) y construye un índice basado en ellos. El índice se puede usar para buscar coincidencias mucho más rápidamente que un análisis secuencial, pero para consultas de infijo y sufijo y prefijo. Siempre trate de evitar reinventar lo que alguien más ha desarrollado cuando pueda.

Créditos

Las dos soluciones son prácticamente textuales de Elegir un método de búsqueda de texto PostgreSQL . Recomiendo leerlo para un análisis detallado de las opciones de búsqueda de texto disponibles en PotsgreSQL.

jpmc26
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White 9
No volví a esto hasta después de Navidad, así que disculpa la demora en elegir una respuesta. Los índices Trigram terminaron siendo lo más fácil en mi caso y me ayudaron más, aunque es la respuesta menos literal a la pregunta formulada, por lo que no estoy seguro de cuál es la política de SE para elegir las respuestas adecuadas. De cualquier manera, gracias a todos por su ayuda.
Bo Jeanes
5

Creo que este es mi 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');

FILAS

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

Matrices

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

FILAS

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

Matrices

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"}           |
+----+-------------------------------------------+
David דודו Markovitz
fuente
3

Pregunta formulada

Tabla de prueba:

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 recursivo en una 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;

El CROSS JOIN LATERAL( , LATERALpara abreviar) es seguro, porque el resultado agregado de la subconsulta siempre devuelve una fila. Usted obtiene ...

  • ... una matriz con un elemento de cadena vacío para str = ''en la tabla base
  • ... una matriz con un elemento NULL str IS NULLen la tabla base

Terminado con un constructor de matriz barato en la subconsulta, por lo que no hay agregación en la consulta externa.

Una obra maestra de las características de SQL, pero la sobrecarga de rCTE puede evitar el máximo rendimiento.

Fuerza bruta para un número trivial de elementos.

Para su caso con un número trivialmente pequeño de elementos , un enfoque simple sin subconsulta puede ser más rápido:

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

Suponiendo un máximo de 5 elementos como usted comentó. Puede expandirse fácilmente para obtener más.

Si un dominio dado tiene menos elementos, las substring()expresiones en exceso devuelven NULL y son eliminadas por array_remove().

En realidad, la expresión de arriba ( right(str, strpos(str, '.')), anidada varias veces puede ser más rápida (aunque difícil de leer) ya que las funciones de expresión regular son más caras.

Una bifurcación de la consulta de @ Dudu

La consulta inteligente de @ Dudu podría mejorarse 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;

También se usa LEFT JOIN LATERAL ... ON truepara preservar posibles filas con valores NULL.

Función PL / pgSQL

Lógica similar a la rCTE. Sustancialmente más simple y más rápido que lo que tienes:

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;

El OUTparámetro se devuelve al final de la función automáticamente.

No hay necesidad de inicializar result, porque NULL::text[] || text 'a' = '{a}'::text[].
Esto solo funciona si 'a'se escribe correctamente. NULL::text[] || 'a'(literal de cadena) generaría un error porque Postgres elige el array || arrayoperador.

strpos()devuelve 0si no se encuentra ningún punto, por lo que right()devuelve una cadena vacía y el ciclo termina

Esta es probablemente la más rápida de todas las soluciones aquí.

Todos ellos funcionan en Postgres 9.3+ (a excepción de la notación de corte de matriz corta . Agregué un límite superior en el violín para que funcione en la página 9.3:. )
arr[3:]arr[3:999]

SQL Fiddle.

Enfoque diferente para optimizar la búsqueda

Estoy con @ jpmc26 (y usted mismo): un enfoque completamente diferente será preferible. Me gusta la combinación de jpmc26 reverse()y a text_pattern_ops.

Un índice de trigrama sería superior para coincidencias parciales o difusas. Pero como solo le interesan las palabras completas , la búsqueda de texto completo es otra opción. Espero un tamaño de índice sustancialmente menor y, por lo tanto, un mejor rendimiento.

pg_trgm y FTS admiten consultas que no distinguen entre mayúsculas y minúsculas , por cierto.

Los nombres de host como q.x.t.como t.com(palabras con puntos en línea) se identifican como tipo "host" y se tratan como una sola palabra. Pero también hay coincidencia de prefijos en FTS (que a veces parece pasarse por alto). El manual:

Además, *se puede adjuntar a un lexema para especificar la coincidencia de prefijos:

Usando la idea inteligente de @ jpmc26 con reverse(), podemos hacer que esto funcione:

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

Que es compatible con un índice:

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

Tenga en cuenta la 'simple'configuración: no queremos que se utilicen las derivaciones o tesauros con la 'english'configuración predeterminada .

Alternativamente (con una mayor variedad de consultas posibles) podríamos usar la nueva capacidad de búsqueda de frases de búsqueda de texto en Postgres 9.6. Las notas de lanzamiento:

Se puede especificar una consulta de búsqueda de frase en la entrada tsquery utilizando los nuevos operadores <->y . El primero significa que los lexemas anteriores y posteriores deben aparecer adyacentes entre sí en ese orden. Esto último significa que deben estar exactamente separados por lexemas.<N>N

Consulta:

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

Reemplace dot ( '.') con espacio ( ' ') para evitar que el analizador clasifique 't.com' como nombre de host y, en su lugar, use cada palabra como lexema separado.

Y un índice coincidente para acompañarlo:

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

Se me ocurrió algo semi-viable, pero me encantaría recibir comentarios sobre el enfoque. He escrito muy poco PL / pgSQL, así que siento que todo lo que hago es bastante hacky y me sorprende cuando funciona.

Sin embargo, aquí es donde llegué 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;

Esto funciona así:

# 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
fuente
Agregué una función plpgsql más simple a mi respuesta.
Erwin Brandstetter
1

Yo uso la función de ventana:

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
fuente
1

Una variante de la solución de @Dudu Markovitz, que también funciona con versiones de PostgreSQL que (todavía) no reconocen [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
fuente