AQO - Adaptive Abfrageoptimierung in PostgreSQL

Moderne DBMS verwenden bei der Durchführung von Abfragen das Kostenoptimierungsmodell - basierend auf den in den Konfigurationsdateien gespeicherten Koeffizienten und den gesammelten Statistiken berechnen sie den „Preis“ des Empfangs und das Volumen der resultierenden Zeilensätze. Wenn Abfragen erneut ausgeführt werden, werden Kosten und Selektivität neu berechnet. Sie können eine Abfrage ausführen und die tatsächlichen Werte dieser Parameter anzeigen. Während der (Standard-) Neuplanung verwendet der DBMS-Optimierer diese Informationen jedoch in keiner Weise.



Was aber, wenn der Optimierer die tatsächlichen Werte für Kosten, Selektivität und andere erforderliche Parameter für die Ausführung von Abfragen beibehält und sich bei Wiederholung nicht nur auf die standardmäßig gesammelten Statistiken konzentriert, sondern auch auf diejenigen, die nach der vorherigen Ausführung gespeichert wurden?



Dies wird als adaptive Abfrageoptimierung bezeichnet und ist vielversprechend. Einige DBMS verwenden solche Technologien bereits.



Unternehmen Postgres Professional arbeitet seit mehreren Jahren an der AQO-Erweiterung für PostgreSQL, die (in irgendeiner Form) adaptive Optimierung implementiert. Die Arbeiten sind noch im Gange, aber es gibt bereits etwas zu testen.



Schauen wir uns zunächst den Themenbereich der Abfrageoptimierung genauer an.



Warum ein Planer einen suboptimalen Plan auswählen kann



SQL-Abfragen können auf verschiedene Arten ausgeführt werden. Wenn beispielsweise zwei Tabellen verknüpft sind, kann dies auf verschiedene Arten erfolgen - mithilfe verschachtelter Schleifen, Zusammenführen und Hashing. Je mehr Tabellen an der Abfrage beteiligt sind, desto mehr Variationen ihrer Verknüpfungen. Die Aufgabe des Planers besteht darin, aus vielen Variationen den Ausführungsplan der Abfrage mit den niedrigsten Kosten auszuwählen.



Wie bereits erwähnt, verwenden die Scheduler vieler DBMS in ihrer Arbeit statistische Informationen, die entweder automatisch oder manuell erfasst werden. Der Planer berechnet anhand dieser Statistiken die geschätzten Kosten.



Im Allgemeinen funktionieren die Planer moderner DBMS in den meisten Situationen gut. In einigen Fällen kann der gewählte Plan jedoch weit vom Optimum entfernt sein.



Beispielsweise,Das Fehlen aktueller statistischer Informationen führt dazu, dass sich der Planer bei seiner Arbeit an (höchstwahrscheinlich) falschen Daten zur Anzahl der Zeilen in den zu verbindenden Tabellen orientiert. Übermäßiges Understatement (oder Übertreiben) der Kardinalität führt zur Auswahl nicht optimaler Methoden für den Zugriff auf Daten in Tabellen.



Ein weiterer wichtiger Grund kann das Fehlen notwendiger Indizes sein . In Ermangelung von Indizes hat der Scheduler eine begrenzte Auswahl an Datenzugriffsmethoden.



Verwendung abhängiger (korrelierter) Bedingungenkann sich auch negativ auf den Betrieb des DBMS auswirken. Der Scheduler geht (standardmäßig) davon aus, dass alle Bedingungen in einer Abfrage unabhängig voneinander sind, dh der Wert einer Bedingung wirkt sich in keiner Weise auf die andere aus. Dies ist nicht immer der Fall. Wenn abhängige Bedingungen verwendet werden (z. B. Postleitzahl und Stadt), berechnet der Planer auch die falschen Kosten und die falsche Kardinalität der Verbindungen.



Die Verwendung von Funktionen in Bedingungen kann sich auch auf den Scheduler auswirken . Eine Funktion für den Planer ist eine „Black Box“. Er weiß nicht, wie viele Zeilen die Funktion zurückgeben wird, was auch zu fehlerhaften Plankosten führen kann.



Möglichkeiten, die Arbeit des Schedulers zu beeinflussen



