Varför?
orsaken är detta:
Snabb fråga:
-> 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)) ~~* (...)
Långsam fråga:
-> 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 (...)
Att utöka sökmönstret med en annan karaktär gör att Postgres antar ännu färre träffar. (Vanligtvis är detta en rimlig uppskattning.) Postgres har uppenbarligen inte tillräckligt exakt statistik (ingen, faktiskt, fortsätt läsa) för att förvänta dig samma antal träffar som du verkligen får.
Detta orsakar ett byte till en annan frågeplan, vilket är ännu mindre optimalt för den faktiska antal träffar rader=1129
.
Lösning
Antar nuvarande Postgres 9.5 eftersom den inte har deklarerats.
Ett sätt att förbättra situationen är att skapa ett uttrycksindex på uttrycket i predikatet. Detta gör att Postgres samlar statistik för det faktiska uttrycket, vilket kan hjälpa frågan även om själva indexet inte används för frågan . Utan indexet finns det ingen statistik för uttrycket överhuvudtaget. Och om det görs rätt kan indexet användas för frågan, det är ännu mycket bättre. Men det finns flera problem med ditt nuvarande uttryck:
unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%' )
Överväg den här uppdaterade frågan, baserat på några antaganden om dina hemliga tabelldefinitioner:
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' || '%');
Huvudpunkter
Varför f_unaccent()
? Eftersom unaccent()
kan inte indexeras. Läs detta:
Jag använde funktionen som beskrivs där för att tillåta följande (rekommenderas!) flerkolumns funktionella 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);
Om du inte är bekant med trigramindex, läs detta först:
Och möjligen:
Se till att köra den senaste versionen av Postgres (för närvarande 9.5). Det har skett avsevärda förbättringar av GIN-index. Och du kommer att vara intresserad av förbättringar i pg_trgm 1.2, planerad att släppas med den kommande Postgres 9.6:
Förberedda uttalanden är ett vanligt sätt att köra frågor med parametrar (särskilt med text från användarinmatning). Postgres måste hitta en plan som fungerar bäst för en given parameter. Lägg till jokertecken som konstanter till söktermen så här:
f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')
('vicenti'
skulle ersättas med en parameter.) Så Postgres vet att vi har att göra med ett mönster som varken är förankrat till vänster eller höger - vilket skulle tillåta olika strategier. Relaterat svar med mer information:
Eller kanske omplanera frågan för varje sökterm (eventuellt med dynamisk SQL i en funktion). Men se till att planeringstiden inte äter någon möjlig prestationsvinst.
WHERE
villkor på kolumner i pess
motsäger . Postgres tvingas konvertera det till en LEFT JOIN
INNER JOIN
. Vad värre är att sammanfogningen kommer sent i sammanfogningsträdet. Och eftersom Postgres inte kan ordna om dina anslutningar (se nedan), kan det bli väldigt dyrt. Flytta tabellen till den första position i FRÅN
klausul för att eliminera rader tidigt. Följer LEFT JOIN
s eliminerar inte några rader per definition. Men med så många tabeller är det viktigt att flytta sammanfogningar som kan föröka sig rader till slutet.
Du går med i 13 bord, 12 av dem med LEFT JOIN
vilket lämnar 12!
möjliga kombinationer - eller 11! * 2!
om vi tar den LEFT JOIN
det är verkligen en INNER JOIN
. Det är för många för Postgres att utvärdera alla möjliga permutationer för den bästa frågeplanen. Läs om join_collapse_limit
:
- Exempelfråga för att visa Cardinality-uppskattningsfel i PostgreSQL
- SQL INNER JOIN över flera tabeller lika med WHERE-syntax
Standardinställningen för join_collapse_limit
är 8 , vilket innebär att Postgres inte kommer att försöka ändra ordning på tabeller i din FROM
sats och tabellordningen är relevant .
Ett sätt att kringgå detta skulle vara att dela upp den prestandakritiska delen i en CTE
gillar @joop kommenterade
. Ställ inte in join_collapse_limit
mycket högre eller tider för frågeplanering som involverar många sammanfogade tabeller kommer att försämras.
Om ditt sammansatta datum heter 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
Förutsatt du bygger från tre numeriska kolumner för år, månad och dag, vilka är definierade NOT NULL
, använd det här istället:
e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
Om FM
mallmönstermodifierare:
Men egentligen borde du lagra datumet som datatyp datum
till att börja med.
Även förenklat:
format('%s (%s)', p.abrev, p.prenome) AS determinador
Kommer inte att göra frågan snabbare, men den är mycket renare. Se format()
.
Till sist, alla vanliga råd för prestandaoptimering gäller:
Om du får allt detta rätt bör du se mycket snabbare frågor för alla mönster.