Повільний пошук в повному обсязі через дико неточні оцінки рядків


10

Повнотекстові запити щодо цієї бази даних (зберігання квитків RT ( Tracker Tracker )), здається, займають дуже багато часу. Таблиця вкладених файлів (що містить дані повного тексту) становить приблизно 15 Гб.

Схема бази даних така, вона становить приблизно 2 мільйони рядків:

rt4 = # \ d + вкладення
                                                    Таблиця "public.attachments"
     Стовпець | Тип | Модифікатори | Зберігання | Опис
----------------- + ----------------------------- + - -------------------------------------------------- ------ + ---------- + -------------
 id | ціле | not null nextval ('attachments_id_seq' :: regclass) | рівнина |
 трансакціонід | ціле | не нульовий | рівнина |
 батьківський | ціле | не нуль за замовчуванням 0 | рівнина |
 messageid | характер варіюється (160) | | розширений |
 предмет | характер варіюється (255) | | розширений |
 ім'я файлу | характер варіюється (255) | | розширений |
 змістовий тип | характер варіюється (80) | | розширений |
 змістовне кодування | характер варіюється (80) | | розширений |
 зміст | текст | | розширений |
 заголовки | текст | | розширений |
 творець | ціле | не нуль за замовчуванням 0 | рівнина |
 створено | позначка часу без часового поясу | | рівнина |
 contentindex | цвектор | | розширений |
Індекси:
    "PRIMARY KEY" "attachments_pkey", btree (id)
    "вкладення1" btree (батьківський)
    "attachments2" btree (трансакціонід)
    "вкладення3" btree (батьківський, трансакціонідний)
    "contentindex_idx" джин (contentindex)
Має OID: ні

Я можу дуже швидко запитати власну базу даних (<1s) із запитом, таким як:

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 вихід

                                                                             ПИТАННЯ ПЛАНУ 
-------------------------------------------------- -------------------------------------------------- -------------------------------------------------- --------------
 Сукупна (вартість = 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)
               -> Повторне сканування на головних квитках (вартість = 0,00..9818,37 рядків = 598 ширина = 8) (фактичний час = 0,008..256,042 рядки = 96990 циклів = 1)
                     Фільтр: (((статус) :: текст "видалено" :: текст) І (id = ефективні) І ((тип) :: текст = "квиток": текст))
               -> Сканування покажчика за допомогою транзакцій1 для транзакцій транзакцій_1 (вартість = 0,00..51,36 рядків = 15 ширина = 8) (фактичний час = 0,102..0,202 рядки = 18 циклів = 96990)
                     Індекс Cond: (((objecttype) :: text = 'RT :: Ticket' :: text) AND (objectid = main.id))
         -> Індексувати сканування за допомогою вкладених файлів2 для вкладених файлів_2 (вартість = 0,00..1,61 рядка = 1 ширина = 4) (фактичний час = 0,266..0,266 рядків = 0 циклів = 1714818)
               Індекс Cond: (transactionid = сделки_1.id)
               Фільтр: (contentindex @@ plainto_tsquery ('frobnicate' :: текст))
 Загальна тривалість виконання: 477778,883 мс

Наскільки я можу сказати, проблема виглядає в тому, що він не використовує індекс, створений у contentindexполі ( 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 як джерело вибірковості. Після того, як це буде змушене це зробити, корегуючі стовпці, що вводять в оману таблицю квитків, вже не будуть виглядати настільки привабливо. Звичайно, якщо хтось шукає "проблему", а не "frobnicate", це може призвести до відмови.

Варіант 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.