AQO - Optimisation adaptative des requĂȘtes PostgreSQL

Lors de l'exĂ©cution de requĂȘtes, les SGBD modernes utilisent un modĂšle d'optimisation des coĂ»ts - basĂ© sur les coefficients stockĂ©s dans les fichiers de configuration et les statistiques collectĂ©es, le «coĂ»t» d'obtention et le volume des ensembles de lignes rĂ©sultants sont calculĂ©s. Lorsque les requĂȘtes sont rĂ©pĂ©tĂ©es, le coĂ»t et la sĂ©lectivitĂ© sont recalculĂ©s. Vous pouvez exĂ©cuter la requĂȘte et voir les valeurs rĂ©elles de ces paramĂštres, cependant, dans le processus de replanification (standard), l'optimiseur de SGBD n'utilise en aucune façon ces informations.



Mais que se passe-t-il si l'optimiseur enregistre les valeurs rĂ©elles de coĂ»t, de sĂ©lectivitĂ© et d'autres paramĂštres nĂ©cessaires pour exĂ©cuter une requĂȘte et, lors de sa nouvelle exĂ©cution, est guidĂ© non seulement par les statistiques collectĂ©es standard, mais Ă©galement par celles enregistrĂ©es aprĂšs l'exĂ©cution prĂ©cĂ©dente?



Cela s'appelle l'optimisation adaptative des requĂȘtes et est prometteur. Certains SGBD utilisent dĂ©jĂ  de telles technologies.



Société Postgres Professional travaillant depuis plusieurs années sur l'extension AQO pour PostgreSQL, qui implémente (sous une forme ou une autre) l'optimisation adaptative. Le travail est toujours en cours, mais il y a déjà quelque chose à tester.



Tout d'abord, examinons de plus prĂšs le domaine de l'optimisation des requĂȘtes.



Pourquoi le planificateur peut choisir un plan sous-optimal



La requĂȘte SQL peut ĂȘtre effectuĂ©e de diffĂ©rentes maniĂšres. Par exemple, lorsqu'il y a une jointure de deux tables, cela peut ĂȘtre fait de plusieurs maniĂšres diffĂ©rentes - en utilisant des boucles imbriquĂ©es, la fusion, le hachage. Plus il y a de tables qui participent Ă  la requĂȘte, plus il y a de variations dans leurs jointures. La tĂąche du planificateur est de choisir le plan d'exĂ©cution de la requĂȘte avec le coĂ»t le plus bas parmi de nombreuses variantes.



Comme déjà mentionné, dans leur travail, les planificateurs de nombreux SGBD utilisent des informations statistiques collectées automatiquement ou manuellement. Le planificateur calcule un coût estimé sur la base de ces statistiques.



En gĂ©nĂ©ral, les planificateurs de SGBD modernes fonctionnent bien dans la plupart des situations. Cependant, dans certains cas, le plan choisi peut ĂȘtre trĂšs loin d'ĂȘtre optimal.



Par exemple,le manque d'informations statistiques à jour conduit au fait que le planificateur est guidé dans son travail par des données (trÚs probablement) incorrectes sur le nombre de lignes dans les tables jointes. Une sous-estimation (ou surestimation) excessive de la cardinalité conduit au choix de méthodes non optimales d'accÚs aux données dans les tableaux.



Une autre raison importante pourrait ĂȘtre l' absence d'index requis . En l'absence d'index, le planificateur dispose d'un choix limitĂ© de mĂ©thodes d'accĂšs aux donnĂ©es.



Utilisation de conditions dĂ©pendantes (corrĂ©lĂ©es)peut Ă©galement affecter nĂ©gativement le fonctionnement du SGBD. Le planificateur (par dĂ©faut) suppose que toutes les conditions d'une requĂȘte sont indĂ©pendantes les unes des autres, c'est-Ă -dire que la valeur d'une condition n'affecte en aucune façon l'autre. Ce n'est pas toujours le cas. Si des conditions dĂ©pendantes sont utilisĂ©es (par exemple, code postal et ville), le planificateur calculera Ă©galement le mauvais coĂ»t et la cardinalitĂ© des connexions.



Le planificateur peut Ă©galement ĂȘtre affectĂ© par l' utilisation de fonctions dans l'environnement . Une fonction pour le planificateur est une «boĂźte noire», il ne sait pas combien de lignes la fonction renverra, ce qui peut Ă©galement entraĂźner un coĂ»t de plan erronĂ©.



Façons d'influencer le travail de l'ordonnanceur



Des statistiques à jour sont une condition indispensable au bon travail de l'ordonnanceur. Tout d'abord, assurez-vous que le systÚme est configuré pour collecter réguliÚrement des informations statistiques.


