Пошук у триграмі стає набагато повільнішим, оскільки рядок пошуку збільшується


16

У базі даних Postgres 9.1 у мене є таблиця table1з ~ 1.5M рядками та стовпцем label(спрощені назви заради цього питання).

Існує функціональний триграм-індекс на lower(unaccent(label))( unaccent()був незмінний, щоб дозволити його використання в індексі).

Наступний запит досить швидкий:

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword%')));
 count 
-------
     1
(1 row)

Time: 394,295 ms

Але наступний запит повільніше:

SELECT count(*) FROM table1
WHERE (lower(unaccent(label)) like lower(unaccent('%someword and some more%')));
 count 
-------
     1
(1 row)

Time: 1405,749 ms

А додавання більше слів ще повільніше, хоча пошук жорсткіший.

Я спробував простий трюк, щоб запустити підзапит на перше слово, а потім на запит з повною рядком пошуку, але (на жаль) планувальник запитів побачив мої махінації:

EXPLAIN ANALYZE
SELECT * FROM (
   SELECT id, title, label from table1
   WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(unaccent(label)) like lower(unaccent('%someword and some more%'));
Сканування бітної карти на таблицю1 (вартість = 16216.01..16220.04 рядків = 1 ширина = 212) (фактичний час = 1824.017..1824.019 рядків = 1 петля = 1)
  Перевірити умову: ((нижня (нецентральна ((мітка): :: текст)) ~~ '% someword%' :: текст) І (нижня (неприйнятна ((мітка): :: текст)) ~~ '% деяке слово та ще деякі % ':: текст))
  -> Сканування індексу растрових зображень на table1_label_hun_gin_trgm (вартість = 0,00..16216.01 рядків = 1 ширина = 0) (фактичний час = 1823.900..1823.900 рядків = 1 петля = 1)
        Індекс Cond: ((нижній (невідповідний ((мітка): :: текст)) ~~ '% someword%' :: текст) AND (нижній (невідповідний ((мітка) :: текст)) ~ ~ '% someword та ще деякі % ':: текст))
Загальна тривалість виконання: 1824,064 мс

Моя остання проблема полягає в тому, що пошуковий рядок надходить із веб-інтерфейсу, який може надсилати досить довгі рядки і, таким чином, бути досить повільним, а також може становити вектор DOS.

Отже, мої запитання:

  • Як пришвидшити запит?
  • Чи є спосіб розбити його на підзапити, щоб це було швидше?
  • Можливо, пізніша версія Postgres краща? (Я спробував 9.4, і здається, не швидше: все-таки такий же ефект. Можливо, пізніша версія?)
  • Можливо, потрібна інша стратегія індексації?

1
Слід зазначити, що unaccent()також надається додатковий модуль, і Postgres не підтримує індекси функції за замовчуванням, оскільки це не так IMMUTABLE. Ви, мабуть, щось змінили, і у своєму запитанні слід згадати, що саме ви зробили. Моя постійна порада: stackoverflow.com/a/11007216/939860 . Крім того, триграмні індекси підтримують невідчутну до регістру відповідність. Ви можете спростити: WHERE f_unaccent(label) ILIKE f_unaccent('%someword%')- з відповідним індексом. Детальніше: stackoverflow.com/a/28636000/939860 .
Erwin Brandstetter

Я просто оголосив unaccentнепорушну. Я додав це до питання.
P.Peter

Майте на увазі, що хак перезаписується під час оновлення unaccentмодуля. Одна з причин, чому я пропоную замість цього функціональну обгортку.
Erwin Brandstetter

Відповіді:


34

У PostgreSQL 9.6 з'явиться нова версія pg_trgm, 1.2, що буде набагато краще з цього приводу. Додавши трохи зусиль, ви також можете змусити цю нову версію працювати під PostgreSQL 9.4 (ви повинні застосувати патч і самостійно скласти модуль розширення та встановити його).

Найстаріша версія - це пошук кожного триграму в запиті та з’єднання їх, а потім застосуйте фільтр. Нова версія зробить, це вибрати найрідкіснішу триграму в запиті та шукати саме цю, а потім фільтрувати решту пізніше.

Машини для цього не існує в 9.1. У 9.4 ця техніка була додана, але pg_trgm тоді не був пристосований, щоб використовувати її.

Ви все ще матимете потенційну проблему DOS, оскільки зловмисник може створити запит, у якого є лише загальні триграми. наприклад, "% і%", або навіть "% a%"


Якщо ви не можете оновити до pg_trgm 1.2, то іншим способом обману планувальника буде:

WHERE (lower(unaccent(label)) like lower(unaccent('%someword%'))) 
AND   (lower(unaccent(label||'')) like 
      lower(unaccent('%someword and some more%')));

Об’єднавши порожній рядок з міткою, ви надумаєте планувальнику думати, що він не може використовувати індекс у тій частині пропозиції де. Таким чином, він використовує індекс лише на% someword%, і застосовує фільтр лише до цих рядків.


Крім того, якщо ви завжди шукаєте цілі слова, ви можете використовувати функцію для токенізації рядка в масив слів і використовувати звичайний вбудований індекс GIN (не pg_trgm) для цієї функції, що повертає масив.


13
Варто згадати, що саме ви написали патч. А попередні тести на працездатність вражають. Це дійсно заслуговує на більшу кількість оновлень (також для пояснень та вирішення поточної версії).
Ервін Брандстеттер

Мене більше зацікавило б хоча б посилання на механізм, який ви використовували для впровадження виправлення, якого не було в 9.1. Але, я погоджуюся з Ервіном поганою відповіддю.
Еван Керролл

3

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

SELECT *
FROM (
   select id, title, label
   from   table1
   where  lower(unaccent(label)) like lower(unaccent('%someword%'))
   ) t1
WHERE lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

EXPLAIN вихід:

Сканування бітної карти на таблиці1 (вартість = 6749.11..7332.71 рядків = 1 ширина = 212) (фактичний час = 256.607..256.609 рядків = 1 петля = 1)
  Перевірити умову: (нижня (без уваги ((label_hun) :: текст)) ~~ '% someword%' :: текст)
  Фільтр: (нижній (нижній (невідповідний ((мітка): :: текст))) ~~ '% деяке слово і ще трохи%' :: текст)
  -> Сканування індексу растрових зображень на table1_label_hun_gin_trgm (вартість = 0,00..6749,11 рядків = 147 ширина = 0) (фактичний час = 256,499..256,499 рядків = 1 петля = 1)
        Індекс Cond: (нижній (невідповідний ((мітка) :: текст)) ~~ '% someword%' :: текст)
Загальний час виконання: 256,653 мс

Отже, оскільки індексу немає lower(lower(unaccent(label))), це створило б послідовне сканування, тому воно перетворюється на простий фільтр. Більше того, простий AND також зробить те саме:

SELECT id, title, label
FROM table1
WHERE lower(unaccent(label)) like lower(unaccent('%someword%'))
AND   lower(lower(unaccent(label))) like lower(unaccent('%someword and more%'))

Звичайно, це евристика, яка може не працювати добре, якщо вирізана частина, яка використовується при скануванні індексів, є дуже поширеною. Але в нашій базі даних насправді не так багато повторень, якщо я використовую близько 10-15 символів.

Залишилося два невеликих питання:

  • Чому не може післяскреси зрозуміти, що щось подібне буде корисним?
  • Що роблять постграми за часовий діапазон 0..256,499 (див. Аналіз результатів)?

1
У часовому діапазоні між 0 і 256.499 він будує растрову карту. На 256.499 він видає свій перший вихід, який є растровою. Що також є його останнім виходом, оскільки він створює лише один вихід - єдиний завершений растровий малюнок.
jjanes
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.