Узгодження шаблону з LIKE, SIMILAR TO або регулярними виразами в PostgreSQL


94

Мені довелося написати простий запит, де я шукаю ім’я людей, які починаються з 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може бути корисним індекс, якщо ви дбаєте про ефективність.
Мартін Сміт

Відповіді:


161

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

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також надає цих операторів :

  • % - оператор "подібності"
  • <%(комутатор %>:) - оператор "word_s similarity" в Postgres 9.6 або пізнішої версії
  • <<%(комутатор %>>:) - оператор "строгого___подібності" в Постгресі 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 не підтримує обчислювані стовпці в базових таблицях a la SQL Server, але новий стовпець можна підтримувати за допомогою тригера. Очевидно, що цей новий стовпець буде індексований.

Крім того, індекс на виразі дасть вам те саме, дешевше. Наприклад:

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

Запити, які відповідають виразу в їх умовах, можуть використовувати цей індекс.

Таким чином, звернення до продуктивності приймається при створенні або зміні даних, тому може бути придатним лише для середовища з низькою активністю (тобто набагато менше записів, ніж читання).


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 () виглядає лише на першому символі рядка.


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()прослуховування за допомогою @ Sole021, але це не сумісно з UTF8 (або будь-яким іншим кодуванням з цього приводу), повертаючи просто перший байт, тому його слід застосовувати лише у випадках, коли порівняння суперечить простому 7 -бітові символи ASCII.


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'

1
Не зовсім. column LIKE 'B%'буде більш ефективним, ніж використання функції підрядки в стовпці.
ypercubeᵀᴹ
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.