Il existe plusieurs façons de remĂ©dier aux situations dĂ©crites ci-dessus et d'aider le planificateur Ă  choisir les plans d'exĂ©cution de requĂȘte les plus optimaux.



Sans index, l'ordonnanceur n'a qu'une seule façon d'obtenir des donnĂ©es: l'analyse sĂ©quentielle des tables (et ce n'est pas toujours mauvais et coĂ»teux). Dans certains cas, la crĂ©ation des index nĂ©cessaires peut aider Ă  accĂ©lĂ©rer l'accĂšs aux donnĂ©es - vous n'avez pas Ă  analyser la table entiĂšre. Mais l'utilisation d'index (la recherche du nĂ©cessaire, la crĂ©ation, la maintenance) n'est pas gratuite. IdĂ©alement, ils devraient ĂȘtre utilisĂ©s exactement lĂ  oĂč ils sont nĂ©cessaires. Et oĂč ce n'est pas nĂ©cessaire - ne l'utilisez pas.



Lorsque vous utilisez des conditions de jointure corrĂ©lĂ©es dans des requĂȘtes, vous pouvez gĂ©nĂ©rer des statistiques Ă©tendues- «dites» clairement Ă  l'optimiseur que les conditions sont liĂ©es les unes aux autres. Pour ce faire, l'administrateur de base de donnĂ©es (ou le dĂ©veloppeur) doit bien connaĂźtre ses donnĂ©es et surveiller les conditions dĂ©pendantes dans les requĂȘtes, car le nombre de combinaisons de colonnes dĂ©pendantes est difficile Ă  prĂ©dire Ă  l'avance. Des statistiques avancĂ©es devront ĂȘtre crĂ©Ă©es manuellement pour chacune de ces options.



Lors de la création d'une fonction, vous pouvez spécifier le coût d'exécution approximatif et / ou une estimation du nombre de lignes renvoyées par la fonction. Dans la version 12 , il est devenu possible d'utiliser des fonctions d'assistance pour améliorer les estimations du planificateur basées sur des arguments. Il s'agit également d'une méthode manuelle, qui ne donne pas toujours le meilleur résultat.



Si tout le reste Ă©choue, vous pouvez rĂ©Ă©crire manuellement la demande, par exemple, en utilisant des vues matĂ©rialisĂ©es, des expressions de table communes (CTE). Ou pour clarifier les exigences du domaine et, Ă©ventuellement, rĂ©Ă©crire radicalement la logique de requĂȘte.



Et il y a une autre façon de « soupçon » le planificateur - l' optimisation des requĂȘtes d' adaptation ( une daptive q uery o ptimization). L'idĂ©e derriĂšre cette mĂ©thode est qu'aprĂšs l'exĂ©cution de la requĂȘte, les informations statistiques rĂ©elles sont stockĂ©es et, lorsque la requĂȘte donnĂ©e (ou similaire) est Ă  nouveau exĂ©cutĂ©e, l'optimiseur peut s'y fier.



Le SGBD Postgres Pro Enterprise est une extension pour l'optimisation adaptative des requĂȘtes appelĂ©e AQO... Cette extension est publiĂ©e sur github: github.com/postgrespro/aqo , vous pouvez l'essayer avec vanilla PostgreSQL, plus Ă  ce sujet ci-dessous.



Comment fonctionne le module



Le module AQO utilise l'apprentissage automatique dans son travail. Vous pouvez en savoir plus sur le principe de fonctionnement dans un article d'Oleg Ivanov Utilisation du machine learning pour augmenter la productivitĂ© de PostgreSQL et plus en dĂ©tail dans la prĂ©sentation Optimisation adaptative des requĂȘtes (rapport sur YouTube ).



L'essence de cette méthode est briÚvement décrite ci-dessous:



Pour estimer le coût, le planificateur a besoin d'une estimation de la cardinalité, et pour cela, à son tour, une estimation de la sélectivité des conditions est nécessaire.



Pour des conditions simples (telles que «attribut = constante» ou «attribut> constante»), l'ordonnanceur dispose d'un modÚle par lequel il estime la sélectivité. Pour ce faire, il utilise des informations statistiques: nombre de valeurs d'attributs uniques, histogrammes, etc.

Pour les conditions composées d'éléments simples utilisant des connecteurs logiques, le planificateur applique des formules facilement calculées:



  • sel (pas A) = 1 - sel (A)
  • sel (pas A) = 1 - sel (A)
  • sel (A et B) = sel (A) * sel (B)
  • sel (A ou B) = sel (pas (pas A et pas B)) = 1 - (1 - sel (A)) * (1 - sel (B))


