Медленный полнотекстовый поиск из-за крайне неточных оценок строк


10

Полнотекстовые запросы к этой базе данных (хранение билетов RT ( Request Tracker )) выполняются очень долго. Таблица вложений (содержащая полнотекстовые данные) составляет около 15 ГБ.

Схема базы данных выглядит следующим образом, это около 2 миллионов строк:

rt4 = # \ d + вложения
                                                    Таблица "public.attachments"
     Колонка | Тип | Модификаторы | Хранение Описание
----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + -------------
 id | целое число | notval по умолчанию nextval ('attachments_id_seq' :: regclass) | равнина |
 транзакции | целое число | не нуль | равнина |
 родитель | целое число | не нуль по умолчанию 0 | равнина |
 MessageID | изменение характера (160) | | расширенный |
 предмет | изменение характера (255) | | расширенный |
 имя файла | изменение характера (255) | | расширенный |
 тип контента | изменение характера (80) | | расширенный |
 кодирование контента | изменение характера (80) | | расширенный |
 содержание | текст | | расширенный |
 заголовки | текст | | расширенный |
 создатель | целое число | не нуль по умолчанию 0 | равнина |
 создано | отметка времени без часового пояса | | равнина |
 contentindex | цветок | | расширенный |
Индексы:
    "attachments_pkey" ПЕРВИЧНЫЙ КЛЮЧ, btree (id)
    "attachments1" btree (родитель)
    "attachments2" btree (транзакция)
    "attachments3" btree (родитель, транзакция)
    "contentindex_idx" джин (contentindex)
Имеет OIDs: нет

Я могу быстро запросить базу данных (<1 с) с помощью запроса, такого как:

select objectid
from attachments
join transactions on attachments.transactionid = transactions.id
where contentindex @@ to_tsquery('frobnicate');

Однако, когда RT запускает запрос, который должен выполнить поиск по полнотекстовому индексу по той же таблице, обычно требуется сотни секунд. Результат анализа запроса выглядит следующим образом:

запрос

SELECT COUNT(DISTINCT main.id)
FROM Tickets main
JOIN Transactions Transactions_1 ON ( Transactions_1.ObjectType = 'RT::Ticket' )
                                AND ( Transactions_1.ObjectId = main.id )
JOIN Attachments Attachments_2 ON ( Attachments_2.TransactionId = Transactions_1.id )
WHERE (main.Status != 'deleted')
AND ( ( ( Attachments_2.ContentIndex @@ plainto_tsquery('frobnicate') ) ) )
AND (main.Type = 'ticket')
AND (main.EffectiveId = main.id);

