Wie kann ich alle nachfolgenden Teilzeichenfolgen nach einem Delimeter generieren?


8

Bei einer Zeichenfolge, die mehrere Instanzen eines Trennzeichens enthalten kann, möchte ich alle Teilzeichenfolgen generieren, die nach diesem Zeichen beginnen.

Wenn ich beispielsweise einen String wie 'a.b.c.d.e'(oder ein Array {a,b,c,d,e}, nehme ich an) gebe, möchte ich ein Array wie folgt generieren:

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

Die beabsichtigte Verwendung dient als Auslöser zum Füllen einer Spalte, um das Abfragen von Domainnamen-Teilen zu vereinfachen (dh alle q.x.t.comzur Abfrage zu finden t.com), wenn in eine andere Spalte geschrieben wird.

Es scheint ein unangenehmer Weg zu sein, dies zu lösen (und es kann sehr gut sein), aber jetzt bin ich gespannt, wie eine solche Funktion in (Postgres ') SQL geschrieben werden könnte.

Da es sich um E-Mail-Domain-Namen handelt, ist es schwer zu sagen, wie viele Elemente maximal möglich sind, aber die überwiegende Mehrheit wäre sicherlich <5.


@ErwinBrandstetter ja. Entschuldigung für die Verspätung (Feiertage usw.). Ich habe die Antwort auf den Trigrammindex ausgewählt, weil sie mein eigentliches Problem am besten gelöst hat. Doch ich bin empfindlich auf die Tatsache , dass meine Frage speziell war , wie ich eine Zeichenfolge auf diese Weise auseinanderbrechen könnte (aus Gründen der Neugier) , damit ich bin mir nicht sicher , ob ich die beste Metrik verwendet habe , die akzeptierte Antwort zu wählen.
Bo Jeanes

Die beste Antwort sollte diejenige sein, die die gegebene Frage am besten beantwortet. Letztendlich haben Sie die Wahl. Und der Auserwählte scheint mir ein gültiger Kandidat zu sein.
Erwin Brandstetter

Antworten:


3

Ich glaube nicht, dass Sie hier eine separate Spalte benötigen. Dies ist ein XY-Problem. Sie versuchen nur, eine Suffix-Suche durchzuführen. Es gibt zwei Möglichkeiten, dies zu optimieren.

Verwandeln Sie die Suffix-Abfrage in eine Präfix-Abfrage

Sie tun dies im Grunde, indem Sie alles umkehren.

Erstellen Sie zuerst einen Index auf der Rückseite Ihrer Spalte:

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

Dann fragen Sie mit dem gleichen ab:

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

Sie können einen UPPERAnruf tätigen, wenn Sie die Groß- und Kleinschreibung nicht berücksichtigen möchten:

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

Trigrammindizes

Die andere Option sind Trigrammindizes. Sie sollten dies auf jeden Fall verwenden, wenn Sie Infix-Abfragen benötigen ( LIKE 'something%something'oder LIKE '%something%'Abfragen eingeben).

Aktivieren Sie zuerst die Trigrammindexerweiterung:

CREATE EXTENSION pg_trgm;

(Dies sollte mit PostgreSQL ohne zusätzliche Installation geliefert werden.)

Erstellen Sie dann einen Trigrammindex für Ihre Spalte:

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

Dann wählen Sie einfach:

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

Auch hier können Sie eine einwerfen UPPER, um die Groß- und Kleinschreibung nicht zu berücksichtigen, wenn Sie möchten:

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

Ihre Frage wie geschrieben

Trigram-Indizes funktionieren tatsächlich mit einer etwas allgemeineren Form dessen, was Sie unter der Haube verlangen. Es zerlegt die Zeichenfolge in Stücke (Trigramme) und erstellt darauf basierend einen Index. Der Index kann dann verwendet werden, um viel schneller nach Übereinstimmungen zu suchen als bei einem sequentiellen Scan, jedoch nach Infix- sowie Suffix- und Präfixabfragen. Versuchen Sie immer zu vermeiden, das neu zu erfinden, was jemand anderes entwickelt hat, wenn Sie können.

Credits

Die beiden Lösungen stammen ziemlich wörtlich aus der Auswahl einer PostgreSQL-Textsuchmethode . Ich empfehle dringend, es zu lesen, um eine detaillierte Analyse der verfügbaren Textsuchoptionen in PotsgreSQL zu erhalten.


Kommentare sind nicht für eine ausführliche Diskussion gedacht. Dieses Gespräch wurde in den Chat verschoben .
Paul White 9

Ich bin erst nach Weihnachten darauf zurückgekommen, also entschuldige ich mich für die Verzögerung bei der Auswahl einer Antwort. Trigram-Indizes waren in meinem Fall die einfachste Sache und haben mir am meisten geholfen, obwohl dies die am wenigsten wörtliche Antwort auf die gestellte Frage ist. Daher bin ich mir nicht sicher, welche SE-Richtlinien für die Auswahl geeigneter Antworten gelten. Wie auch immer, ich danke Ihnen allen für Ihre Hilfe.
Bo Jeanes

5

Ich denke das ist mein Favorit.


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

REIHEN

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

REIHEN

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

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

ODER

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
;

ODER

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

Frage gestellt

Testtabelle:

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

Rekursiver CTE in einer LATERAL-Unterabfrage

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;

Das CROSS JOIN LATERAL( , LATERALkurz) ist sicher, da das Gesamtergebnis der Unterabfrage immer eine Zeile zurückgibt. Du erhältst ...

  • ... ein Array mit einem leeren String-Element für str = ''in der Basistabelle
  • ... ein Array mit einem NULL-Element für str IS NULLin der Basistabelle

Eingepackt mit einem billigen Array-Konstruktor in der Unterabfrage, also keine Aggregation in der äußeren Abfrage.

Ein Vorzeigeobjekt für SQL-Funktionen, aber der rCTE-Overhead kann die Spitzenleistung beeinträchtigen.

Brute Force für eine triviale Anzahl von Elementen

Für Ihren Fall mit einer trivial kleinen Anzahl von Elementen kann ein einfacher Ansatz ohne Unterabfrage schneller sein:

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

Angenommen, maximal 5 Elemente, wie Sie kommentiert haben. Sie können leicht für mehr erweitern.

Wenn eine bestimmte Domäne weniger Elemente enthält, geben überschüssige substring()Ausdrücke NULL zurück und werden von entfernt array_remove().

Tatsächlich kann der right(str, strpos(str, '.')mehrmals verschachtelte Ausdruck von oben ( ) schneller sein (obwohl er schwer zu lesen ist), da Funktionen für reguläre Ausdrücke teurer sind.

Eine Abzweigung von @ Dudus Abfrage

@ Dudus intelligente Abfrage könnte verbessert werden mit 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;

Wird auch verwendet LEFT JOIN LATERAL ... ON true, um mögliche Zeilen mit NULL-Werten beizubehalten.

PL / pgSQL-Funktion

Ähnliche Logik wie beim rCTE. Wesentlich einfacher und schneller als das, was Sie haben:

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;

Der OUTParameter wird am Ende der Funktion automatisch zurückgegeben.

Eine Initialisierung ist nicht erforderlich result, da NULL::text[] || text 'a' = '{a}'::text[].
Dies funktioniert nur mit 'a'der richtigen Eingabe. NULL::text[] || 'a'(String-Literal) würde einen Fehler auslösen, da Postgres den array || arrayOperator auswählt .

strpos()Gibt zurück, 0wenn kein Punkt gefunden wurde. right()Gibt also eine leere Zeichenfolge zurück und die Schleife endet.

Dies ist wahrscheinlich die schnellste aller Lösungen hier.

Alle funktionieren in Postgres 9.3+
(mit Ausnahme der kurzen Array-Slice-Notation arr[3:]. Ich habe eine Obergrenze in die Geige eingefügt, damit sie in Seite 9.3 funktioniert : arr[3:999].)

SQL Fiddle.

Anderer Ansatz zur Optimierung der Suche

Ich bin bei @ jpmc26 (und bei Ihnen ): Ein völlig anderer Ansatz ist vorzuziehen. Ich mag die Kombination von jpmc26 reverse()und a text_pattern_ops.

Ein Trigrammindex wäre für Teil- oder Fuzzy-Übereinstimmungen überlegen. Da Sie jedoch nur an ganzen Wörtern interessiert sind , ist die Volltextsuche eine weitere Option. Ich erwarte eine wesentlich kleinere Indexgröße und damit eine bessere Performance.

pg_trgm sowie FTS unterstützen Abfragen, bei denen die Groß- und Kleinschreibung nicht berücksichtigt wird.

Hostnamen wie q.x.t.comoder t.com(Wörter mit Inline-Punkten) werden als Typ "Host" identifiziert und als ein Wort behandelt . Es gibt aber auch Präfix-Matching in FTS (was manchmal übersehen zu werden scheint). Das Handbuch:

Auch *kann auf ein Lexem angebracht werden Präfix Anpassung anzugeben:

Mit der intelligenten Idee von @ jpmc26 reverse()können wir Folgendes erreichen :

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

Welches wird von einem Index unterstützt:

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

Beachten Sie die 'simple'Konfiguration: Wir möchten nicht, dass der Stemming oder Thesaurus mit der Standardkonfiguration 'english'verwendet wird.

Alternativ (mit einer größeren Anzahl möglicher Abfragen) könnten wir die neue Phrasensuchfunktion der Textsuche in Postgres 9.6 verwenden. Die Versionshinweise:

Eine Phrasensuchabfrage kann in tsquery input mit den neuen Operatoren <->und angegeben werden . Ersteres bedeutet, dass die Lexeme davor und danach in dieser Reihenfolge nebeneinander erscheinen müssen. Letzteres bedeutet, dass sie genau Lexeme voneinander entfernt sein müssen.<N>N

Abfrage:

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

Ersetzen Sie dot ( '.') durch space ( ' '), um zu verhindern, dass der Parser 't.com' als Hostnamen klassifiziert, und verwenden Sie stattdessen jedes Wort als separates Lexem.

Und dazu ein passender Index:

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

2

Ich habe mir etwas Semi-Workable ausgedacht, aber ich würde gerne Feedback zu diesem Ansatz erhalten. Ich habe sehr wenig PL / pgSQL geschrieben, daher habe ich das Gefühl, dass alles, was ich tue, ziemlich hackig ist und ich bin überrascht, wenn es funktioniert.

Trotzdem habe ich hier Folgendes erreicht:

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;

Das funktioniert so:

# 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

Ich habe meiner Antwort eine einfachere plpgsql-Funktion hinzugefügt.
Erwin Brandstetter

1

Ich benutze die Fensterfunktion:

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 ;

Ergebnis:

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

Eine Variante der Lösung von @Dudu Markovitz, die auch mit PostgreSQL-Versionen funktioniert, die [i:] (noch) nicht erkennen:

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)
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.