Ces formules supposent l'indĂ©pendance (non-corrĂ©lation) des conditions A et B, en raison de laquelle nous obtenons des estimations incorrectes dans le cas oĂč cette hypothĂšse est violĂ©e.

AQO complique la formule: introduit son propre coefficient pour chaque condition simple. En utilisant l'apprentissage automatique (en utilisant la régression du plus proche voisin), AQO sélectionne ces coefficients de sorte que la sélectivité calculée par la formule corresponde le mieux à la sélectivité réelle qu'AQO a observée précédemment.



Pour cela, le module enregistre:



  • , ;
  • .


Dans ses travaux, l'AQO distingue les conditions jusqu'à des constantes. Cela permet de réduire la complexité du problÚme à résoudre, et d'ailleurs, dans la plupart des cas, l'information n'est toujours pas perdue: AQO ne «voit» pas la valeur de la constante, mais il «voit» la sélectivité de la condition.

La situation dans laquelle la perte se produit toujours est les conditions qui sont évaluées en tant que constante quelles que soient les valeurs spécifiques. Par exemple, pour certaines conditions, le planificateur ne peut pas faire d'estimations raisonnables et choisit une constante par défaut (par exemple, la sélectivité de la condition "expression1 = expression2" est toujours évaluée à 0,005 et "expression1> expression2" est toujours évaluée à 1/3).



Ainsi, AQO améliore l'estimation de la sélectivité des conditions complexes (et, par conséquent, l'estimation des coûts, ce qui peut conduire au choix d'un plan d'exécution plus adéquat).



Installation du module



Pour essayer les fonctionnalités du module sur PostgreSQL vanilla, vous devez utiliser un correctif spécial, puis construire le systÚme à partir des sources. Pour en savoir plus, lisez le fichier README sur github.



Si Postgres Pro Enterprise est utilisé, l'installation du module AQO se fera en mode standard:



shared_preload_libraries = 'aqo'



AprÚs cela, vous pouvez créer une extension dans la base de données requise.



Préparation de la base de données



Regardons un exemple concret du fonctionnement du module AQO dans une base de données de démonstration . Nous utiliserons une grande base de données contenant des informations sur les vols pour l'année de septembre 2016 à septembre 2017.



Commencez par créer une extension:



CREATE EXTENSION aqo;




Ensuite, dĂ©sactivez le traitement des requĂȘtes parallĂšles afin que l'affichage des plans parallĂšles ne distrait pas de la tĂąche principale:



max_parallel_workers_per_gather = 0;



pour que le planificateur PostgreSQL ait plus d'options pour joindre des tables, créez deux index:



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


Lors de l'analyse des rĂ©sultats, nous nous concentrerons sur la valeur de BUFFERS comme le nombre de pages Ă  lire pour faire le travail. Examinons Ă©galement le temps d'exĂ©cution (mais le temps sur un systĂšme chargĂ© et sur un ordinateur portable Ă  la maison peut ĂȘtre trĂšs diffĂ©rent).



Augmentons le cache tampon et work_mem pour que tout le travail soit effectué en RAM:



shared_buffers = '256MB';

work_mem = '256MB';




Utilisation du module AQO



Formulons une demande: vous devez obtenir des passagers qui ont volé en classe affaires à partir d'une certaine date et qui sont arrivés avec un retard ne dépassant pas une heure.

Nous exĂ©cutons la demande sans utiliser AQO (ci-aprĂšs, une partie des informations qui n’affectent pas la comprĂ©hension du fonctionnement du module a Ă©tĂ© supprimĂ©e des plans):



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';




Et voyons le plan rĂ©sultant: dans ce cas, le planificateur a considĂ©rĂ© le plan optimal, dans lequel, d'abord, en utilisant un scan bitmap ( ), nous obtenons un ensemble de lignes de la table des vols, que nous avons hachĂ© (un nƓud ) avec un ensemble de lignes de la table ticket_flights obtenu Ă  l'aide de l'index scan ( ). Le rĂ©sultat rĂ©sultant sera utilisĂ© comme l'ensemble de lignes externe pour la jointure de boucle imbriquĂ©e finale (nƓud ). L'ensemble interne de cette jointure est obtenu Ă  l'aide d'un balayage d'index exclusif de la table tickets ( ). L'opĂ©ration la plus «volumineuse» consiste Ă  obtenir un ensemble de lignes interne pour la boucle imbriquĂ©e - 106 205 tampons y sont lus.



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





Ce plan peut ĂȘtre qualifiĂ© de relativement bon, car une boucle imbriquĂ©e rejoint un nombre relativement faible de lignes dans l'ensemble externe.