Aktuelle Statistiken sind eine unabdingbare Voraussetzung für die angemessene Arbeit des Schedulers. Stellen Sie zunächst sicher, dass das System so konfiguriert ist, dass regelmäßig statistische Informationen erfasst werden.


Es gibt verschiedene Möglichkeiten, um die oben beschriebenen Situationen zu beheben und dem Planer bei der Auswahl der optimalsten Abfrageausführungspläne zu helfen.



Ohne Indizes hat der Scheduler nur eine Möglichkeit, Daten zu erhalten - das sequentielle Scannen von Tabellen (und dies ist nicht immer schlecht und teuer). In einigen Fällen kann das Erstellen der erforderlichen Indizes den Datenzugriff beschleunigen. Sie müssen nicht die gesamte Tabelle scannen. Die Verwendung von Indizes (Suche nach dem Notwendigen, Erstellung, Pflege) ist jedoch nicht kostenlos. Idealerweise sollten sie genau dort eingesetzt werden, wo sie benötigt werden. Und wo nicht benötigt - nicht verwenden.



Wenn korreliert mit Join - Bedingungen in Abfragen können Sie erzeugen eine erweiterte Statistik- den Optimierer explizit auffordern, dass die Bedingungen miteinander zusammenhängen. Dazu muss der DBA (oder Entwickler) seine Daten gut kennen und abhängige Bedingungen in Abfragen verfolgen, da die Anzahl der Kombinationen abhängiger Spalten im Voraus schwer vorherzusagen ist. Erweiterte Statistiken müssen für jede dieser Optionen manuell erstellt werden.



Beim Erstellen einer Funktion können Sie die ungefähren Ausführungskosten und / oder eine Schätzung der Anzahl der von der Funktion zurückgegebenen Zeilen angeben . In Version 12 wurde es möglich, Hilfsfunktionen zu verwenden, um die Schätzungen des Planers in Abhängigkeit von den Argumenten zu verbessern. Dies ist auch ein manueller Weg, der nicht immer ein optimales Ergebnis liefert.



Wenn alles andere fehlschlägt, können Sie die Abfrage manuell neu schreibenVerwenden Sie beispielsweise materialisierte Ansichten, Common Table Expressions (CTE). Oder um die Anforderungen des Themenbereichs zu klären und möglicherweise die Logik der Anforderung radikal umzuschreiben.



Und es ist eine andere Art von „Hinweise“ an den Scheduler - adaptive Abfrageoptimierung ( a daptive q uery o ptimization). Die Idee dieser Methode ist, dass nach der Ausführung der Abfrage echte statistische Informationen gespeichert werden und der Optimierer sich darauf verlassen kann, wenn die angegebene (oder ähnliche) Abfrage wiederholt wird.



Das DBMS Postgres Pro Enterprise ist eine Erweiterung für die adaptive Abfrageoptimierung mit dem Namen AQO... Diese Erweiterung ist auf github veröffentlicht: github.com/postgrespro/aqo , Sie können sie mit Vanilla PostgreSQL ausprobieren, mehr dazu weiter unten.



Das Funktionsprinzip des Moduls



Das AQO-Modul verwendet in seiner Arbeit maschinelles Lernen. Weitere Informationen zum Funktionsprinzip finden Sie in dem Artikel von Oleg Ivanov über maschinelles Lernen zur Steigerung der PostgreSQL-Leistung und ausführlicher in der Präsentation Adaptive Query Optimization (Bericht auf YouTube ).



Das Wesentliche dieser Methode wird im Folgenden kurz beschrieben:



Um die Kosten zu bewerten, benötigt der Planer eine Bewertung der Kardinalität, und dafür ist wiederum eine Bewertung der Selektivität der Bedingungen erforderlich.



Für einfache Bedingungen (wie „Attribut = Konstante“ oder „Attribut> Konstante“) verfügt der Scheduler über ein Modell, anhand dessen er die Selektivität schätzt. Dazu verwendet er statistische Informationen: die Anzahl der eindeutigen Attributwerte, Histogramme usw.

Für Bedingungen, die aus einfachen Elementen bestehen, die logische Verknüpfungen verwenden, verwendet der Planer einfach zu berechnende Formeln:



  • sel (nicht A) = 1 - sel (A)
  • sel (nicht A) = 1 - sel (A)
  • sel (A und B) = sel (A) * sel (B)
  • sel (A oder B) = sel (nicht (nicht A und nicht B)) = 1 - (1 - sel (A)) * (1 - sel (B))


