PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Warum verlangsamt eine geringfügige Änderung des Suchbegriffs die Abfrage so sehr?

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 LEFT JOIN . Postgres ist gezwungen, dies in einen 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 :

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.