Maintenant, faisons une expĂ©rience et voyons comment le plan proposĂ© changera (ou ne changera pas) en fonction du changement des dates de la demande. Les dates sont sĂ©lectionnĂ©es de maniĂšre Ă  augmenter sĂ©quentiellement la plage de lignes de la table Vols qui satisfont Ă  la condition, ce qui conduit Ă  une erreur de planification dans l'Ă©valuation de la cardinalitĂ© d'accĂšs Ă  cette table. Dans le plan ci-dessus, vous pouvez voir qu'avec la premiĂšre date, l'optimiseur ne se trompe presque pas dans la cardinalitĂ© ( ). Remplaçons une Ă  une les dates suivantes dans la requĂȘte:Bitmap Heap Scan on flights f (rows=8331) (actual rows=7673)







  • 01/04/2017
  • 01/01/2017
  • 01/08/2016


Et voyons le résultat:



Plans de requĂȘte sans 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.



Si vous collectez un bref résumé, sans utiliser le module AQO, le planificateur fonctionne comme suit:

Date             Tampons Temps, ms Un commentaire
01/08/2017   116 831       675.836 La boucle imbriquĂ©e et la jointure de hachage sont utilisĂ©es, les tables de vols et de billets sont analysĂ©es par index
01/04/2017   991 756      2771,563 Le mĂȘme plan, mais pas optimal. Lorsque vous choisissez l'accĂšs par index pour les tables Vols et Billets, vous pouvez voir que le planificateur fait une grosse erreur lors du calcul de la cardinalitĂ©
01/01/2017 1,640,772      4498.741 Le mĂȘme plan sous-optimal. Mais le planificateur dĂ©cide de faire une analyse sĂ©quentielle de la table des vols
01/08/2016       60 142      3014.577 Le plan a finalement changĂ© - l'optimiseur comprend qu'il devra sĂ©lectionner un grand nombre de lignes dans les tableaux, il continue donc Ă  analyser sĂ©quentiellement les tableaux des vols et des billets. La boucle imbriquĂ©e inefficace (dans ce cas) la remplace par une jointure de hachage.
Plans de requĂȘte avec 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



.



Regardons à nouveau le résultat:

Date             Tampons Temps, ms Un commentaire
01/08/2017   116 831      662.966 Le plan est le mĂȘme que sans utiliser le module
01/04/2017     60298    2218,074 En utilisant les conseils de module, l'optimiseur comprend qu'un grand nombre de chaĂźnes sont prĂ©vues pour ĂȘtre jointes, et dĂ©jĂ  Ă  cette Ă©tape amĂ©liore le plan en remplaçant la boucle imbriquĂ©e par une jointure par hachage
01/01/2017     60142    2746,485 Le plan s'est un peu amĂ©liorĂ© - au lieu d'accĂ©der au bitmap de la table des vols, son scan sĂ©quentiel est utilisĂ©
01/08/2016     60142    3253.861 Le plan est restĂ© inchangĂ© - le meilleur plan dans ce cas
Avec AQO activé, le planificateur se rend compte rapidement qu'il doit passer d'une jointure de boucle imbriquée et d'une utilisation d'index à une jointure par hachage et une analyse séquentielle.



RĂ©sumer



L'utilisation du module AQO pour l'optimisation adaptative des requĂȘtes prĂ©sente Ă  la fois des avantages et des inconvĂ©nients.



L'un des avantages de l'utilisation d'un module est que vous n'avez pas Ă  suivre les conditions dĂ©pendantes dans les requĂȘtes. Dans certains cas, la vitesse d'exĂ©cution des requĂȘtes peut augmenter. Et il existe diffĂ©rents modes d'utilisation du module. Par exemple, vous pouvez utiliser AQO pour optimiser uniquement certains types de requĂȘtes.



Parmi les inconvénients du module, on peut distinguer des frais généraux supplémentaires pour la formation et la sauvegarde des statistiques dans les structures du module. Et les informations statistiques collectées par le module ne sont pas transférées vers des répliques.



Le module AQO n'est pas une solution miracle contre tous les problĂšmes de planification possibles. Par exemple, dans certaines situations, le module peut remplacer les statistiques Ă©tendues (si vous ne les crĂ©ez pas Ă  la main), ou ne fera pas attention aux statistiques non pertinentes. Mais le module ne crĂ©era pas les index nĂ©cessaires et, de plus, ne rĂ©Ă©crira pas le texte de la requĂȘte.



Par consĂ©quent, vous ne devez pas activer le module pour toutes les demandes. Les candidats d'optimisation idĂ©aux pour AQO sont des requĂȘtes oĂč l'erreur du planificateur dans le calcul de la cardinalitĂ© des nƓuds se traduit par un mauvais plan. Et, pour une raison quelconque, il n'est pas possible d'influencer le raffinement de cette estimation.



All Articles