Comment puis-je générer toutes les sous-chaînes de fin à la suite d'un délimiteur?

8

Étant donné une chaîne qui peut contenir plusieurs instances d'un délimiteur, je veux générer toutes les sous-chaînes commençant après ce caractère.

Par exemple, étant donné une chaîne comme 'a.b.c.d.e'(ou un tableau {a,b,c,d,e}, je suppose), je veux générer un tableau comme:

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

L'utilisation prévue est un déclencheur pour remplir une colonne pour une interrogation plus facile des parties de nom de domaine (c'est-à-dire trouver tout q.x.t.compour la requête t.com) chaque fois qu'une autre colonne est écrite.

Cela semble être un moyen gênant de résoudre cela (et cela peut très bien l'être), mais maintenant je suis curieux de savoir comment une fonction comme celle-ci pourrait être écrite en (Postgres ') SQL.

Ce sont des noms de domaine de messagerie, il est donc difficile de dire quel est le nombre maximal d'éléments possibles, mais la grande majorité serait certainement <5.

Bo Jeanes
la source
@ErwinBrandstetter oui. Désolé pour le retard (vacances etc.). J'ai choisi la réponse de l'index de trigramme car elle a en fait résolu mon vrai problème le mieux. Cependant, je suis sensible au fait que ma question portait spécifiquement sur la façon de briser une chaîne de cette manière (par curiosité), donc je ne suis pas sûr d'avoir utilisé la meilleure métrique pour choisir la réponse acceptée.
Bo Jeanes
La meilleure réponse devrait être celle qui répond le mieux à la question donnée. En fin de compte, c'est votre choix. Et l'élu me semble être un candidat valable.
Erwin Brandstetter

Réponses:

3

Je ne pense pas que vous ayez besoin d'une colonne distincte ici; c'est un problème XY. Vous essayez simplement de faire une recherche de suffixe. Il existe deux façons principales d'optimiser cela.

Transformez la requête de suffixe en requête de préfixe

Pour ce faire, vous inversez tout.

Créez d'abord un index au revers de votre colonne:

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

Ensuite, interrogez en utilisant la même chose:

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

Vous pouvez lancer un UPPERappel si vous souhaitez le rendre insensible à la casse:

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

Index des trigrammes

L'autre option est les index trigrammes. Vous devriez certainement l'utiliser si vous avez besoin de requêtes infixes ( LIKE 'something%something'ou LIKE '%something%'tapez des requêtes).

Activez d'abord l'extension d'index de trigramme:

CREATE EXTENSION pg_trgm;