Diese Formeln setzen die Unabhängigkeit (Unkorrelation) der Bedingungen A und B voraus, weshalb wir falsche Schätzungen erhalten, wenn diese Annahme verletzt wird.

AQO verkompliziert die Formel: führt für jede einfache Bedingung einen eigenen Koeffizienten ein. Unter Verwendung von maschinellem Lernen (unter Verwendung der Regression des nächsten Nachbarn) passt AQO diese Koeffizienten so an, dass die durch die Formel berechnete Selektivität am besten mit der tatsächlichen Selektivität übereinstimmt, die AQO zuvor beobachtet hat.



Dazu speichert das Modul:



  • , ;
  • .


AQO unterscheidet in seiner Arbeit zwischen Bedingungen bis hin zu Konstanten. Dies ermöglicht es, die Komplexität des zu lösenden Problems zu verringern, und außerdem gehen in den meisten Fällen immer noch keine Informationen verloren: AQO "sieht" den Wert der Konstante nicht, sondern "sieht" die Selektivität der Bedingung.

Die Situation, in der der Verlust immer noch auftritt, sind die Bedingungen, die unabhängig von den spezifischen Werten als Konstante bewertet werden. Beispielsweise kann der Planer für einige Bedingungen keine vernünftigen Schätzungen vornehmen und wählt eine Standardkonstante (zum Beispiel wird die Selektivität der Bedingung "Ausdruck1 = Ausdruck2" immer mit 0,005 und "Ausdruck1> Ausdruck2" mit 1/3 bewertet).



Somit kann AQO die Bewertung der Selektivität komplexer Bedingungen verbessern (und infolgedessen die Bewertung der Kosten, was zur Wahl eines angemesseneren Umsetzungsplans führen kann).



Modulinstallation



Um die Funktionalität des Moduls unter Vanilla PostgreSQL zu testen, müssen Sie einen speziellen Patch verwenden und das System dann aus dem Quellcode erstellen. Weitere Informationen finden Sie in der README-Datei auf github.



Wenn Postgres Pro Enterprise verwendet wird, wird die Installation des AQO-Moduls im Standardmodus fortgesetzt:



shared_preload_libraries = 'aqo'



Danach können Sie eine Erweiterung in der erforderlichen Datenbank erstellen.



Datenbankvorbereitung



Schauen wir uns ein konkretes Beispiel an, wie das AQO-Modul in einer Demo-Datenbank funktioniert . Wir werden eine große Datenbank mit Informationen zu Flügen für das Jahr von September 2016 bis September 2017 verwenden.



Erstellen wir zunächst eine Erweiterung:



CREATE EXTENSION aqo;




Deaktivieren Sie als Nächstes die parallele Abfrageverarbeitung, damit die Anzeige paralleler Pläne nicht von der Hauptaufgabe ablenkt:



max_parallel_workers_per_gather = 0;



Damit der PostgreSQL-Scheduler mehr Optionen zum Verknüpfen von Tabellen bietet, erstellen Sie zwei Indizes:



CREATE INDEX ON flights (scheduled_departure );
CREATE INDEX ON ticket_flights (fare_conditions );


Bei der Analyse der Ergebnisse konzentrieren wir uns auf den Wert von BUFFERS als die Anzahl der Seiten, die gelesen werden müssen, um die Aufgabe auszuführen. Schauen wir uns auch die Ausführungszeit an (aber die Zeit auf einem geladenen System und einem Heim-Laptop kann sehr unterschiedlich sein).



Erhöhen wir den Puffercache und work_mem, damit alle Arbeiten im RAM ausgeführt werden:



shared_buffers = '256MB';

work_mem = '256MB';




Verwenden des AQO-Moduls



Wir werden eine Anfrage formulieren: Es ist erforderlich, Passagiere zu empfangen, die ab einem bestimmten Datum eine Business Class geflogen sind und spätestens mit einer Stunde verspätet angekommen sind.

Lassen Sie uns die Anforderung ohne Verwendung von AQO ausführen (im Folgenden wurde ein Teil der Informationen, die das Verständnis der Funktionsweise des Moduls nicht beeinträchtigen, aus den Plänen entfernt):



