PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"

Tausende von Vertriebsleitern im ganzen Land dokumentieren in unserem CRM-System tĂ€glich zehntausende Kontakte — Interaktionen mit potenziellen oder bereits bestehenden Kunden. Um diesen Kunden zu finden, muss man zuerst suchen, und das vorzugsweise sehr schnell. Dies erfolgt meist ĂŒber den Namen.

Es ist also wenig ĂŒberraschend, dass ich bei einer erneuten Analyse der "schweren" Anfragen in einer der am stĂ€rksten belasteten Datenbanken — unserem eigenen Unternehmenskonto von SBS, "an der Spitze" eine Anfrage fĂŒr die "schnelle" Suche nach Namen fĂŒr die Unternehmensprofile entdeckte.

DarĂŒber hinaus ergab eine weitere Untersuchung ein interessantes Beispiel zuerst der Optimierung, dann der Verschlechterung der Leistung der Anfrage durch die fortlaufende Bearbeitung vieler Teams, die alle nur das Beste wollten.

0: Was wollte der Benutzer?

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"[КДПВ hier herunter]

Was versteht der Benutzer normalerweise, wenn er von einer "schnellen" Suche nach Namen spricht? Es handelt sich in den seltensten FĂ€llen um eine "ehrliche" Suche nach einem Teilstring wie ... LIKE '%Rose%' — denn dann erscheinen nicht nur 'Roselia' und 'GeschĂ€ft Rose', sondern auch 'DieRose' . Es ist sehr praktisch, wenn alles an einem Ort ist. Dadurch können unsere Spezialisten bei Problemen schnell helfen. 'Das Haus des Weihnachtsmannes'.

Der Benutzer geht im Alltagsleben davon aus, dass Sie ihm bereitwillig eine Suche am Anfang des Wortes anbieten im Titel und ihm relevantere Ergebnisse anzeigen, die mit "beginnt mit" dem eingegebenen Wort ĂŒbereinstimmen. Und das tun Sie praktisch sofort — wĂ€hrend der Eingabe.

1: EinschrÀnkungen der Aufgabe

Und erst recht wird niemand bewusst 'rose shop' eingeben,damit Sie fĂŒr jedes Wort nach einem PrĂ€fix suchen mĂŒssen. Nein, es ist viel einfacher fĂŒr den Benutzer, auf den schnellen Vorschlag fĂŒr das letzte Wort zu reagieren, als absichtlich "unvollstĂ€ndig" mit den vorherigen zu sein — schauen Sie, wie das jeder Suchmaschine funktioniert.

Im Allgemeinen der richtige sind die Anforderungen an die Aufgabe zu formulieren — mehr als die HĂ€lfte der Lösung. Manchmal kann eine sorgfĂ€ltige Analyse des Use Cases den Ergebnis erheblich beeinflussen..

Was macht also der abstrakte Entwickler?

1.0: externe Suchmaschine

Oh, die Suche ist kompliziert, ich möchte mich damit ĂŒberhaupt nicht beschĂ€ftigen — lassen Sie uns das DevOps-Team damit beauftragen! Lassen Sie sie uns ein externes Suchsystem auf Basis der Datenbank einrichten: Sphinx, ElasticSearch,


Eine funktionale, wenn auch arbeitsintensive Lösung in Bezug auf Synchronisation und AktualitĂ€t. Doch in unserem Fall ist dies nicht nötig, da die Suche ausschließlich innerhalb der Daten jedes einzelnen Kontos erfolgt. Und die Daten sind stark verĂ€nderlich – selbst wenn der Manager jetzt einen Datensatz hinzugefĂŒgt hat, 'Rosenladen', kann es in 5-10 Sekunden sein, dass er sich daran erinnert, eine E-Mail-Adresse vergessen zu haben und diese finden und korrigieren möchte.

Deshalb – lassen Sie uns direkt in der Datenbank suchen. Zum GlĂŒck ermöglicht es uns PostgreSQL, dies zu tun, und nicht nur auf eine Weise – die werden wir auch betrachten.

1.1: „echte“ Teilzeichenfolge

Wir hĂ€ngen uns an das Wort „Teilzeichenfolge“. Gerade fĂŒr die indizierte Suche nach Teilzeichenfolgen (und sogar mittels regulĂ€rer AusdrĂŒcke!) gibt es ein ausgezeichnetes Modul pg_trgm! Allerdings muss das Ergebnis dann richtig sortiert werden.

Lassen Sie uns zur Vereinfachung ein solches Modell verwenden:

CREATE TABLE firms(
  id
    serial
      PRIMARY KEY
, name
    text
);

Wir laden 7,8 Millionen DatensÀtze realer Organisationen hoch und indizieren:

CREATE EXTENSION pg_trgm;
CREATE INDEX ON firms USING gin(lower(name) gin_trgm_ops);

Lassen Sie uns die ersten 10 DatensĂ€tze fĂŒr die Teilzeichenfolgensuche finden:

WÄHLEN
  *
VON
  firmen
WO
  lower(name) ~ ('(^|s)' || 'rĐŸĐ·Đ°')
BESTELLEN NACH
  lower(name) ~ ('^' || 'rĐŸĐ·Đ°') DESC -- zuerst "anfangend mit"
, lower(name) -- der Rest alphabetisch
LIMIT 10;

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"
[sehen Sie sich explain.tensor.ru an]

Naja, so ist das
 26ms, 31MB gelesene Daten und ĂŒber 1,7K gefilterte DatensĂ€tze — fĂŒr 10 Suchanfragen. Die Overheads sind zu groß, gibt es keine effizientere Möglichkeit?

1.2: Textsuche? Das ist doch FTS!

TatsĂ€chlich bietet PostgreSQL eine sehr leistungsfĂ€hige Mechanismus fĂŒr die Volltextsuche (Full Text Search), einschließlich der Möglichkeit der PrĂ€fixsuche. Eine großartige Option, sogar ohne zusĂ€tzliche Erweiterungen! Lassen Sie uns das ausprobieren:

INDEX ERSTELLEN IN firmen USING gin(to_tsvector('simple'::regconfig, lower(name)));

WÄHLEN
  *
VON
  firmen
WO
  to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'rĐŸĐ·Đ°:*')
BESTELLEN NACH
  lower(name) ~ ('^' || 'rĐŸĐ·Đ°') DESC
, lower(name)
LIMIT 10;

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"
[sehen Sie sich explain.tensor.ru an]

Hier hat uns die Parallelisierung der AbfrageausfĂŒhrung etwas geholfen, wodurch die Zeit auf 11msreduziert wurde. Und wir mussten auch 1,5-mal weniger lesen — insgesamt 20MB. Je weniger wir lesen, desto besser, denn je grĂ¶ĂŸer das Volumen, das wir durchlesen, desto höher ist die Wahrscheinlichkeit eines Cache-Misses, und jede zusĂ€tzlich vom Speicher gelesene Datenseite stellt potenzielle „Bremsen“ fĂŒr die Abfrage dar.

1.3: Doch LIKE?

Jeder vorherige Vorschlag ist gut, aber wenn wir ihn hunderttausend Mal am Tag ausfĂŒhren, wird das schon umfangreicher. 2 TB an gelesenen Daten. Im besten Fall erfolgt dies aus dem Speicher, aber wenn wir Pech haben, muss es sogar von der Festplatte kommen. Versuchen wir also, es kleiner zu machen.

Lassen Sie uns daran denken, dass der Benutzer zuerst sehen möchte die, die mit „
“ beginnen. Das ist doch ein klassischer Fall von PrĂ€fixsuche mit Hilfe von text_pattern_ops! Und nur wenn uns bis zu 10 gefundene DatensĂ€tze „fehlen“, mĂŒssen wir sie mit FTS-Suche nachlesen:

CREATE INDEX ON firms(lower(name) text_pattern_ops);

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('rose' || '%')
LIMIT 10;

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"
[sehen Sie sich explain.tensor.ru an]

Ausgezeichnete Werte — nur 0,05 ms und etwas mehr als 100 KB gelesen! Nur wir haben die Sortierung nach Namen, vergessen, damit der Benutzer sich nicht in den Ergebnissen verliert:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('rose' || '%')
ORDER BY
  lower(name)
LIMIT 10;

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"
[sehen Sie sich explain.tensor.ru an]

Oh, das sieht schon nicht mehr so schön aus — es gibt zwar einen Index, aber die Sortierung ignoriert ihn
 Es ist natĂŒrlich schon um ein Vielfaches effizienter als die vorherige Variante, aber


1.4: „mit Feile nachbessern“

Es gibt doch einen Index, der es ermöglicht, sowohl im Bereich zu suchen als auch die Sortierung ordentlich zu nutzen — ein normaler B-Baum!

CREATE INDEX ON firms(lower(name));

Die Anfrage muss nur manuell erstellt werden:

SELECT
  *
FROM
  firms