(Cela devrait venir avec PostgreSQL prêt à l'emploi sans aucune installation supplémentaire.)

Créez ensuite un index trigramme sur votre colonne:

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

Sélectionnez ensuite:

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

Encore une fois, vous pouvez ajouter un UPPERpour le rendre insensible à la casse si vous le souhaitez:

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

Votre question telle qu'elle est écrite

Les index de trigrammes fonctionnent en fait en utilisant une forme un peu plus générale de ce que vous demandez sous le capot. Il décompose la chaîne en morceaux (trigrammes) et construit un index basé sur ceux-ci. L'index peut ensuite être utilisé pour rechercher des correspondances beaucoup plus rapidement qu'une analyse séquentielle, mais pour des requêtes d'infixe ainsi que de suffixe et de préfixe. Essayez toujours d'éviter de réinventer ce que quelqu'un d'autre a développé lorsque vous le pouvez.

Crédits

Les deux solutions sont à peu près textuellement issues du choix d'une méthode de recherche de texte PostgreSQL . Je recommande fortement de lui donner une lecture pour une analyse détaillée des options de recherche de texte disponibles dans PotsgreSQL.

jpmc26
la source
Les commentaires ne sont pas pour une discussion approfondie; cette conversation a été déplacée vers le chat .
Paul White 9
Je n'y suis revenu qu'après Noël, donc je m'excuse du retard dans le choix d'une réponse. Les index de trigrammes ont fini par être la chose la plus simple dans mon cas et m'ont le plus aidé, bien que ce soit la réponse la moins littérale à la question posée, donc je ne sais pas quelle est la politique de SE pour choisir les réponses appropriées. Dans tous les cas, merci à tous pour votre aide.
Bo Jeanes
5

Je pense que c'est mon préféré.


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

LIGNES

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

TABLEAUX

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

LIGNES

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

TABLEAUX

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
la source
3

Question posée

Table de test:

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 récursif dans une sous-requête LATÉRALE

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;

Le CROSS JOIN LATERAL( , LATERALpour faire court) est sûr, car le résultat agrégé de la sous-requête renvoie toujours une ligne. Vous obtenez ...

  • ... un tableau avec un élément de chaîne vide pour str = ''dans la table de base
  • ... un tableau avec un élément NULL pour str IS NULLdans la table de base

Enveloppé avec un constructeur de tableau bon marché dans la sous-requête, donc pas d'agrégation dans la requête externe.

Un exemple des fonctionnalités SQL, mais la surcharge rCTE peut empêcher des performances optimales.

Force brute pour un nombre trivial d'éléments

Pour votre cas avec un petit nombre d'éléments , une approche simple sans sous-requête peut être plus rapide:

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

En supposant un maximum de 5 éléments comme vous l'avez commenté. Vous pouvez facilement vous développer pour plus.

Si un domaine donné a moins d'éléments, les substring()expressions en excès renvoient NULL et sont supprimées par array_remove().

En fait, l'expression ci-dessus ( right(str, strpos(str, '.')), imbriquée plusieurs fois peut être plus rapide (bien que difficile à lire) car les fonctions d'expression régulière sont plus chères.

Un fork de la requête de @ Dudu

La requête intelligente de @ Dudu pourrait être améliorée avec 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;

Également utilisé LEFT JOIN LATERAL ... ON truepour conserver les lignes possibles avec des valeurs NULL.

Fonction PL / pgSQL

Logique similaire à celle du rCTE. Sensiblement plus simple et plus rapide que ce que vous avez:

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;

Le OUTparamètre est renvoyé automatiquement à la fin de la fonction.

Il n'est pas nécessaire de l'initialiser result, car NULL::text[] || text 'a' = '{a}'::text[].
Cela ne fonctionne qu'avec 'a'une saisie correcte. NULL::text[] || 'a'(chaîne littérale) déclencherait une erreur car Postgres choisit l' array || arrayopérateur.

strpos()renvoie 0si aucun point n'est trouvé, right()renvoie donc une chaîne vide et la boucle se termine.

C'est probablement la plus rapide de toutes les solutions ici.

Tous fonctionnent dans Postgres 9.3+
(à l'exception de la notation de tranche de tableau court arr[3:]. J'ai ajouté une limite supérieure dans le violon pour le faire fonctionner dans pg 9.3:. arr[3:999])

SQL Fiddle.

Approche différente pour optimiser la recherche

Je suis avec @ jpmc26 (et vous-même): une approche complètement différente sera préférable. J'aime la combinaison de jpmc26 reverse()et de a text_pattern_ops.

Un index de trigrammes serait supérieur pour les correspondances partielles ou floues. Mais comme vous n'êtes intéressé que par des mots entiers , la recherche en texte intégral est une autre option. Je m'attends à une taille d'index beaucoup plus petite et donc à de meilleures performances.

pg_trgm ainsi que les requêtes insensibles à la casse FTS , btw.

Les noms d'hôte comme q.x.t.comou t.com(mots avec des points en ligne) sont identifiés comme de type "hôte" et traités comme un seul mot. Mais il y a aussi la correspondance des préfixes dans FTS (qui semble parfois être négligée). Le manuel:

En outre, *peut être attaché à un lexème pour indiquer le préfixe correspondant:

En utilisant l'idée intelligente de @ jpmc26 avec reverse(), nous pouvons faire en sorte que cela fonctionne:

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

Qui est pris en charge par un index:

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

Notez la 'simple'configuration: nous ne voulons pas que le radical ou le dictionnaire des synonymes soit utilisé avec la 'english'configuration par défaut .

Alternativement (avec une plus grande variété de requêtes possibles), nous pourrions utiliser la nouvelle capacité de recherche d'expression de la recherche de texte dans Postgres 9.6. Les notes de version:

Une requête de recherche de phrase peut être spécifiée dans l'entrée tsquery à l'aide des nouveaux opérateurs <->et . Le premier signifie que les lexèmes avant et après doivent apparaître côte à côte dans cet ordre. Ce dernier signifie qu'ils doivent être exactement séparés les lexèmes.<N>N

Requete:

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

Remplacez dot ( '.') par space ( ' ') pour empêcher l'analyseur de classer «t.com» comme nom d'hôte et utilisez plutôt chaque mot comme lexème distinct.

Et un index correspondant pour l'accompagner:

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

J'ai trouvé quelque chose de semi-réalisable, mais j'aimerais avoir des commentaires sur l'approche. J'ai écrit très peu de PL / pgSQL donc j'ai l'impression que tout ce que je fais est assez hacky et je suis surpris quand ça marche.

Néanmoins, c'est là que j'ai pu:

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;

Cela fonctionne ainsi:

# 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
la source
J'ai ajouté une fonction plpgsql plus simple à ma réponse.
Erwin Brandstetter
1

J'utilise la fonction fenêtre:

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 ;

Résultat:

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
la source
1

Une variante de la solution de @Dudu Markovitz, qui fonctionne également avec des versions de PostgreSQL qui ne reconnaissent pas (encore) [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
la source