EXPLAIN (ANALYZE, BUFFERS, TIMING OFF) SELECT t.ticket_no
  FROM flights f
   	JOIN ticket_flights tf ON f.flight_id = tf.flight_id
   	JOIN tickets t ON tf.ticket_no = t.ticket_no
 WHERE f.scheduled_departure > '2017-08-01'::timestamptz
   AND f.actual_arrival < f.scheduled_arrival + interval '1 hour'
   AND tf.fare_conditions = 'Business';




Und sehen wir uns den resultierenden Plan an: In diesem Fall hat der Planer den optimalen Plan betrachtet, bei dem wir zuerst mithilfe eines Bitmap-Scans ( ) eine Reihe von Zeilen aus der Flugtabelle erhalten , die wir (einen Knoten ) mit einer Reihe von Zeilen aus der Tabelle ticket_flights haben, die unter Verwendung des Index erhalten wurden scan ( ). Das Ergebnis wird als externer Satz von Zeilen für die endgültige Verbindung durch eine verschachtelte Schleife (Knoten ) verwendet. Der innere Satz für diesen Join wird durch einen exklusiven Index-Scan der Tickets ( ) -Tabelle erhalten . Die "sperrigste" Operation besteht darin, das interne Rowset für die verschachtelte Schleife abzurufen - 106 205 Puffer werden darin eingelesen.



Nested Loop (rows=33210) (actual rows=31677)

  Buffers: shared hit=116830 read=1

  ->  Hash Join (rows=33210) (actual rows=31677)

        Hash Cond: (tf.flight_id = f.flight_id)

        ->  Index Scan ... on ticket_flights tf  

              Index Cond: fare_conditions = 'Business'

        ->  Hash

              ->  Bitmap Heap Scan on flights f (rows=8331) (actual rows=7673)

                    Recheck Cond: scheduled_departure > '2017-08-01'

                    Filter: actual_arrival < scheduled_arrival + '01:00:00'::interval

                    ->  Bitmap Index Scan on ... [flights]

                          Index Cond: scheduled_departure > '2017-08-01'

                          Buffers: shared hit=44 read=1

  ->   Index Only Scan  ... on tickets t (rows=1 width=14) (actual rows=1 loops=31677)

        Index Cond: (ticket_no = tf.ticket_no)

        Buffers: shared hit=106205

 Planning Time: 9.326 ms

 Execution Time: 675.836 ms




Bitmap Heap Scan on flightsHash JoinIndex Scan ... on ticket_flightsNested LoopIndex Only Scan ... on tickets





Dieser Plan kann als relativ gut bezeichnet werden, da eine verschachtelte Schleife eine relativ kleine Anzahl von Zeilen in der äußeren Menge verbindet.



Jetzt führen wir ein Experiment durch und sehen, wie sich der vorgeschlagene Plan abhängig von der Änderung der Daten in der Anfrage ändern wird (oder nicht). Daten werden so ausgewählt, dass der Zeilenbereich der Flugtabelle, der die Bedingung erfüllt, konsistent vergrößert wird, was zu einem Planungsfehler bei der Bewertung der Kardinalität des Zugriffs auf diese Tabelle führt. Im obigen Plan können Sie sehen, dass der Optimierer beim ersten Datum fast nicht in der Kardinalität verwechselt wird ( ). Ersetzen Sie die folgenden Daten in der Anfrage:Bitmap Heap Scan on flights f (rows=8331) (actual rows=7673)







  • 2017-04-01
  • 2017-01-01
  • 2016-08-01


Und sehen wir uns das Ergebnis an:



Abfragepläne ohne AQO
2017-04-01



Nested Loop (rows=31677) (actual rows=292392)

  Buffers: shared hit=991756

  ->  Hash Join (rows=31677) (actual rows=292392)

        Hash Cond: (tf.flight_id = f.flight_id)

        ->  Index Scan … on ticket_flights tf

              Index Cond: fare_conditions = 'Business')

        ->  Hash

              ->  Bitmap Heap Scan on flights f (rows=7673) (actual rows=70553)

                    Recheck Cond: scheduled_departure > '2017-04-01'

                    Filter: actual_arrival < (scheduled_arrival + '01:00:00'::interval)

                    ->  Bitmap Index Scan on ... [flights]

                          Index Cond: scheduled_departure > '2017-04-01'

                          Buffers: shared hit=160

  ->  Index Only Scan ... on tickets t ( rows=1 width=14) (actual rows=1 loops=292392)

        Index Cond: (ticket_no = tf.ticket_no)

        Buffers: shared hit=980995

 Planning Time: 5.980 ms

 Execution Time: 2771.563 ms



