sql >> Database teknologi >  >> RDS >> PostgreSQL

Hvorfor bremser en lille ændring i søgetermen forespørgslen så meget?

Hvorfor?

årsagen er dette:

Hurtig forespørgsel:

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

Langsom forespørgsel:

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

Udvidelse af søgemønsteret med et andet tegn får Postgres til at antage endnu færre hits. (Dette er typisk et rimeligt skøn.) Postgres har åbenbart ikke præcise nok statistikker (ingen, faktisk, fortsæt med at læse) til at forvente det samme antal hits, som du virkelig får.

Dette medfører et skift til en anden forespørgselsplan, som er endnu mindre optimal for den faktiske antal hits rows=1129 .

Løsning

Under den nuværende Postgres 9.5, da den ikke er blevet erklæret.

En måde at forbedre situationen på er at oprette et udtryksindeks på udtrykket i prædikatet. Dette får Postgres til at indsamle statistik for det faktiske udtryk, hvilket kan hjælpe forespørgslen selvom selve indekset ikke bruges til forespørgslen . Uden indekset er der ingen statistik for udtrykket overhovedet. Og hvis det gøres rigtigt, kan indekset bruges til forespørgslen, det er endnu meget bedre. Men der er flere problemer med dit nuværende udtryk:

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

Overvej denne opdaterede forespørgsel baseret på nogle antagelser om dine ikke-oplyste tabeldefinitioner:

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

Vigtige punkter

Hvorfor f_unaccent() ? Fordi unaccent() kan ikke indekseres. Læs dette:

Jeg brugte funktionen skitseret der for at tillade følgende (anbefalet!) flerkolonne funktionelt trigram GIN indeks :

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

Hvis du ikke er bekendt med trigramindekser, så læs dette først:

Og muligvis:

Sørg for at køre den seneste version af Postgres (i øjeblikket 9.5). Der er sket væsentlige forbedringer af GIN-indekser. Og du vil være interesseret i forbedringer i pg_trgm 1.2, der er planlagt til at blive frigivet med den kommende Postgres 9.6:

Forberedte erklæringer er en almindelig måde at udføre forespørgsler med parametre (især med tekst fra brugerinput). Postgres skal finde en plan, der fungerer bedst for en given parameter. Tilføj jokertegn som konstanter til søgeordet som dette:

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

('vicenti' ville blive erstattet med en parameter.) Så Postgres ved, at vi har at gøre med et mønster, der hverken er forankret til venstre eller højre - hvilket ville tillade forskellige strategier. Relateret svar med flere detaljer:

Eller måske omplanlægge forespørgslen for hver søgeterm (evt. ved hjælp af dynamisk SQL i en funktion). Men sørg for at planlægningstiden ikke tærer på nogen mulig præstationsgevinst.

WHERE betingelse på kolonner i pess modsiger LEFT JOIN . Postgres er tvunget til at konvertere det til en INNER JOIN . Hvad værre er sammenføjningen kommer sent i join-træet. Og da Postgres ikke kan genbestille dine joins (se nedenfor), kan det blive meget dyrt. Flyt tabellen til den første position i FROM klausul for at eliminere rækker tidligt. Følger LEFT JOIN s eliminerer ikke nogen rækker pr. definition. Men med så mange tabeller er det vigtigt at flytte joins, der kan multipliceres rækker til slutningen.

Du deltager i 13 borde, 12 af dem med LEFT JOIN som efterlader 12! mulige kombinationer - eller 11! * 2! hvis vi tager den ene LEFT JOIN i betragtning, at det virkelig er en INNER JOIN . Det er også mange for Postgres at evaluere alle mulige permutationer for den bedste forespørgselsplan. Læs om join_collapse_limit :

Standardindstillingen for join_collapse_limit er 8 , hvilket betyder, at Postgres ikke vil forsøge at omarrangere tabeller i din FROM klausul og rækkefølgen af ​​tabeller er relevant .

En måde at løse dette på ville være at opdele den præstationskritiske del i en CTE like @joop kommenterede . Indstil ikke join_collapse_limit meget højere eller tider for forespørgselsplanlægning, der involverer mange sammenføjede tabeller, forringes.

Om din sammenkædede dato navngivet data :

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

Forudsat du bygger ud fra tre numeriske kolonner for år, måned og dag, som er defineret IKKE NULL , brug dette i stedet:

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

Om FM skabelonmønstermodifikator:

Men egentlig bør du gemme datoen som datatype dato til at begynde med.

Også forenklet:

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

Vil ikke gøre forespørgslen hurtigere, men det er meget renere. Se format() .

Først og fremmest, alle de sædvanlige råd til ydelsesoptimering gælder:

Hvis du får alt dette rigtigt, skulle du se meget hurtigere forespørgsler for alle mønstre.



  1. Oprettelse af tabel-/kolonnekombinationer ved hjælp af SQL Query eller Laravel SQL Query Builder

  2. 5 måder at køre et SQL-script fra en fil i SQLite

  3. Hvordan tilføjer jeg indekser til MySQL-tabeller?

  4. Oracle trimspool kun efterfølgende emner (ikke førende emner)