WHERE
  lower(name) >= 'rose' AND
  lower(name) <= ('rose' || chr(65535)) -- fĂŒr UTF8, fĂŒr einbyteige - chr(255)
ORDER BY
   lower(name)
LIMIT 10;

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"
[sehen Sie sich explain.tensor.ru an]

Ausgezeichnet – die Sortierung funktioniert und der Ressourcenverbrauch bleibt "mikroskopisch". tausendmal effizienter als "reines" FTS! Es bleibt, alles in eine Anfrage zu vereinen:

(
  SELECT
    *
  FROM
    firms
  WHERE
    lower(name) >= 'rose' AND
    lower(name) <= ('rose' || chr(65535)) -- fĂŒr UTF8, fĂŒr einbyteige Kodierungen - chr(255)
  ORDER BY
     lower(name)
  LIMIT 10
)
UNION ALL
(
  SELECT
    *
  FROM
    firms
  WHERE
    to_tsvector('simple'::regconfig, lower(name)) @@ to_tsquery('simple', 'rose:*') AND
    lower(name) NOT LIKE ('rose' || '%') -- "beginnend mit" haben wir bereits oben gefunden
  ORDER BY
    lower(name) ~ ('^' || 'rose') DESC -- verwenden Sie dieselbe Sortierung, um NICHT den btree-Index zu verwenden
  , lower(name)
  LIMIT 10
)
LIMIT 10;

Ich möchte darauf hinweisen, dass die zweite Unterabfrage nur ausgefĂŒhrt wird wenn die erste weniger als erwartet zurĂŒckgibt zuletzt LIMIT Anzahl der Zeilen. Über diese Art der Abfrageoptimierung habe ich bereits frĂŒher geschrieben..

Ja, wir haben nun gleichzeitig einen btree und gin auf der Tabelle, trotzdem kam statistisch heraus, dass weniger als 10% der Abfragen die AusfĂŒhrung des zweiten Blocks erreichen.. Das heißt, bei diesen typischen, im Voraus bekannten EinschrĂ€nkungen fĂŒr die Aufgabe konnten wir den gesamten Ressourcenverbrauch des Servers nahezu um das Tausendfache reduzieren!

1.5*: kommen wir ohne Feilen aus

Höher LIKE uns hat die falsche Sortierung behindert. Aber sie kann 'auf den richtigen Weg' gebracht werden durch die Angabe des USING-Operators:

StandardmĂ€ĂŸig wird unterstellt ASC. DarĂŒber hinaus kann der Name eines spezifischen Sortieroperators in der Anweisung angegeben werden USING. Der Sortieroperator muss ein Mitglied der 'kleiner als' oder 'grĂ¶ĂŸer als' Familie von B-Baum-Operatoren sein. ASC entspricht normalerweise USING < und DESC entspricht normalerweise USING >.

In unserem Fall ist 'kleiner' das ~<~:

SELECT
  *
FROM
  firms
WHERE
  lower(name) LIKE ('rose' || '%')
ORDER BY
  lower(name) USING ~<~
LIMIT 10;

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"
[sehen Sie sich explain.tensor.ru an]

2: wie Anfragen 'verderben'

Jetzt lassen wir unsere Anfrage 'ziehen' ein halbes Jahr bis ein Jahr und stellen mit Erstaunen fest, dass sie wieder 'in den TOP' ist mit den Werten des gesamten tĂ€glichen 'Pufferns' des Speichers (buffers shared hit) bei 5.5TB — das heißt noch mehr, als es ursprĂŒnglich war.

NatĂŒrlich ist unser GeschĂ€ft gewachsen und die Last hat zugenommen, aber nicht so sehr! Das bedeutet, hier stimmt etwas nicht — lassen Sie uns das klĂ€ren.

2.1: Geburt des Paging

Irgendwann hatte ein anderes Entwicklerteam den Wunsch, die Möglichkeit zu schaffen, aus der schnellen Zeilenansicht in das Register mit denselben, aber erweiterten Ergebnissen zu „springen“. Und welches Register kommt ohne Seitenavigation aus? Lassen Sie uns das hinzufĂŒgen!

( ... LIMIT  + 10)
UNION ALL
( ... LIMIT  + 10)
LIMIT 10 OFFSET ;

Jetzt konnte der Entwickler ohne großen Aufwand das Suchergebnisregister mit einer Art „Seitenladung“ anzeigen.

