Как я могу сгенерировать все конечные подстроки после разделителя?

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 да. Извините за задержку (праздники и т. Д.). Я выбрал ответ по индексу триграмм, потому что он действительно решил мою реальную проблему лучше всех. Тем не менее, я чувствую тот факт, что мой вопрос был конкретно о том, как я могу разбить строку таким образом (ради любопытства), поэтому я не уверен, использовал ли я лучший показатель, чтобы выбрать приемлемый ответ.
Бо
Лучший ответ должен быть лучшим ответом на данный вопрос. В конечном счете, это ваш выбор. И выбранный мне кажется подходящим кандидатом.
Эрвин Брандштеттер

Ответы:

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

Индексы триграмм

Другой вариант - индексы триграмм. Вы обязательно должны использовать это, если вам нужны инфиксные запросы ( 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');

Ваш вопрос как написано

Индексы триграмм на самом деле работают, используя несколько более общую форму того, что вы просите скрытно. Он разбивает строку на части (триграммы) и строит на их основе индекс. Затем индекс можно использовать для поиска совпадений гораздо быстрее, чем последовательное сканирование, но для запросов с инфиксными, а также суффиксными и префиксными запросами. Всегда старайтесь не изобретать то, что кто-то еще разработал, когда можете.

кредиты

Эти два решения в значительной степени дословны от выбора метода текстового поиска PostgreSQL . Я настоятельно рекомендую прочитать его для подробного анализа доступных вариантов текстового поиска в PotsgreSQL.

jpmc26
источник
Комментарии не для расширенного обсуждения; этот разговор был перенесен в чат .
Пол Уайт 9
Я вернулся к этому только после Рождества, поэтому извиняюсь за задержку в выборе ответа. Индексы триграмм оказались в моем случае самой легкой вещью и помогли мне больше всего, хотя это наименее буквальный ответ на заданный вопрос, поэтому я не уверен, какова политика SE для выбора подходящих ответов. В любом случае, спасибо всем за вашу помощь.
Бо
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"}           |
+----+-------------------------------------------+
Дэвид Markודו Марковиц
источник
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"}           |
+----+-------------------------------------------+
Дэвид Markודו Марковиц
источник
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

Рекурсивный 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 = ''в базовой таблице
  • ... массив с элементом NULL str IS 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().

На самом деле, выражение from ( 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 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 Fiddle.

Другой подход к оптимизации поиска

Я с @ jpmc26 (и вы сами): совершенно другой подход будет предпочтительнее. Мне нравится комбинация jpmc26 reverse()и a 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');

Замените dot ( '.') на space ( ' '), чтобы синтаксический анализатор не классифицировал 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 в свой ответ.
Эрвин Брандштеттер,
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, который также работает с версиями PostgreSQL, которые еще не распознают [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
источник