デリミタに続くすべての後続部分文字列を生成するにはどうすればよいですか?


8

区切り文字の複数のインスタンスを含む可能性がある文字列が与えられた場合、その文字の後に始まるすべての部分文字列を生成したいと思います。

たとえば、次のような文字列'a.b.c.d.e'(または配列{a,b,c,d,e})を指定した場合、次のような配列を生成します。

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

意図された使用法は、別の列が書き込まれるときはいつでも、ドメイン名部分のクエリを容易にするための列を埋める(つまりq.x.t.com、クエリのすべてを見つけるt.com)ためのトリガーとしてです。

これを解決するには厄介な方法のように見えますが(そうなる可能性が非常に高いかもしれません)、このような関数を(Postgresの)SQLでどのように記述することができるか知りたいです。

これらはメールのドメイン名であるため、可能な最大要素数を特定することは困難ですが、大多数は<5です。


@ErwinBrandstetterはい。遅れて申し訳ありません(休日など)。トライグラムインデックスの答えを選んだのは、実際に私の実際の問題を最もよく解決したからです。しかし、私の質問は具体的にどのようにしてこのように文字列を分解できるのか(好奇心のために)であるという事実に敏感なので、受け入れられた回答を選択するために最適なメトリックを使用したかどうかはわかりません。
Bo Jeanes 2017年

最良の答えは、与えられた質問に最もよく答えるものでなければなりません。最終的に、それはあなたの選択です。そして、選ばれたものは私にとって有効な候補者のようです。
Erwin Brandstetter 2017年

回答:


3

ここに別の列は必要ないと思います。これはXY問題です。あなたはサフィックス検索をしようとしているだけです。これを最適化するには、主に2つの方法があります。

サフィックスクエリをプレフィックスクエリに変換する

あなたは基本的にすべてを逆にすることによってこれを行います。

まず、列の反対側にインデックスを作成します。

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

次に、同じものを使用してクエリを実行します。

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

UPPER大文字と小文字を区別しない場合は、呼び出しをスローできます。

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

トライグラムインデックス

もう1つのオプションは、トライグラムインデックスです。インフィックスクエリ(LIKE 'something%something'またはLIKE '%something%'タイプクエリ)が必要な場合は、必ずこれを使用してください。

まず、トライグラムインデックス拡張を有効にします。

CREATE EXTENSION pg_trgm;

(これは追加のインストールなしでPostgreSQLに付属しています。)

次に、列にトライグラムインデックスを作成します。

CREATE INDEX ON yourtable USING GIST(yourcolumn gist_trgm_ops);

次に選択するだけです:

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

繰り返しますが、必要にUPPER応じて、大文字と小文字を区別しないようにスローすることができます。

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

書かれたあなたの質問

トライグラムインデックスは、実際には、内部で求めているもののやや一般的な形式を使用して機能します。文字列を断片(トライグラム)に分解し、それらに基づいてインデックスを作成します。インデックスを使用すると、シーケンシャルスキャンよりもはるかに迅速に一致を検索できますが、接頭辞、接尾辞、接頭辞のクエリも検索できます。できるときは常に、他の誰かが開発したものを再発明しないようにしてください。

クレジット

2つのソリューションは、PostgreSQLのテキスト検索方法の選択とまったく同じです。PotsgreSQLで利用可能なテキスト検索オプションの詳細な分析を読むことを強くお勧めします。


コメントは詳細な議論のためのものではありません。この会話はチャットに移動しました
ポールホワイト9

クリスマスが終わるまでこれに戻ってこなかったので、回答の選択が遅れたことをお詫びします。トライグラムインデックスは私の質問の中で最も簡単なものになり、私に最も役立ちましたが、それは尋ねられた質問に対する文字通りの答えではありません。いずれにせよ、皆様のご協力に感謝いたします。
Bo Jeanes 2017年

5

これは私のお気に入りだと思います。


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

アレイ

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

ROWS

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

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

または

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

アレイ

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
;

または

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

質問する

テストテーブル:

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

LATERALサブクエリでの再帰CTE

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;

CROSS JOIN LATERAL, LATERAL略して)サブクエリの集計結果は常に行を返すため、安全です。あなたが得る...

  • ...空の文字列要素をstr = ''持つベーステーブルの配列
  • ... str IS NULLベーステーブルのNULL要素を持つ配列

サブクエリで安価な配列コンストラクターでラップされているため、外部クエリで集計は行われません。

SQL機能の代表例ですが、rCTEオーバーヘッドにより、最高のパフォーマンスが妨げられる可能性があります。

取るに足らない数の要素に対する総当たり

要素の数がごく少数の場合、サブクエリを使用しない単純なアプローチの方が高速です。

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

あなたがコメントしたような最大5つの要素を想定しています。もっと簡単に拡張できます。

特定のドメインの要素が少ない場合、過剰なsubstring()式はNULLを返し、によって削除されarray_remove()ます。

実際、above(right(str, strpos(str, '.'))からの式は、正規表現関数の方がコストが高いため、数回ネストされている方が(読むのは面倒ですが)高速です。

@Duduのクエリのフォーク

@Duduのスマートクエリは、次のように改善される可能性があります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;

LEFT JOIN LATERAL ... ON trueNULL値を含む可能性のある行を保持するためにも使用します。

PL / pgSQL関数

rCTEと同様のロジック。あなたが持っているものよりも実質的にシンプルで速い:

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;

OUTパラメータは自動的に関数の最後に返されます。

初期化する必要はありませんresult、のでNULL::text[] || text 'a' = '{a}'::text[]
これ'a'は正しく入力された場合にのみ機能します。NULL::text[] || 'a'(文字列リテラル)Postgresがarray || array演算子を選択するため、エラーが発生します。

strpos()0ドットが見つからない場合はを返すためright()、空の文字列を返し、ループが終了します。

これはおそらくここでのすべてのソリューションの中最速です。

それらはすべてPostgres 9.3以降で機能します(短い配列スライス表記を除きます。フィドルに上限を追加して、9.3で機能するようにしています :)
arr[3:]arr[3:999]

SQL Fiddle。

検索を最適化するための異なるアプローチ

私は@ jpmc26(そしてあなた自身)と一緒です:完全に異なるアプローチが望ましいでしょう。jpmc26さんの組み合わせのようなI reverse()text_pattern_ops

トライグラムインデックスは、部分一致またはあいまい一致に対して優れています。ただし、興味があるのは単語全体だけなので、全文検索も選択肢の1つです。インデックスサイズが大幅に小さくなり、パフォーマンスが向上することを期待しています。

pg_trgmとFTSは大文字小文字を区別しないクエリをサポートします。

q.x.t.comまたはt.com(インラインドットのある単語)のようなホスト名は、タイプ「ホスト」として識別され、1つの単語として扱われます。しかし、FTS にはプレフィックスマッチングもあります(見過ごされがちです)。マニュアル:

また、*語彙素に添付して、プレフィックスの一致を指定できます。

@ jpmc26のスマートなアイデアをreverse()で使用すると、これを機能させることができます。

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

これはインデックスでサポートされています。

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

'simple'構成に注意してください。デフォルトの構成でステミングやシソーラスを使用しないでください'english'

別の方法としては(可能なクエリの種類が多ければ多いほど)、Postgres 9.6のテキスト検索の新しいフレーズ検索機能を使用できます。リリースノート:

新しい演算子<->and を使用して、tsquery入力でフレーズ検索クエリを指定できます。前者は、前後の語彙素がこの順序で隣接している必要があることを意味します。後者は、それらが正確に語彙素離れている必要があることを意味します。<N>N

クエリ:

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

ドット('.')をスペース(' ')に置き換えて、パーサーが「t.com」をホスト名として分類しないようにし、代わりに各単語を個別の語彙素として使用します。

そして、それに合わせて一致するインデックス:

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

2

私は半実用的なものを思いつきましたが、そのアプローチについてのフィードバックが大好きです。私はPL / pgSQLをほとんど書いていないので、私がするすべてのことはかなりハックであり、それが機能することに驚いています。

それにもかかわらず、これは私がやったところです:

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;

これは次のように機能します:

# 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

私の回答に、より簡単なplpgsql関数を追加しました。
Erwin Brandstetter 2016

1

私は窓関数を使用します:

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 ;

結果:

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

@Dudu Markovitzによるソリューションの変形で、[i:]を(まだ)認識しないPostgreSQLのバージョンでも機能します。

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)
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.