NatĂŒrlich, tatsĂ€chlich, wird fĂŒr jede nachfolgende Seite mehr und mehr Daten gelesen (alles von der letzten Anfrage, das wir weglassen, plus der benötigte „Rest“) – das ist also ein klares Antipattern. Besser wĂ€re es, die Suche in der nĂ€chsten Iteration vom im Interface gespeicherten SchlĂŒssel zu starten, aber dazu mehr ein anderes Mal.

2.2: der Wunsch nach Exotik

Irgendwann wĂŒnschte sich der Entwickler, die Ergebnismenge mit Daten aus einer anderen Tabelle zu variieren, wofĂŒr die gesamte vorherige Anfrage in eine CTE gesendet wurde:

WITH q AS (
  ...
  LIMIT  + 10
)
SELECT
  *
, (SELECT ...) sub_query -- eine Abfrage zu der verknĂŒpften Tabelle
FROM
  q
LIMIT 10 OFFSET ;

Und selbst so ist es nicht schlecht, da die geschachtelte Anfrage nur fĂŒr 10 zurĂŒckgegebene DatensĂ€tze berechnet wird, wenn nicht


2.3: DISTINCT ist sinnlos und erbarmungslos

Irgendwo im Verlauf dieser Evolution ging aus dem 2. Unterabfrage etwas verloren NOT LIKE Bedingung. Es ist klar, dass danach UNION ALL beginnt es damit, einige DatensĂ€tze doppelt zurĂŒckzugeben — zuerst gefunden durch den Anfang des Strings und dann noch einmal — durch den Anfang des ersten Wortes dieses Strings. Im Extremfall könnten alle DatensĂ€tze der 2. Unterabfrage mit den DatensĂ€tzen der ersten ĂŒbereinstimmen.

Was macht der Entwickler, anstatt die Ursache zu suchen?.. Keine Frage!

  • erweitern wir die GrĂ¶ĂŸe der ursprĂŒnglichen Stichproben
  • wir wenden DISTINCT an, damit nur Einzelinstanzen jeder Zeile entstehen

WITH q AS (
  ( ... LIMIT  + 10)
  UNION ALL
  ( ... LIMIT  + 10)
  LIMIT  + 10
)
SELECT DISTINCT
  *
, (SELECT ...) sub_query
FROM
  q
LIMIT 10 OFFSET ;

Das heißt, es ist klar, dass das Ergebnis letztlich genau dasselbe ist, aber die Chance, im 2. Unterabfrage CTE «durchzufallen», ist erheblich gestiegen, und ohne das, es wird offensichtlich mehr gelesen.

Aber das ist nicht das grĂ¶ĂŸte Problem. Da der Entwickler gebeten hat, nicht nach bestimmten, sondern nach allen Feldern auszuwĂ€hlen DISTINCT DatensĂ€tze, werden automatisch auch das Feld sub_query — das Ergebnis der Unterabfrage — einbezogen. Jetzt, um dies auszufĂŒhren Aufzeichnungen, sodass das Feld sub_query — das Ergebnis der Unterabfrage — automatisch dorthin gelangt. Jetzt, um dies auszufĂŒhren DISTINCT, musste die Basis bereits erfĂŒllen nicht 10 Unterabfragen, sondern alle + 10!

2.4: Kooperation steht ĂŒber allem!

So lebten die Entwickler — sorglos, denn im Register fehlte es den Usern offensichtlich an Geduld, um N auf signifikante Werte zu „drehen“ angesichts der chronischen Verzögerungen beim Abrufen jeder nĂ€chsten „Seite“.

Bis Entwickler aus einer anderen Abteilung kamen und sich dazu entschlossen, diese praktische Methode zu nutzen fĂŒr die iterative Suche – das bedeutet, wir nehmen einen Abschnitt aus einer Auswahl, filtern nach zusĂ€tzlichen Bedingungen, zeichnen das Ergebnis auf, dann den nĂ€chsten Abschnitt (was in unserem Fall durch die Erhöhung von N erreicht wird), und so weiter, bis der Bildschirm gefĂŒllt ist.

Insgesamt erreichte N fast 17K , und innerhalb von 24 Stunden wurden „in Kette“ nicht weniger als 4K solcher Abfragen ausgefĂŒhrt. Die letzten dieser wurden bereits mit1GB RAM bei jeder Iteration gescannt. VollstĂ€ndige Heimautomatisierung im Neubau. Fortsetzung


Gesamt

PostgreSQL Antipatterns: eine Geschichte ĂŒber die iterative Verbesserung der Suche nach Namen, oder "Optimierung hin und zurĂŒck"

Quelle: habr.com

Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server đŸ”„ Kaufen Sie zuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster