Waarom?
De reden is dit:
Snelle vraag:
-> 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)) ~~* (...)
Langzame zoekopdracht:
-> 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 (...)
Door het zoekpatroon uit te breiden met een ander karakter neemt Postgres nog minder treffers aan. (Normaal gesproken is dit een redelijke schatting.) Postgres heeft duidelijk niet nauwkeurig genoeg statistieken (geen enkele, blijf lezen) om hetzelfde aantal hits te verwachten als u werkelijk krijgt.
Dit veroorzaakt een overstap naar een ander zoekplan, dat nog minder optimaal is voor de werkelijke aantal hits rows=1129
.
Oplossing
Ervan uitgaande dat de huidige Postgres 9.5 niet is gedeclareerd.
Een manier om de situatie te verbeteren is het maken van een expressie-index op de uitdrukking in het predikaat. Dit zorgt ervoor dat Postgres statistieken verzamelt voor de eigenlijke expressie, wat de zoekopdracht kan helpen zelfs als de index zelf niet wordt gebruikt voor de zoekopdracht . Zonder de index zijn er geen statistieken helemaal voor de uitdrukking. En als het goed wordt gedaan, kan de index worden gebruikt voor de query, dat is nog veel beter. Maar er zijn meerdere problemen met je huidige uitdrukking:
unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')
Overweeg deze bijgewerkte zoekopdracht, gebaseerd op enkele aannames over uw niet-openbaar gemaakte tabeldefinities:
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' || '%');
Belangrijkste punten
Waarom f_unaccent()
? Omdat unaccent()
kan niet worden geïndexeerd. Lees dit:
Ik heb de daar beschreven functie gebruikt om het volgende (aanbevolen!) functionele trigram met meerdere kolommen GIN index toe te staan :
CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);
Als u niet bekend bent met trigram-indexen, lees dan eerst dit:
En mogelijk:
Zorg ervoor dat u de nieuwste versie van Postgres gebruikt (momenteel 9.5). Er zijn aanzienlijke verbeteringen aangebracht in de GIN-indexen. En u zult geïnteresseerd zijn in verbeteringen in pg_trgm 1.2, gepland voor release met de komende Postgres 9.6:
Voorbereide verklaringen zijn een gebruikelijke manier om query's met parameters uit te voeren (vooral met tekst van gebruikersinvoer). Postgres moet een plan vinden dat het beste werkt voor een bepaalde parameter. Voeg jokertekens toe als constanten naar de naar de zoekterm als volgt:
f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')
('vicenti'
zou worden vervangen door een parameter.) Dus Postgres weet dat we te maken hebben met een patroon dat noch links noch rechts verankerd is - wat verschillende strategieën mogelijk zou maken. Gerelateerd antwoord met meer details:
Of misschien de query opnieuw plannen voor elke zoekterm (eventueel met behulp van dynamische SQL in een functie). Maar zorg ervoor dat de planningstijd niet ten koste gaat van mogelijke prestatiewinst.
De WHERE
voorwaarde op kolommen in pess
is in tegenspraak met de . Postgres wordt gedwongen om dat om te zetten in een LEFT JOIN
INNER JOIN
. Wat erger is, de join komt laat in de join-boom. En aangezien Postgres uw joins niet opnieuw kan ordenen (zie hieronder), kan dat erg duur worden. Verplaats de tabel naar de eerste positie in de FROM
clausule om rijen vroegtijdig te elimineren. Volgend LEFT JOIN
s elimineren per definitie geen rijen. Maar met zoveel tafels is het belangrijk om joins te verplaatsen die kunnen vermenigvuldigen rijen naar het einde.
Je sluit je aan bij 13 tafels, waarvan 12 met LEFT JOIN
wat resulteert in 12!
mogelijke combinaties - of 11! * 2!
als we de ene nemen LEFT JOIN
houd er rekening mee dat is echt een INNER JOIN
. Dat is te veel voor Postgres om alle mogelijke permutaties te evalueren voor het beste queryplan. Lees over join_collapse_limit
:
- Voorbeeldquery om de schattingsfout van de kardinaliteit in PostgreSQL weer te geven
- SQL INNER JOIN over meerdere tabellen gelijk aan WHERE-syntaxis
De standaardinstelling voor join_collapse_limit
is 8 , wat betekent dat Postgres niet zal proberen de tabellen opnieuw te ordenen in uw FROM
clausule en de volgorde van tabellen is relevant .
Een manier om dit te omzeilen zou zijn om het prestatiekritische deel te splitsen in een CTE
zoals @joop heeft gereageerd
. Stel join_collapse_limit
niet in veel hoger of de tijden voor het plannen van query's waarbij veel samengevoegde tabellen betrokken zijn, zullen verslechteren.
Over je aaneengeschakelde datum genaamd 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
Aangenomen je bouwt uit drie numerieke kolommen voor jaar, maand en dag, die zijn gedefinieerd NOT NULL
, gebruik in plaats daarvan dit:
e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
Over de FM
sjabloon patroon modifier:
Maar eigenlijk moet u de datum opslaan als gegevenstype date
om mee te beginnen.
Ook vereenvoudigd:
format('%s (%s)', p.abrev, p.prenome) AS determinador
Zal de query niet sneller maken, maar het is veel schoner. Zie format()
.
Allereerst, al het gebruikelijke advies voor prestatie-optimalisatie van toepassing:
Als je dit allemaal goed doet, zou je veel snellere zoekopdrachten moeten zien voor alle patronen.