, . . , (Bitmap Heap Scan on flights f (rows=7673) (actual rows=70553)), , Nested Loop, , .



() — Flights , ( , ).



2017-01-01



Nested Loop (rows=187710) (actual rows=484569)

  Buffers: shared hit=1640723 read=49

  ->  Hash Join (rows=187738) (actual rows=484569)

        Hash Cond: (tf.flight_id = f.flight_id)

        ->  Index Scan ... on ticket_flights tf

              Index Cond: fare_conditions = 'Business'

        ->  Hash

              ->  Seq Scan on flights f (rows=45352) (actual rows=116985)

                    Filter: scheduled_departure > '2017-01-01'::date 

                              AND actual_arrival < scheduled_arrival + '01:00:00'::interval

  ->  Index Only Scan ... on tickets t (rows=1) (actual rows=1 loops=484569)

        Index Cond: (ticket_no = tf.ticket_no)

        Buffers: shared hit=1630118 read=49

 Planning Time: 6.225 ms

 Execution Time: 4498.741 ms



, . flights, ( ) .

tickets — (1 630 118).



2016-08-01



Hash Join (rows=302200) (actual rows=771441)

   Hash Cond: (t.ticket_no = tf.ticket_no)

   Buffers: shared hit=25499 read=34643

   ->  Seq Scan on tickets t (rows=2949857) (actual rows=2949857)

   ->  Hash

         ->  Hash Join (rows=302236) (actual rows=771441)

               Hash Cond: (tf.flight_id = f.flight_id)

               ->  Index Scan on ticket_flights tf

                     Index Cond: fare_conditions = 'Business'

               ->  Hash

                     ->  Seq Scan on flights f (rows=73005) (actual rows=188563)

                           Filter: scheduled_departure > '2016-08-01'::date) 

                                     AND actual_arrival < scheduled_arrival + '01:00:00'::interval

 Planning Time: 9.990 ms

 Execution Time: 3014.577 ms



((rows=302236) (actual rows=771441)). , , : Hash Join Nested Loop.



Zusammenfassend funktioniert der Planer ohne Verwendung des AQO-Moduls wie folgt:

Datum             Puffer Zeit, ms Ein Kommentar
2017-08-01   116 831       675,836 Es wird eine verschachtelte Schleife und ein Hash-Join verwendet. Flüge und Tickets werden nach Index gescannt
2017-04-01   991 756      2771,563 Der gleiche Plan, aber nicht optimal. Bei der Auswahl des Zugriffs nach Index für die Tabellen "Flüge" und "Tickets" wird festgestellt, dass der Planer bei der Berechnung der Kardinalität sehr falsch ist
2017-01-01 1,640,772      4498.741 Der gleiche suboptimale Plan. Der Planer beschließt jedoch, zu einem sequentiellen Scan der Flugtabelle zu wechseln.
2016-08-01       60142      3014,577 Der Plan hat sich endlich geändert - der Optimierer erkennt, dass er viele Zeilen aus den Tabellen auswählen muss, und wechselt daher zum sequentiellen Scannen der Flüge und Tickets. Eine ineffektive (in diesem Fall) verschachtelte Schleife wird durch einen Hash-Join ersetzt.
Abfragepläne mit AQO
AQO. :



SET aqo.mode = 'learn';



, :



2017-08-01



, , . AQO .



2017-04-01



