Warum?
Der Grund ist das:
Schnellabfrage:
-> 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)) ~~* (...)
Langsame Abfrage:
-> 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 (...)
Eine Erweiterung des Suchmusters um ein weiteres Zeichen führt dazu, dass Postgres noch weniger Treffer annimmt. (Normalerweise ist dies eine vernünftige Schätzung.) Postgres verfügt offensichtlich nicht über ausreichend genaue Statistiken (eigentlich keine, lesen Sie weiter), um die gleiche Anzahl von Treffern zu erwarten, die Sie wirklich erhalten.
Dies führt zu einem Wechsel zu einem anderen Abfrageplan, der für den tatsächlichen noch weniger optimal ist Anzahl der Treffer rows=1129
.
Lösung
Aktuelles Postgres 9.5 vorausgesetzt, da es nicht deklariert wurde.
Eine Möglichkeit, die Situation zu verbessern, besteht darin, einen Ausdrucksindex zu erstellen auf den Ausdruck im Prädikat. Dadurch sammelt Postgres Statistiken für den eigentlichen Ausdruck, was der Abfrage helfen kann, auch wenn der Index selbst nicht für die Abfrage verwendet wird . Ohne den Index gibt es keine Statistiken für den Ausdruck überhaupt. Und wenn es richtig gemacht wird, kann der Index für die Abfrage verwendet werden, das ist sogar noch viel besser. Aber es gibt mehrere Probleme mit Ihrem aktuellen Ausdruck:
unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%')
Betrachten Sie diese aktualisierte Abfrage basierend auf einigen Annahmen über Ihre nicht offengelegten Tabellendefinitionen:
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' || '%');
Wichtige Punkte
Warum f_unaccent()
? Weil unaccent()
kann nicht indiziert werden. Lesen Sie dies:
Ich habe die dort skizzierte Funktion verwendet, um das folgende (empfohlene!) mehrspaltige funktionale Trigramm GIN index zu ermöglichen :
CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);
Wenn Sie mit Trigramm-Indizes nicht vertraut sind, lesen Sie zuerst Folgendes:
Und evtl.:
Stellen Sie sicher, dass Sie die neueste Version von Postgres ausführen (derzeit 9.5). Die GIN-Indizes wurden erheblich verbessert. Und Sie werden an den Verbesserungen in pg_trgm 1.2 interessiert sein, das mit dem kommenden Postgres 9.6 veröffentlicht werden soll:
Vorbereitete Erklärungen sind eine gängige Methode, um Abfragen mit Parametern auszuführen (insbesondere mit Text aus Benutzereingaben). Postgres muss einen Plan finden, der für jeden gegebenen Parameter am besten funktioniert. Fügen Sie Platzhalter als Konstanten hinzu zum zum Suchbegriff wie folgt:
f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')
('vicenti'
würde durch einen Parameter ersetzt werden.) Postgres weiß also, dass wir es mit einem Muster zu tun haben, das weder links noch rechts verankert ist - was unterschiedliche Strategien zulassen würde. Zugehörige Antwort mit weiteren Details:
Oder vielleicht die Abfrage für jeden Suchbegriff neu planen (evtl. mit dynamischem SQL in einer Funktion). Stellen Sie jedoch sicher, dass die Planungszeit keinen möglichen Leistungsgewinn auffrisst.
Das WHERE
Bedingung für Spalten in pess
widerspricht dem . Postgres ist gezwungen, dies in einen LEFT JOIN
INNER JOIN
umzuwandeln . Was noch schlimmer ist, der Join kommt spät im Join-Baum. Und da Postgres Ihre Joins nicht neu anordnen kann (siehe unten), kann das sehr teuer werden. Verschieben Sie die Tabelle an die erste Position im FROM
-Klausel, um Zeilen frühzeitig zu eliminieren. Folgen Sie LEFT JOIN
s eliminieren per Definition keine Zeilen. Aber bei so vielen Tabellen ist es wichtig, Joins zu verschieben, die sich multiplizieren könnten Zeilen bis zum Ende.
Sie treten 13 Tischen bei, 12 davon mit LEFT JOIN
was 12!
übrig lässt mögliche Kombinationen - oder 11! * 2!
wenn wir den einen LEFT JOIN
nehmen berücksichtigen, dass dies wirklich ein INNER JOIN
ist . Das ist auch viele für Postgres, um alle möglichen Permutationen für den besten Abfrageplan auszuwerten. Lesen Sie mehr über join_collapse_limit
:
- Beispielabfrage zum Anzeigen des Kardinalitätsschätzungsfehlers in PostgreSQL
- SQL INNER JOIN über mehrere Tabellen gleich WHERE-Syntax
Die Standardeinstellung für join_collapse_limit
ist 8 , was bedeutet, dass Postgres nicht versucht, Tabellen in Ihrem FROM
neu anzuordnen -Klausel und die Reihenfolge der Tabellen ist relevant .
Eine Möglichkeit, dies zu umgehen, wäre, den leistungskritischen Teil in einen CTE
wie @joop hat kommentiert
. Legen Sie join_collapse_limit
nicht fest viel höher oder die Zeiten für die Abfrageplanung mit vielen verknüpften Tabellen verschlechtern sich.
Über Ihr verkettetes Datum mit dem Namen 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
Vorausgesetzt Sie bauen aus drei numerischen Spalten für Jahr, Monat und Tag auf, die NOT NULL
definiert sind , verwenden Sie stattdessen Folgendes:
e.ano::text || to_char(e.mes2, 'FM"-"00')
|| to_char(e.dia, 'FM"-"00') AS data
Über den FM
Modifikator für Vorlagenmuster:
Aber eigentlich sollten Sie das Datum als Datentyp date
zu beginnen.
Auch vereinfacht:
format('%s (%s)', p.abrev, p.prenome) AS determinador
Die Abfrage wird nicht schneller, aber viel sauberer. Siehe format()
.
Das Wichtigste zum Schluss, all die üblichen Ratschläge zur Leistungsoptimierung gilt:
Wenn Sie das alles richtig machen, sollten Sie viel schnellere Abfragen für alle sehen Muster.