PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Защо лека промяна в думата за търсене забавя толкова много заявката?

Защо?

Причината е това:

Бърза заявка:

->  Hash Left Join  (cost=1378.60..2467.48 rows=15 width=79) (actual time=41.759..85.037 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* (...)

Бавна заявка:

->  Hash Left Join  (cost=1378.60..2467.48 rows=1 width=79) (actual time=35.084..80.209 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* unacc (...)

Разширяването на шаблона за търсене с друг знак кара Postgres да приема още по-малко попадения. (Обикновено това е разумна оценка.) Postgres очевидно няма достатъчно прецизна статистика (всъщност никаква, продължавайте да четете), за да очаквате същия брой посещения, който наистина получавате.

Това води до превключване към различен план за заявка, който е дори по-малко оптимален за действителните брой посещения rows=1129 .

Решение

Приемаме текущия Postgres 9.5, тъй като не е деклариран.

Един от начините да подобрите ситуацията е да създадете индекс на израза върху израза в предиката. Това кара Postgres да събира статистика за действителния израз, което може да помогне на заявката дори ако самият индекс не се използва за заявката . Без индекса няма няма статистика за израза изобщо. И ако се направи правилно, индексът може да се използва за заявката, това е дори много по-добре. Но има множество проблеми с текущия ви израз:

uaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike uaccent('%vicen%' )

Разгледайте тази актуализирана заявка въз основа на някои предположения относно вашите неразкрити дефиниции на таблици:

SELECT e.id
     , (SELECT count(*) FROM imgitem
        WHERE tabid = e.id AND tab = 'esp') AS imgs -- count(*) is faster
     , e.ano, e.mes, e.dia
     , e.ano::text || to_char(e.mes2, 'FM"-"00')
                   || to_char(e.dia,  'FM"-"00') AS data    
     , pl.pltag, e.inpa, e.det, d.ano anodet
     , format('%s (%s)', p.abrev, p.prenome) AS determinador
     , d.tax
     , coalesce(v.val,v.valf)   || ' ' || vu.unit  AS altura
     , coalesce(v1.val,v1.valf) || ' ' || vu1.unit AS dap
     , d.fam, tf.nome família, d.gen, tg.nome AS gênero, d.sp
     , ts.nome AS espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
FROM      pess    p                        -- reorder!
JOIN      det     d   ON d.detby   = p.id  -- INNER JOIN !
LEFT JOIN tax     tf  ON tf.oldfam = d.fam
LEFT JOIN tax     tg  ON tg.oldgen = d.gen
LEFT JOIN tax     ts  ON ts.oldsp  = d.sp
LEFT JOIN tax     ti  ON ti.oldinf = d.inf  -- unused, see @joop's comment
LEFT JOIN esp     e   ON e.det     = d.id
LEFT JOIN loc     l   ON l.id      = e.loc
LEFT JOIN var     v   ON v.esp     = e.id AND v.key  = 265
LEFT JOIN varunit vu  ON vu.id     = v.unit
LEFT JOIN var     v1  ON v1.esp    = e.id AND v1.key = 264
LEFT JOIN varunit vu1 ON vu1.id    = v1.unit
LEFT JOIN pl          ON pl.id     = e.pl
WHERE f_unaccent(p.abrev)   ILIKE f_unaccent('%' || 'vicenti' || '%') OR
      f_unaccent(p.prenome) ILIKE f_unaccent('%' || 'vicenti' || '%');

Основни точки

Защо f_unaccent() ? Тъй като unaccent() не може да се индексира. Прочетете това:

Използвах функцията, описана там, за да позволя следния (препоръчителен!) многоколонен функционален GIN индекс триграма :

CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);

Ако не сте запознати с триграмните индекси, първо прочетете това:

И евентуално:

Не забравяйте да стартирате най-новата версия на Postgres (в момента 9.5). Има съществени подобрения на GIN индексите. И ще се интересувате от подобрения в pg_trgm 1.2, планиран да бъде пуснат с предстоящия Postgres 9.6:

Подготвени извлечения са често срещан начин за изпълнение на заявки с параметри (особено с текст от потребителско въвеждане). Postgres трябва да намери план, който работи най-добре за даден параметър. Добавете заместващи символи като константи към думата за търсене по този начин:

f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')

('vicenti' ще бъде заменено с параметър.) Така че Postgres знае, че имаме работа с модел, който не е закотвен нито отляво, нито отдясно - което би позволило различни стратегии. Свързан отговор с повече подробности:

Или може би препланирайте заявката за всеки термин за търсене (възможно е да използвате динамичен SQL във функция). Но се уверете, че времето за планиране не пречи на възможното повишаване на производителността.

КЪДЕ условие за колони в pess противоречи на LEFT JOIN . Postgres е принуден да преобразува това в INNER JOIN . Което е по-лошо, присъединяването идва късно в дървото на присъединяването. И тъй като Postgres не може да пренареди вашите съединения (вижте по-долу), това може да стане много скъпо. Преместете масата напървата позиция в ОТ клауза за ранно премахване на редове. След LEFT JOIN s не елиминират никакви редове по дефиниция. Но с толкова много таблици е важно да се преместват съединения, които могат да умножават редове до края.

Вие се присъединявате към 13 маси, 12 от тях с LEFT JOIN което оставя 12! възможни комбинации - или 11! * 2! ако вземем едното LEFT JOIN предвид това наистина е INNER JOIN . Това е също много за Postgres, за да оцени всички възможни пермутации за най-добрия план за заявка. Прочетете за join_collapse_limit :

Настройката по подразбиране за join_collapse_limit е8 , което означава, че Postgres няма да се опита да пренареди таблици във вашия FROM клауза и редът на таблиците е уместен .

Един от начините да се заобиколи това би бил да се раздели критичната за производителността част на CTE като @joop коментира . Не задавайте join_collapse_limit много по-високи или времената за планиране на заявки, включващи много обединени таблици, ще се влошат.

Относно вашата свързана дата с име данни :

cast(cast(e.ano as varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||' -'|| right('0'||cast(e.dia като varchar(2)),2) като varchar(10)) като данни

Приемаме изграждате от три цифрови колони за година, месец и ден, които са дефинирани NOT NULL , използвайте това вместо това:

e.ano::text || to_char(e.mes2, 'FM"-"00')
            || to_char(e.dia,  'FM"-"00') AS data

Относно FM модификатор на модел на шаблон:

Но наистина трябва да съхранявате датата като тип данни date като начало.

Също опростено:

format('%s (%s)', p.abrev, p.prenome) AS determinador

Няма да направи заявката по-бърза, но е много по-чиста. Вижте format() .

Първо, накрая, всички обичайни съвети за оптимизиране на производителността се прилага:

Ако направите всичко това правилно, трябва да видите много по-бързи заявки за всички шаблони.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. PostgreSQL:Показване на таблици в PostgreSQL

  2. Опишете набор от резултати в PostgreSQL?

  3. Извлечете първите N записа на JSON масив с Postgresql заявка

  4. Коя стойност на Postgres трябва да използвам в DATABASE_ENGINE на Django?

  5. Изброяване и превключване на бази данни в PostgreSQL