sql >> Databáze >  >> RDS >> PostgreSQL

Proč nepatrná změna hledaného výrazu tak zpomaluje dotaz?

Proč?

Důvod je toto:

Rychlý dotaz:

->  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)) ~~* (...)

Pomalý dotaz:

->  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 (...)

Rozšíření vyhledávacího vzoru o další znak způsobí, že Postgres předpokládá ještě méně zásahů. (Obvykle se jedná o rozumný odhad.) Postgres zjevně nemá dostatečně přesné statistiky (žádné, ve skutečnosti pokračujte ve čtení), abyste očekávali stejný počet zásahů, jaký skutečně získáte.

To způsobí přechod na jiný plán dotazů, který je pro aktuální ještě méně optimální počet zobrazení řádky=1129 .

Řešení

Za předpokladu, že aktuální Postgres 9.5 nebyl deklarován.

Jedním ze způsobů, jak situaci zlepšit, je vytvořit index výrazů na výraz v predikátu. Díky tomu Postgres shromažďuje statistiky pro skutečný výraz, což může pomoci dotazu i když samotný index není pro dotaz použit . Bez indexu nejsou žádné statistiky za výraz vůbec. A pokud se to udělá správně, index lze použít pro dotaz, je to ještě mnohem lepší. Existuje alevíce problémů s vaším aktuálním výrazem:

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

Zvažte tento aktualizovaný dotaz na základě některých předpokladů o vašich nezveřejněných definicích tabulek:

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' || '%');

Hlavní body

Proč f_unaccent() ? Protože unaccent() nelze indexovat. Přečtěte si toto:

Použil jsem tam uvedenou funkci, abych umožnil následující (doporučeno!) vícesloupcový funkční trigram GIN index :

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

Pokud nejste obeznámeni s indexy trigramů, přečtěte si nejprve toto:

A možná:

Ujistěte se, že používáte nejnovější verzi Postgres (aktuálně 9.5). Došlo k podstatným vylepšením indexů GIN. A budou vás zajímat vylepšení v pg_trgm 1.2, jehož vydání je naplánováno na nadcházející Postgres 9.6:

Připravená prohlášení jsou běžným způsobem provádění dotazů s parametry (zejména s textem z uživatelského vstupu). Postgres musí najít plán, který nejlépe funguje pro daný parametr. Přidejte zástupné znaky jako konstanty na hledaný výraz takto:

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

('vicenti' Postgres tedy ví, že máme co do činění se vzorem, který není ukotven ani vlevo, ani vpravo – což by umožňovalo různé strategie. Související odpověď s dalšími podrobnostmi:

Nebo možná přeplánujte dotaz pro každý hledaný výraz (možná pomocí dynamického SQL ve funkci). Ujistěte se však, že čas na plánování nesníží žádné možné zvýšení výkonu.

WHERE podmínka na sloupcích v pess odporuje LEFT JOIN . Postgres je nucen to převést na INNER JOIN . A co je horší, spojení přichází pozdě ve stromu spojení. A protože Postgres nemůže změnit pořadí vašich spojení (viz níže), může se to velmi prodražit. Přesuňte tabulku na první pozici v OD doložka k předčasnému odstranění řádků. Následuje LEFT JOIN s nevylučují žádné řádky podle definice. Ale s takovým množstvím tabulek je důležité přesouvat spojení, která se mohou násobit řádků až do konce.

Připojujete se ke 13 stolům, 12 z nich pomocí LEFT JOIN což ponechává 12! možné kombinace - nebo 11! * 2! pokud vezmeme jeden LEFT JOIN v úvahu, že je to skutečně INNER JOIN . To je taky mnoho pro Postgres k vyhodnocení všech možných permutací pro nejlepší plán dotazů. Přečtěte si o join_collapse_limit :

Výchozí nastavení pro join_collapse_limit je 8 , což znamená, že se Postgres nebude snažit změnit pořadí tabulek ve vašem FROM klauzule a pořadí tabulek je relevantní .

Jedním ze způsobů, jak to obejít, by bylo rozdělit část kritickou z hlediska výkonu na CTE jako @joop přidal komentář . Nenastavujte join_collapse_limit mnohem vyšší nebo časy pro plánování dotazů zahrnující mnoho spojených tabulek se zhorší.

O vašem zřetězeném datu s názvem data :

cast(cast(e.ano jako varchar(4))||'-'||right('0'||cast(e.mes as varchar(2)),2)||' -'|| right('0'||cast(e.dia as varchar(2)),2) as varchar(10)) as data

Za předpokladu sestavujete ze tří číselných sloupců pro rok, měsíc a den, které jsou definovány NOT NULL , použijte místo toho toto:

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

O FM modifikátor vzoru šablony:

Ale ve skutečnosti byste měli datum uložit jako datový typ date pro začátek.

Také zjednodušené:

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

Dotaz to nezrychlí, ale je mnohem čistší. Viz format() .

Nejdříve všechny obvyklé rady pro optimalizaci výkonu platí:

Pokud se vám to všechno podaří, měli byste vidět mnohem rychlejší dotazy pro vše vzory.



  1. pg_config, ruby ​​pg, problém s postgresql 9.0 po upgradu, centos 5

  2. Rozdíl mezi sys.sql_modules, sys.system_sql_modules a sys.all_sql_modules v SQL Server

  3. Migrace databáze Laravel - chyba přejmenování sloupce - Je požadován výčet neznámého typu databáze

  4. mysqli_stmt::bind_param():Počet prvků v řetězci definice typu neodpovídá počtu proměnných vazby