如何在定界符之后生成所有尾随子字符串?


8

给定一个可能包含定界符的多个实例的字符串,我想生成该字符之后的所有子字符串。

例如,给定一个类似'a.b.c.d.e'(或array {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是的。对不起,延迟(假期等)。我选择了trigram索引答案,因为它实际上最好地解决了我的实际问题。但是,我很敏感地意识到我的问题是专门针对如何以这种方式将字符串分开(出于好奇的缘故),因此我不确定是否已使用最佳指标来选择可接受的答案。
Bo Jeanes

最佳答案应该是回答给定问题的最佳答案。最终,这是您的选择。被选中的人对我来说似乎是有效的候选人。
Erwin Brandstetter

Answers:


3

我认为您不需要在这里单独列出;这是一个XY问题。您只是在尝试进行后缀搜索。有两种优化方法。

将后缀查询转换为前缀查询

基本上,您可以通过反转所有操作来做到这一点。

首先在列的背面创建一个索引:

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

Trigram索引

另一个选项是Trigram索引。如果需要中缀查询(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');

您的书面问题

Trigram索引实际上使用的是您所要求的一般形式。它将字符串分解成几段(三元组),并基于这些建立索引。然后,可以使用索引比顺序扫描更快地搜索匹配项,但是可以搜索中缀,后缀和前缀查询。在可能的情况下,请始终尝试避免重塑他人的发展。

学分

这两种解决方案几乎都是从选择PostgreSQL文本搜索方法开始的。我强烈建议您阅读它,以详细分析PotsgreSQL中可用的文本搜索选项。


评论不作进一步讨论;此对话已转移至聊天
保罗·怀特9

直到圣诞节之后,我才再回到这个问题上,所以为延迟选择答案表示歉意。在我看来,Trigram索引最终是最容易的事情,并且对我帮助最大,尽管它对所提问题的字面回答最少,所以我不确定选择合适答案的SE的政策是什么。无论哪种方式,谢谢大家的帮助。
Bo Jeanes

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

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

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()

实际上,right(str, strpos(str, '.')由于正则表达式函数更昂贵,因此嵌套数次的above()表达式可能会更快(尽管笨拙)。

@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 true用于保留具有NULL值的可能行。

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+中使用
(短数组切片符号除外arr[3:]。我在小提琴中添加了上限,以使其在pg 9.3:中可以使用arr[3:999]。)

SQL提琴。

优化搜索的不同方法

我与@ jpmc26(和您自己)在一起:最好采用完全不同的方法。我喜欢jpmc26 reverse()和和的组合text_pattern_ops

对于部分匹配或模糊匹配,三元组索引会更好。但是,由于您只对整个单词感兴趣,因此全文搜索是另一种选择。我希望索引的大小大大减小,从而获得更好的性能。

pg_trgm以及FTS支持不区分大小写的查询,顺便说一句。

主机名(例如q.x.t.com或)t.com(带内嵌点的单词)被标识为“主机”类型,并被视为一个单词。但是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中文本搜索的新短语搜索功能。发行说明:

可以使用新的运算符<->和在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

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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.