PostgreSQL Antipatterns: schÀdliche JOINs und OR

FĂŒrchten Sie Operationen, die Buffers bringen

Anhand einer kleinen Abfrage betrachten wir einige universelle AnsĂ€tze zur Optimierung von Abfragen in PostgreSQL. Ob Sie sie nutzen oder nicht, liegt bei Ihnen, aber es ist gut, darĂŒber Bescheid zu wissen.

In den spĂ€teren Versionen von PG könnte sich die Situation mit der „Intelligenz“ des Planers Ă€ndern, aber fĂŒr 9.4/9.6 sieht es hier ungefĂ€hr gleich aus wie in den Beispielen hier.

Ich nehme eine völlig reale Abfrage:

SELECT
  TRUE
FROM
  "Dokument" d
INNER JOIN
  "DokumentErweiterung" doc_ex
    USING("@Dokument")
INNER JOIN
  "DokumentTyp" t_doc ON
    t_doc."@DokumentTyp" = d."DokumentTyp"
WHERE
  (d."Person3" = 19091 or d."Mitarbeiter" = 19091) AND
  d."$Entwurf" IS NULL AND
  d."Gelöscht" IS NOT TRUE AND
  doc_ex."Zustand"[1] IS TRUE AND
  t_doc."DokumentTyp" = 'Arbeitsplan'
LIMIT 1;

ĂŒber Tabellennamen und FelderZu den „russischen“ Bezeichnungen von Feldern und Tabellen kann man unterschiedlich stehen, aber das ist Geschmackssache. Da wir in „Tensor“ keine auslĂ€ndischen Entwickler haben und PostgreSQL es uns erlaubt, Namen sogar mit Hieroglyphen zu geben, wenn sie in AnfĂŒhrungszeichen gesetzt sind, ziehen wir es vor, Objekte eindeutig und klar zu benennen, um MissverstĂ€ndnisse zu vermeiden.
Schauen wir uns den entstandenen Plan an:
PostgreSQL Antipatterns: schÀdliche JOINs und OR
[auf explain.tensor.ru anschauen]

144ms und fast 53K Buffers — das sind ĂŒber 400MB Daten! Und wir haben GlĂŒck, wenn alle diese Daten zum Zeitpunkt unserer Abfrage im Cache sind, sonst wird es beim Auslesen von der Festplatte um ein Vielfaches lĂ€nger.

Der Algorithmus ist das Wichtigste!

Um jede Abfrage irgendwie zu optimieren, muss man zunÀchst verstehen, was sie eigentlich tun soll.
Lassen Sie die Entwicklung der Datenbankstruktur fĂŒr diesen Artikel vorerst außen vor, und vereinbaren wir, dass wir relativ "kostengĂŒnstig" die Abfrage umschreiben und/oder einige benötigte Indizes auf die Datenbank anwenden können.

Also, die Abfrage:
— ĂŒberprĂŒft die Existenz eines beliebigen Dokuments
— im gewĂŒnschten Zustand und bestimmten Typ
— wo der Autor oder AusfĂŒhrende der benötigte Mitarbeiter ist

JOIN + LIMIT 1

HĂ€ufig ist es fĂŒr den Entwickler einfacher, eine Abfrage zu schreiben, bei der zunĂ€chst eine große Anzahl von Tabellen verbunden wird und dann aus dieser Vielzahl nur ein einzelner Datensatz ĂŒbrig bleibt. Aber einfacher fĂŒr den Entwickler heißt nicht effektiver fĂŒr die Datenbank.
In unserem Fall gab es insgesamt 3 Tabellen — und welcher Effekt


Lassen Sie uns zunÀchst die Verbindung zur Tabelle "DokumentTyp" entfernen und gleichzeitig der Datenbank mitteilen, dass wir einen einzigartigen Typ von Datensatz haben (das wissen wir, aber der Planer ahnt es noch nicht):

WITH T AS (
  SELECT
    "@DokumentTyp"
  FROM
    "DokumentTyp"
  WHERE
    "DokumentTyp" = 'Arbeitsplan'
  LIMIT 1
)
...
WHERE
  d."DokumentTyp" = (TABLE T)
...

Ja, wenn die Tabelle/CTE aus einem einzigen Feld eines einzigen Datensatzes besteht, kann man in PG sogar so schreiben, anstatt

d."DokumentTyp" = (SELECT "@DokumentTyp" FROM T LIMIT 1)

„Faule“ Berechnungen in PostgreSQL-Abfragen

BitmapOr vs UNION

In einigen FĂ€llen kann ein Bitmap Heap Scan sehr kostspielig sein - zum Beispiel in unserer Situation, in der eine recht große Anzahl von DatensĂ€tzen in die erforderlichen Bedingungen fĂ€llt. Wir haben es erhalten aufgrund von OR-Bedingungen, die sich in BitmapOr-Operationen im Plan verwandelt haben.
Kehren wir zur ursprĂŒnglichen Aufgabe zurĂŒck - wir mĂŒssen den Datensatz finden, der einem der Bedingungen entspricht - das heißt, es gibt keinen Grund, alle 59K DatensĂ€tze nach beiden Bedingungen zu durchsuchen. Es gibt einen Weg, eine Bedingung abzuarbeiten und erst zur zweiten ĂŒberzugehen, wenn bei der ersten nichts gefunden wurde.Uns hilft eine solche Konstruktion:

(
  SELECT
    ...
  LIMIT 1
)
UNION ALL
(
  SELECT
    ...
  LIMIT 1
)
LIMIT 1

Der „Àußere“ LIMIT 1 garantiert, dass die Suche endet, sobald der erste Datensatz gefunden wird. Und wenn dieser bereits im ersten Block gefunden wird, erfolgt die AusfĂŒhrung des zweiten nicht (never executed im Plan).

„Verstecken unter CASE“ komplexe Bedingungen

Im ursprĂŒnglichen Anfrage gibt es einen Ă€ußerst ungĂŒnstigen Punkt - die ÜberprĂŒfung des Status der verknĂŒpften Tabelle „DokumentErweiterung“. UnabhĂ€ngig von der Wahrhaftigkeit der anderen Bedingungen in der Aussage (zum Beispiel, d."Entfernt" IS NOT TRUE), wird diese Verbindung immer durchgefĂŒhrt und „kostet Ressourcen“. Ob mehr oder weniger dafĂŒr aufgewendet wird, hĂ€ngt vom Umfang dieser Tabelle ab.
Aber man kann die Anfrage so modifizieren, dass die Suche nach dem verknĂŒpften Datensatz nur dann erfolgt, wenn es tatsĂ€chlich notwendig ist:

SELECT
  ...
FROM
  "Dokument" d
WHERE
  ... 
/*index cond*/ AND
  CASE
    WHEN "$Entwurf" IS NULL AND "Entfernt" IS NOT TRUE THEN (
      SELECT
        "Status"[1] IS TRUE
      FROM
        "DokumentErweiterung"
      WHERE
        "@Dokument" = d."@Dokument"
    )
  END

Da aus der verknĂŒpften Tabelle keine Felder fĂŒr das Ergebnis benötigt werden, können wir den JOIN in eine Bedingung eines Subqueries umwandeln.Wir lassen die indizierten Felder „außerhalb“ des CASE, einfache Bedingungen in den WHEN-Block einbringen - und jetzt wird die „schwere“ Anfrage nur ausgefĂŒhrt, wenn wir in das THEN ĂŒbergehen.
Mein Nachname ist „Insgesamt“

Wir aggregieren die endgĂŒltige Anfrage mit all den oben beschriebenen Mechaniken:

Wir erstellen die resultierende Abfrage unter BerĂŒcksichtigung aller oben beschriebenen Mechanismen:

MIT T AS (
  SELECT
    "@Dokumenttyp"
  FROM
    "Dokumenttyp"
  WHERE
    "Dokumenttyp" = 'Arbeitsplan'
)
  (
    SELECT
      TRUE
    FROM
      "Dokument" d
    WHERE
      ("Person3", "Dokumenttyp") = (19091, (TABEL T)) AND
      FALLS
        WENN "$Entwurf" NULL IST UND "Gelöscht" IST NICHT TRUE DANN (
          SELECT
            "Zustand"[1] IST TRUE
          FROM
            "Dokumenterweiterung"
          WHERE
            "@Dokument" = d."@Dokument"
        )
      END
    LIMIT 1
  )
UNION ALL
  (
    SELECT
      TRUE
    FROM
      "Dokument" d
    WHERE
      ("Dokumenttyp", "Mitarbeiter") = ((TABEL T), 19091) AND
      FALLS
        WENN "$Entwurf" NULL IST UND "Gelöscht" IST NICHT TRUE DANN (
          SELECT
            "Zustand"[1] IST TRUE
          FROM
            "Dokumenterweiterung"
          WHERE
            "@Dokument" = d."@Dokument"
        )
      END
    LIMIT 1
  )
LIMIT 1;

Anpassung [an] Indizes

Ein geschultes Auge hat bemerkt, dass die indizierten Bedingungen in den UNION-Unterblocks leicht variieren – das liegt daran, dass wir bereits geeignete Indizes in der Tabelle haben. Wenn es sie nicht gegeben hĂ€tte, wĂ€re es sinnvoll gewesen, sie zu erstellen: Dokument(Person3, Dokumenttyp) und Dokument(Dokumenttyp, Mitarbeiter).
zur Reihenfolge der Felder in ROW-BedingungenAus der Sicht des Planers kann man natĂŒrlich auch schreiben (A, B) = (constA, constB), und (B, A) = (constB, constA). Aber bei der Aufzeichnung in der Reihenfolge der Felder im Index, ist eine solche Abfrage einfach leichter zu debuggen.
Was steht an?
PostgreSQL Antipatterns: schÀdliche JOINs und OR
[auf explain.tensor.ru anschauen]

Leider hatten wir Pech und im ersten UNION-Block wurde nichts gefunden, daher wurde der zweite tatsĂ€chlich ausgefĂŒhrt. Aber selbst dabei – nur 0,037 ms und 11 Buffers!
Wir haben die Abfrage beschleunigt und die "Bearbeitung" von Daten im Speicher um mehrere Tausend Mal, indem wir ziemlich einfache Techniken angewendet haben – ein gutes Ergebnis bei geringem Aufwand. 🙂

Quelle: habr.com

ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server đŸ”„ ZuverlĂ€ssiges Hosting fĂŒr Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster