Заданный вопрос
Тестовая таблица:
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, '.', ' ')));