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

Perché una leggera modifica nel termine di ricerca rallenta così tanto la query?

Perché?

Il motivo è questo:

Domanda veloce:

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

Domanda lenta:

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

L'estensione del modello di ricerca di un altro personaggio fa sì che Postgres assuma ancora meno risultati. (In genere, questa è una stima ragionevole.) Postgres ovviamente non ha statistiche sufficientemente precise (nessuna, in realtà, continua a leggere) per aspettarsi lo stesso numero di risultati che ottieni davvero.

Ciò provoca il passaggio a un piano di query diverso, che è ancora meno ottimale per il effettivo numero di hit rows=1129 .

Soluzione

Supponendo l'attuale Postgres 9.5 poiché non è stato dichiarato.

Un modo per migliorare la situazione è creare un indice di espressione sull'espressione nel predicato. Ciò fa sì che Postgres raccolga statistiche per l'espressione effettiva, che può aiutare la query anche se l'indice stesso non viene utilizzato per la query . Senza l'indice, non ci sono nessuna statistica per l'espressione a tutti. E se fatto correttamente, l'indice può essere utilizzato per la query, è anche molto meglio. Ma ci sono molti problemi con la tua espressione attuale:

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

Considera questa query aggiornata, basata su alcuni presupposti sulle definizioni delle tabelle non divulgate:

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

Punti principali

Perché f_unaccent() ? Perché unaccent() non può essere indicizzato. Leggi questo:

Ho usato la funzione qui delineata per consentire il seguente (consigliato!) trigramma funzionale multicolonna GIN indice :

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

Se non hai familiarità con gli indici trigram, leggi prima questo:

E possibilmente:

Assicurati di eseguire l'ultima versione di Postgres (attualmente 9.5). Sono stati apportati miglioramenti sostanziali agli indici GIN. E sarai interessato ai miglioramenti in pg_trgm 1.2, programmato per essere rilasciato con il prossimo Postgres 9.6:

Dichiarazioni preparate sono un modo comune per eseguire query con parametri (soprattutto con il testo dell'input dell'utente). Postgres deve trovare un piano che funzioni meglio per ogni dato parametro. Aggiungi caratteri jolly come costanti al termine di ricerca in questo modo:

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

('vicenti' verrebbe sostituito con un parametro.) Quindi Postgres sa che abbiamo a che fare con un modello che non è né ancorato a sinistra né a destra, il che consentirebbe strategie diverse. Risposta correlata con maggiori dettagli:

O forse riorganizzare la query per ogni termine di ricerca (possibilmente utilizzando SQL dinamico in una funzione). Ma assicurati che il tempo di pianificazione non stia consumando alcun possibile guadagno in termini di prestazioni.

Il WHERE condizione sulle colonne in pess contraddice lo LEFT JOIN . Postgres è obbligata a convertirlo in un INNER JOIN . Quel che è peggio, il join arriva in ritardo nell'albero dei join. E poiché Postgres non può riordinare i tuoi join (vedi sotto), può diventare molto costoso. Sposta la tabella al primo posizione nel FROM clausola per eliminare le righe in anticipo. Seguendo LEFT JOIN s non elimina alcuna riga per definizione. Ma con così tante tabelle è importante spostare i join che potrebbero moltiplicarsi righe fino alla fine.

Ti stai unendo a 13 tavoli, 12 dei quali con LEFT JOIN che lascia 12! combinazioni possibili - o 11! * 2! se prendiamo quello LEFT JOIN in considerazione che è davvero un INNER JOIN . È anche molti per Postgres per valutare tutte le possibili permutazioni per il miglior piano di query. Leggi su join_collapse_limit :

L'impostazione predefinita per join_collapse_limit è 8 , il che significa che Postgres non proverà a riordinare le tabelle nel tuo FROM clausola e l'ordine delle tabelle è rilevante .

Un modo per aggirare questo problema sarebbe dividere la parte critica per le prestazioni in un CTE come @joop ha commentato . Non impostare join_collapse_limit molto più alto o i tempi per la pianificazione delle query che coinvolgono molte tabelle unite si deterioreranno.

Informazioni sulla tua data concatenata denominato data :

cast(cast(e.ano as 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

Supponendo costruisci da tre colonne numeriche per anno, mese e giorno, che sono definite NOT NULL , usa invece questo:

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

Informazioni su FM modificatore modello modello:

Ma in realtà, dovresti memorizzare la data come tipo di dati date per cominciare.

Anche semplificato:

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

Non renderà la query più veloce, ma è molto più pulita. Vedi format() .

Per prima cosa, tutti i soliti consigli per l'ottimizzazione delle prestazioni si applica:

Se fai tutto bene, dovresti vedere query molto più veloci per tutti modelli.