EXPLAIN ANALYZE вывод

                                                                             QUERY PLAN 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 Агрегат (стоимость = 51210.60..51210.61 строк = 1 ширина = 4) (фактическое время = 477778.806..477778.806 строк = 1 цикл = 1)
   -> Вложенный цикл (стоимость = 0,00..51210.57 строк = 15 по ширине = 4) (фактическое время = 17943.986..477775.174 строк = 4197 циклов = 1)
         -> Вложенный цикл (стоимость = 0.00..40643.08 строк = 6507 ширина = 8) (фактическое время = 8.526..20610.380 строк = 1714818 циклов = 1)
               -> Seq Scan на основном тикете (стоимость = 0.00..9818.37 строк = 598 ширины = 8) (фактическое время = 0.008..256.042 строк = 96990 циклов = 1)
                     Фильтр: ((((статус) :: текст «удален» :: текст) И (id =ffectiveid) И ((тип) :: текст = «билет» :: текст))
               -> Сканирование индекса с использованием транзакций1 для транзакций транзакций_1 (стоимость = 0,00..51.36 строк = 15 ширины = 8) (фактическое время = 0.102..0.202 строк = 18 циклов = 96990)
                     Индекс Cond: (((тип объекта) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id))
         -> Сканирование индекса с использованием вложений2 на вложениях вложения_2 (стоимость = 0,00..1,61 строк = 1 ширина = 4) (фактическое время = 0,266..0.266 строк = 0 циклов = 1714818)
               Индекс Cond: (транзакция = транзакции_1.id)
               Фильтр: (contentindex @@ plainto_tsquery ('frobnicate' :: text))
 Общее время выполнения: 477778,883 мс

Насколько я могу судить, проблема заключается в том, что он не использует индекс, созданный для contentindexfield ( contentindex_idx), а выполняет фильтр для большого количества совпадающих строк в таблице вложений. Число строк в выходных данных объяснения также представляется крайне неточным, даже после недавнего ANALYZE: оценочные строки = 6507 фактических строк = 1714818.

Я не совсем уверен, куда идти дальше с этим.


Обновление даст дополнительные преимущества. Помимо множества общих улучшений, в частности: 9.2 позволяет сканировать только по индексу и улучшает масштабируемость. Предстоящий 9.4 принесет значительные улучшения для индексов GIN.
Эрвин Брандштеттер

Ответы:


5

Это может быть улучшено тысячей и одним способом, тогда это должно быть делом миллисекунд .

Лучшие запросы

Это всего лишь ваш запрос, переформатированный с использованием псевдонимов и удаленного шума, чтобы очистить туман:

SELECT count(DISTINCT t.id)
FROM   tickets      t
JOIN   transactions tr ON tr.objectid = t.id
JOIN   attachments  a  ON a.transactionid = tr.id
WHERE  t.status <> 'deleted'
AND    t.type = 'ticket'
AND    t.effectiveid = t.id
AND    tr.objecttype = 'RT::Ticket'
AND    a.contentindex @@ plainto_tsquery('frobnicate');

Большая часть проблемы с вашим запросом лежит в первых двух таблицах ticketsи transactions, которые отсутствуют в вопросе. Я заполняюсь образованными догадками.

  • t.status, t.objecttypeИ tr.objecttype, вероятно , не будет text, но enumи , возможно , некоторые очень малое значение ссылки на справочную таблицу.

EXISTS Полусоединение

Предполагая, tickets.idчто это первичный ключ, эта переписанная форма должна быть намного дешевле:

SELECT count(*)
FROM   tickets t
WHERE  status <> 'deleted'
AND    type = 'ticket'
AND    effectiveid = id
AND    EXISTS (
   SELECT 1
   FROM   transactions tr
   JOIN   attachments  a ON a.transactionid = tr.id
   WHERE  tr.objectid = t.id
   AND    tr.objecttype = 'RT::Ticket'
   AND    a.contentindex @@ plainto_tsquery('frobnicate')
   );

Вместо умножения строк на два объединения 1: n, только для свертывания нескольких совпадений в конце count(DISTINCT id), используйте EXISTSполусоединение, которое может перестать искать дальше, как только будет найдено первое совпадение, и в то же время устареет последний DISTINCTшаг. По документации:

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

Эффективность зависит от количества транзакций на тикет и вложений на транзакцию.

Определить порядок соединений с join_collapse_limit

Если вы знаете, что ваш поисковый термин для attachments.contentindexявляется очень избирательным - более избирательным, чем другие условия в запросе (что, вероятно, имеет место для «frobnicate», но не для «проблемы»), вы можете форсировать последовательность соединений. Планировщик запросов едва ли может судить об избирательности отдельных слов, кроме самых распространенных. По документации:

join_collapse_limit( integer)

[...]
Поскольку планировщик запросов не всегда выбирает оптимальный порядок соединения, опытные пользователи могут временно выбрать для этой переменной значение 1, а затем явно указать желаемый порядок соединения.

Используйте SET LOCALс целью установить его только для текущей транзакции.

BEGIN;
SET LOCAL join_collapse_limit = 1;

SELECT count(DISTINCT t.id)
FROM   attachments  a                              -- 1st
JOIN   transactions tr ON tr.id = a.transactionid  -- 2nd
JOIN   tickets      t  ON t.id = tr.objectid       -- 3rd
WHERE  t.status <> 'deleted'
AND    t.type = 'ticket'
AND    t.effectiveid = t.id
AND    tr.objecttype = 'RT::Ticket'
AND    a.contentindex @@ plainto_tsquery('frobnicate');

ROLLBACK; -- or COMMIT;

Порядок WHEREусловий всегда не имеет значения. Здесь важен только порядок соединений.

Или используйте CTE, как объясняет @jjanes в «Варианте 2». для аналогичного эффекта.

Индексы

B-дерево индексов

Возьмите все условия tickets, которые используются одинаково с большинством запросов, и создайте частичный индекс для tickets:

CREATE INDEX tickets_partial_idx
ON tickets(id)
WHERE  status <> 'deleted'
AND    type = 'ticket'
AND    effectiveid = id;

Если одно из условий является переменным, удалите его из WHEREусловия и вместо этого добавьте столбец как столбец индекса.

Еще один на transactions:

CREATE INDEX transactions_partial_idx
ON transactions(objecttype, objectid, id)

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

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

"attachments3" btree (parent, transactionid)

Этот дополнительный индекс является пустой тратой , удалите его:

"attachments1" btree (parent)

Подробности:

Индекс GIN

Добавьте transactionidв свой индекс GIN, чтобы сделать его намного более эффективным. Это может быть еще одна серебряная пуля , потому что она потенциально позволяет сканировать только по индексу, полностью исключая посещение большого стола.
Вам нужны дополнительные классы операторов, предоставляемые дополнительным модулем btree_gin. Подробные инструкции:

"contentindex_idx" gin (transactionid, contentindex)

4 байта из integerстолбца не делают индекс намного больше. Кроме того, к счастью для вас, индексы GIN отличаются от индексов B-дерева в решающем аспекте. По документации:

Многоколонный индекс GIN может использоваться с условиями запроса, которые включают любое подмножество столбцов индекса . В отличие от B-дерева или GiST, эффективность поиска по индексу одинакова независимо от того, какие столбцы индекса используются условиями запроса.

Жирный акцент мой. Так что вам просто нужен один (большой и несколько дорогой) индекс GIN.

Определение таблицы

Переместить integer not null columnsна передний план. Это имеет несколько небольших положительных эффектов на хранение и производительность. В этом случае экономит 4 - 8 байт на строку.

                      Table "public.attachments"
         Column      |            Type             |         Modifiers
    -----------------+-----------------------------+------------------------------
     id              | integer                     | not null default nextval('...
     transactionid   | integer                     | not null
     parent          | integer                     | not null default 0
     creator         | integer                     | not null default 0  -- !
     created         | timestamp                   |                     -- !
     messageid       | character varying(160)      |
     subject         | character varying(255)      |
     filename        | character varying(255)      |
     contenttype     | character varying(80)       |
     contentencoding | character varying(80)       |
     content         | text                        |
     headers         | text                        |
     contentindex    | tsvector                    |

3

Опция 1

Планировщик не имеет представления об истинной природе отношений между EffectiveId и id, и поэтому, вероятно, считает, что предложение:

main.EffectiveId = main.id

будет гораздо более избирательным, чем на самом деле. Если это то, что я думаю, EffectiveID почти всегда равен main.id, но планировщик этого не знает.

Возможно, лучший способ сохранить этот тип отношений, как правило, состоит в том, чтобы определить значение NULL для EffectiveID, чтобы оно означало «фактически то же самое, что и идентификатор», и сохранить в нем что-то, только если есть разница.

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

main.EffectiveId+0 between main.id+0 and main.id+0

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

Вариант 2

Другой подход заключается в использовании CTE:

WITH attach as (
    SELECT * from Attachments 
        where ContentIndex @@ plainto_tsquery('frobnicate') 
)
<rest of query goes here, with 'attach' used in place of 'Attachments'>

Это заставляет планировщика использовать ContentIndex в качестве источника избирательности. Как только это будет сделано, вводящие в заблуждение корреляции столбцов в таблице «Билеты» больше не будут выглядеть так привлекательно. Конечно, если кто-то ищет «проблему», а не «фробникат», это может иметь неприятные последствия.

Вариант 3

Для дальнейшего изучения неверных оценок строк необходимо выполнить приведенный ниже запрос во всех 2 ^ 3 = 8 перестановках различных предложений AND, которые закомментированы. Это поможет выяснить, откуда исходит плохая оценка.

explain analyze
SELECT * FROM Tickets main WHERE 
   main.Status != 'deleted' AND 
   main.Type = 'ticket' AND 
   main.EffectiveId = main.id;
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.