Hash Join (rows=293891) (actual rows=292392)

  Hash Cond: (t.ticket_no = tf.ticket_no)

  Buffers: shared hit=25658 read=34640

  ->  Seq Scan on tickets t  (rows=2949857) (actual rows=2949857)

  ->  Hash

        ->  Hash Join  (rows=293734) (actual rows=292392)

              Hash Cond: (tf.flight_id = f.flight_id)

              ->  Index Scan ... on ticket_flights tf

                    Index Cond: (fare_conditions)::text = 'Business'::text

              ->  Hash

                    ->  Bitmap Heap Scan on flights f

                          Recheck Cond: scheduled_departure > '2017-04-01'::date

                          Filter: actual_arrival < scheduled_arrival + '01:00:00'::interval

                          ->  Bitmap Index Scan on ... [flights]

                                Index Cond: scheduled_departure > '2017-04-01'::date

                                Buffers: shared hit=160

 Planning Time: 9.652 ms

 Execution Time: 2218.074 ms



“”, AQO — . Tickets . . , AQO.



2017-01-01



Hash Join  (rows=484452) (actual rows=484569)

  Hash Cond: (t.ticket_no = tf.ticket_no)

  Buffers: shared hit=25534 read=34608

  ->  Seq Scan on tickets t (rows=2949857) (actual rows=2949857)

  ->  Hash (rows=484464) (actual rows=484569)

        ->  Hash Join (rows=484464) (actual rows=484569)

              Hash Cond: (tf.flight_id = f.flight_id)

              ->  Index Scan ... on ticket_flights tf

                    Index Cond: fare_conditions::text = 'Business'::text

              ->  Hash

                    ->  Seq Scan on flights f (rows=116971) (actual rows=116985)

                          Filter: scheduled_departure > '2017-01-01'::date

                                    AND actual_arrival < scheduled_arrival + '01:00:00'::interval

 Planning Time: 6.264 ms

 Execution Time: 2746.485 ms



— Flights .



2016-08-01



.



Schauen wir uns das Ergebnis noch einmal an:

Datum             Puffer Zeit, ms Ein Kommentar
2017-08-01   116 831      662,966 Der Plan ist der gleiche wie ohne Verwendung des Moduls
2017-04-01     60298    2218.074 Anhand der Modulhinweise versteht der Optimierer, dass eine große Anzahl von Zeichenfolgen zum Verbinden geplant ist, und verbessert bereits in diesem Schritt den Plan, indem die verschachtelte Schleife durch einen Hash-Join ersetzt wird
2017-01-01     60142    2746.485 Der Plan hat sich etwas verbessert - anstatt auf die Bitmap zur Flugtabelle zuzugreifen, wird deren sequentieller Scan verwendet
2016-08-01     60142    3253,861 Der Plan blieb unverändert - der beste Plan in diesem Fall
Wenn AQO aktiviert ist, erkennt der Scheduler schnell, dass er von einer verschachtelten Schleifenverknüpfung und Indexverwendung zu einer Hashverknüpfung und einem sequentiellen Scan wechseln muss.



Zusammenfassen



Die Verwendung des AQO-Moduls zur adaptiven Abfrageoptimierung hat sowohl Vor- als auch Nachteile.



Einer der Vorteile der Verwendung eines Moduls besteht darin, dass Sie abhängige Bedingungen in Abfragen nicht verfolgen müssen. In einigen Fällen kann sich die Geschwindigkeit der Abfrageausführung erhöhen. Es gibt verschiedene Modi für die Verwendung des Moduls. Mit AQO können Sie beispielsweise nur bestimmte Arten von Abfragen optimieren.



Unter den Nachteilen des Moduls können wir zusätzliche Gemeinkosten für die Schulung und das Speichern von Statistiken in den Modulstrukturen herausgreifen. Die vom Modul gesammelten statistischen Informationen werden nicht an Replikate übertragen.



Das AQO-Modul ist keine „Silberkugel“ aller möglichen Probleme des Schedulers. In einigen Situationen kann das Modul beispielsweise erweiterte Statistiken ersetzen (wenn es nicht von Hand erstellt wurde) oder irrelevante Statistiken nicht berücksichtigen. Das Modul erstellt jedoch nicht die erforderlichen Indizes und schreibt darüber hinaus den Abfragetext nicht neu.



Daher sollten Sie das Modul nicht für alle Anforderungen aktivieren. Ideale Optimierungskandidaten für AQO sind Abfragen, bei denen der Fehler des Planers bei der Berechnung der Kardinalität der Knoten zu einem schlechten Plan führt. Aus irgendeinem Grund ist es nicht möglich, die Verfeinerung dieser Schätzung zu beeinflussen.



All Articles