Сопоставление с шаблоном LIKE, SIMILAR TO или регулярными выражениями в PostgreSQL

95

Мне пришлось написать простой запрос, где я иду искать имена людей, которые начинаются с B или D:

SELECT s.name 
FROM spelers s 
WHERE s.name LIKE 'B%' OR s.name LIKE 'D%'
ORDER BY 1

Мне было интересно, есть ли способ переписать это, чтобы стать более производительным. Так что я могу избежать orи / или like?

Лукас Кауфман
источник
Почему вы пытаетесь переписать? Представление? Аккуратность? Является ли s.nameиндексироваться?
Мартин Смит
Я хочу написать для производительности, s.name не индексируется.
Лукас Кауфман
8
Кроме того, если вы ищете без подстановочных знаков и не выбираете никаких дополнительных столбцов, индекс nameможет быть полезен здесь, если вы заботитесь о производительности.
Мартин Смит

Ответы:

162

Ваш запрос в значительной степени оптимальный. Синтаксис не станет намного короче, запрос не станет намного быстрее:

SELECT name
FROM   spelers
WHERE  name LIKE 'B%' OR name LIKE 'D%'
ORDER  BY 1;

Если вы действительно хотите сократить синтаксис , используйте регулярное выражение с ветвями :

...
WHERE  name ~ '^(B|D).*'

Или немного быстрее, с классом персонажа :

...
WHERE  name ~ '^[BD].*'

Быстрый тест без индекса дает более быстрые результаты, чем SIMILAR TOв любом случае для меня.
С соответствующим индексом B-Tree LIKEвыигрывает эту гонку на порядки.

Прочитайте основы сопоставления с образцом в руководстве .

Индекс для превосходной производительности

Если вас интересует производительность, создайте такой индекс для больших таблиц:

CREATE INDEX spelers_name_special_idx ON spelers (name text_pattern_ops);

Делает такой запрос быстрее на порядок. Особые соображения применяются для порядка сортировки, зависящего от локали. Подробнее о классах операторов читайте в руководстве . Если вы используете стандартную локализацию "C" (большинство людей этого не делают), подойдет простой индекс (с классом оператора по умолчанию).

Такой индекс хорош только для левых якорных паттернов (совпадающих с начала строки).

SIMILAR TOили регулярные выражения с базовыми левосторонними выражениями также могут использовать этот индекс. Но не с ветвями (B|D)или классами символов [BD](по крайней мере, в моих тестах на PostgreSQL 9.0).

Для совпадения триграмм или текстового поиска используются специальные индексы GIN или GiST.

Обзор операторов сопоставления с образцом

  • LIKE( ~~) прост и быстр, но ограничен в своих возможностях.
    ILIKE( ~~*) регистронезависимый вариант.
    pg_trgm расширяет поддержку индекса для обоих.

  • ~ (совпадение регулярного выражения) является мощным, но более сложным и может быть медленным для чего-то большего, чем базовые выражения.

  • SIMILAR TOпросто бессмысленно . Своеобразный полукровка LIKEи регулярные выражения. Я никогда не использую это. Смотри ниже.

  • % - это оператор «сходства», предоставляемый дополнительным модулемpg_trgm. Смотри ниже.

  • @@является оператором текстового поиска. Смотри ниже.

pg_trgm - сопоставление триграмм

Начиная с PostgreSQL 9.1, вы можете упростить расширение, pg_trgmчтобы обеспечить поддержку индексов для любого LIKE / ILIKEшаблона (и простых шаблонов регулярных выражений с ~) с использованием индекса GIN или GiST.

Подробности, пример и ссылки:

pg_trgmтакже предоставляет следующие операторы :

  • % - оператор "сходства"
  • <%(commutator %>:) - оператор "word_s Similarity" в Postgres 9.6 или более поздней версии
  • <<%(commutator %>>:) - оператор "strict_word_simility" в Postgres 11 или более поздней версии

Поиск текста

Это специальный тип сопоставления с шаблоном с отдельными типами инфраструктуры и индекса. Он использует словари и основы и является отличным инструментом для поиска слов в документах, особенно для естественных языков.

Сопоставление префиксов также поддерживается:

Так же как поиск фразы начиная с Postgres 9.6:

Рассмотрим введение в руководство и обзор операторов и функций .

Дополнительные инструменты для нечеткого соответствия строк

Дополнительный модуль fuzzystrmatch предлагает еще несколько опций, но производительность, как правило, уступает всем вышеперечисленным.

В частности, различные реализации levenshtein()функции могут быть инструментальными.

Почему регулярные выражения ( ~) всегда быстрее чем SIMILAR TO?

Ответ прост. SIMILAR TOвыражения переписываются внутри регулярных выражений. Таким образом, для каждого SIMILAR TOвыражения есть хотя бы одно более быстрое регулярное выражение (которое позволяет сэкономить на переписывании выражения). Там нет прирост производительности при использовании SIMILAR TO никогда .

А простые выражения, которые можно сделать с помощью LIKE( ~~), в LIKEлюбом случае быстрее .

SIMILAR TOподдерживается только в PostgreSQL, потому что он оказался в ранних версиях стандарта SQL. Они до сих пор не избавились от этого. Но есть планы удалить его и включить вместо него совпадения регулярных выражений - или я так слышал.

EXPLAIN ANALYZEраскрывает это. Попробуйте сами с любым столом!

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name SIMILAR TO 'B%';

раскрывает:

...  
Seq Scan on spelers  (cost= ...  
  Filter: (name ~ '^(?:B.*)$'::text)

SIMILAR TOбыл переписан с помощью регулярного выражения ( ~).

Максимальная производительность в данном конкретном случае

Но EXPLAIN ANALYZEраскрывает больше. Попробуйте, с указанным выше индексом:

EXPLAIN ANALYZE SELECT * FROM spelers WHERE name ~ '^B.*;

раскрывает:

...
 ->  Bitmap Heap Scan on spelers  (cost= ...
       Filter: (name ~ '^B.*'::text)
        ->  Bitmap Index Scan on spelers_name_text_pattern_ops_idx (cost= ...
              Index Cond: ((prod ~>=~ 'B'::text) AND (prod ~<~ 'C'::text))

Внутренне, с индексом , который не зависит от локали известно ( text_pattern_opsили с помощью локали C) простые выражения лево-якорь переписываются с этими операторами текста шаблона: ~>=~, ~<=~, ~>~, ~<~. Это случай ~, ~~или SIMILAR TOкак.

То же самое верно для индексов varcharтипов с varchar_pattern_opsили charс bpchar_pattern_ops.

Итак, применительно к исходному вопросу, это самый быстрый способ :

SELECT name
FROM   spelers  
WHERE  name ~>=~ 'B' AND name ~<~ 'C'
    OR name ~>=~ 'D' AND name ~<~ 'E'
ORDER  BY 1;

Конечно, если вам придется искать соседние инициалы , вы можете упростить дальнейшее:

WHERE  name ~>=~ 'B' AND name ~<~ 'D'   -- strings starting with B or C

Выгода по сравнению с простым использованием ~или ~~крошечная. Если производительность не является вашим главным требованием, вам следует просто придерживаться стандартных операторов - прийти к тому, что у вас уже есть в вопросе.

Эрвин Брандштеттер
источник
У ОП нет индекса по имени, но знаете ли вы, если они это сделали, будет ли в их исходном запросе 2 similarпоиска диапазона и сканирование?
Мартин Смит
2
@MartinSmith: быстрый тест с EXPLAIN ANALYZEпросмотром 2-х растровых индексов. Многократное сканирование индекса растрового изображения может быть объединено довольно быстро.
Эрвин Брандштеттер
Благодарю. Так будет ли какой - либо пробег с заменой ORс UNION ALLили замен name LIKE 'B%'с name >= 'B' AND name <'C'в Postgres?
Мартин Смит
1
@MartinSmith: UNIONне будет, но да, объединение диапазонов в одном WHEREпредложении ускорит запрос. Я добавил больше к своему ответу. Конечно, вы должны принять во внимание ваш язык. Поиск с учетом локали всегда медленнее.
Эрвин Брандштеттер
2
@a_horse_with_no_name: я ожидаю, что нет. Новые возможности pg_tgrm с индексами GIN - удовольствие для общего текстового поиска. Поиск на якоре в начале уже быстрее, чем это.
Эрвин Брандштеттер
11

Как насчет добавления столбца в таблицу. В зависимости от ваших реальных требований:

person_name_start_with_B_or_D (Boolean)

person_name_start_with_char CHAR(1)

person_name_start_with VARCHAR(30)

PostgreSQL не поддерживает вычисляемые столбцы в базовых таблицах в SQL Server, но новый столбец можно поддерживать с помощью триггера. Очевидно, этот новый столбец будет проиндексирован.

Кроме того, индекс по выражению даст вам то же самое, дешевле. Например:

CREATE INDEX spelers_name_initial_idx ON spelers (left(name, 1)); 

Запросы, которые соответствуют выражению в их условиях, могут использовать этот индекс.

Таким образом, снижение производительности происходит при создании или изменении данных, поэтому может подходить только для среды с низкой активностью (т. Е. Гораздо меньше операций записи, чем операций чтения).

onedaywhen
источник
8

Вы могли бы попробовать

SELECT s.name
FROM   spelers s
WHERE  s.name SIMILAR TO '(B|D)%' 
ORDER  BY s.name

Я понятия не имею, могут ли вышеперечисленное или ваше оригинальное выражение саргитировать в Postgres.

При создании предложенного индекса было бы также интересно услышать, как это соотносится с другими вариантами.

SELECT name
FROM   spelers
WHERE  name >= 'B' AND name < 'C'
UNION ALL
SELECT name
FROM   spelers
WHERE  name >= 'D' AND name < 'E'
ORDER  BY name
Мартин Смит
источник
1
Это сработало, и я получил стоимость 1,19, где у меня было 1,25. Спасибо !
Лукас Кауфман
2

То, что я сделал в прошлом, столкнувшись с похожей проблемой производительности, это увеличил ASCII-символ последней буквы и сделал МЕЖДУ. Затем вы получите лучшую производительность для подмножества функциональности LIKE. Конечно, это работает только в определенных ситуациях, но для сверхбольших наборов данных, где вы ищете, например, по имени, производительность снижается от ужасной до приемлемой.

Мел Падден
источник
2

Очень старый вопрос, но я нашел другое быстрое решение этой проблемы:

SELECT s.name 
FROM spelers s 
WHERE ascii(s.name) in (ascii('B'),ascii('D'))
ORDER BY 1

Так как функция ascii () смотрит только на первый символ строки.

Sole021
источник
1
Использует ли это индекс (name)?
ypercubeᵀᴹ
2

Для проверки инициалов я часто использую приведение к "char"(с двойными кавычками). Это не портативный, но очень быстрый. Внутренне он просто удаляет текст и возвращает первый символ, и операции сравнения типа «char» выполняются очень быстро, поскольку тип имеет фиксированную длину в 1 байт:

SELECT s.name 
FROM spelers s 
WHERE s.name::"char" =ANY( ARRAY[ "char" 'B', 'D' ] )
ORDER BY 1

Обратите внимание, что приведение к "char"выполняется быстрее, чем ascii()slution @ Sole021, но оно не совместимо с UTF8 (или любой другой кодировкой в ​​этом отношении), возвращая просто первый байт, поэтому его следует использовать только в тех случаях, когда сравнение выполняется с простым старым 7 -битные символы ASCII.

Ziggy Crueltyfree Zeitgeister
источник
1

Есть два метода, которые еще не упомянуты для решения таких случаев:

  1. частичный (или секционированный - если создается для полного диапазона вручную) индекс - наиболее полезен, когда требуется только подмножество данных (например, во время некоторого обслуживания или временного для некоторых отчетов):

    CREATE INDEX ON spelers WHERE name LIKE 'B%'
  2. разбиение самой таблицы (используя первый символ в качестве ключа разделения) - этот метод особенно стоит рассмотреть в PostgreSQL 10+ (менее болезненное разбиение) и 11+ (сокращение разделов во время выполнения запроса).

Более того, если данные в таблице отсортированы, можно воспользоваться индексом BRIN (по первому символу).

Томаш Пала
источник
-4

Вероятно, быстрее сделать сравнение одного символа:

SUBSTR(s.name,1,1)='B' OR SUBSTR(s.name,1,1)='D'
user2653985
источник
1
На самом деле, нет. column LIKE 'B%'будет более эффективным, чем использование функции подстроки в столбце.
ypercubeᵀᴹ