PostgreSQLLa base de données la plus sophistiquée au monde.
Documentation PostgreSQL 16.6 » Langage SQL » Requêtes parallélisées » Plans parallélisés

15.3. Plans parallélisés #

Comme chaque worker exécute la portion parallélisée du plan jusqu'à la fin, il n'est pas possible de prendre un plan de requête ordinaire et de l'exécuter en utilisant plusieurs workers. Chaque worker produirait une copie complète du jeu de résultats, donc la requête ne s'exécuterait pas plus rapidement qu'à la normale, et produirait des résultats incorrects. À la place, en interne, l'optimiseur considère la portion parallélisée du plan comme un plan partiel ; c'est-à-dire construit pour que chaque processus exécutant le plan ne génère qu'un sous-ensemble des lignes en sortie, et que chacune ait la garantie d'être générée par exactement un des processus participants. Habituellement, cela signifie que le parcours de la table directrice de la requête sera un parcours parallélisable (parallel-aware).

15.3.1. Parcours parallélisés #

Les types suivants de parcours de table sont actuellement parallélisables :

  • Lors d'un parcours séquentiel parallèle, les blocs de la table seront divisés en groupe et partagés entre les processus participant au parcours. Chaque processus terminera le parcours de son groupe de blocs avant de demander un groupe supplémentaire.

  • Lors d'un parcours de bitmap parallèle, un processus est choisi pour être le dirigeant (leader). Ce processus effectue le parcours d'un ou plusieurs index et construit un bitmap indiquant quels blocs de la table doivent être visités. Ces blocs sont alors divisés entre les processus participants comme lors d'un parcours d'accès séquentiel. En d'autres termes, le parcours de la table est effectué en parallèle, mais le parcours d'index associé ne l'est pas.

  • Lors d'un parcours d'index parallèle ou d'un parcours d'index seul parallèle, les processus participants lisent les données depuis l'index chacun à leur tour. Actuellement, les parcours d'index parallèles ne sont supportés que pour les index btree. Chaque processus réclamera un seul bloc de l'index, et scannera et retournera toutes les lignes référencées par ce bloc ; les autres processus peuvent être en train de retourner des lignes d'un bloc différent de l'index au même moment. Les résultats d'un parcours d'index parallèle sont retournés triés au sein de chaque worker parallèle.

Dans le futur, d'autres types de parcours pourraient supporter la parallélisation, comme les parcours d'index autres que btree.

15.3.2. Jointures parallélisées #

Tout comme dans les plans non parallélisés, la table conductrice peut être jointe à une ou plusieurs autres tables en utilisant une boucle imbriquée, une jointure par hachage ou une jointure par tri-fusion. Le côté interne de la jointure peut être n'importe quel type de plan non parallélisé par ailleurs supporté par l'optimiseur, pourvu qu'il soit sans danger de le lancer au sein d'un worker parallèle. Suivant le type de jointure, la relation interne peut aussi être un plan parallélisé.

  • Dans une boucle imbriquée, le côté interne n'est jamais parallèle. Bien qu'il soit exécuté intégralement, c'est efficace si le côté interne est un parcours d'index, car les enregistrements extérieurs sont partagés entre les processus d'aide, et donc aussi les boucles qui recherchent les valeurs dans l'index.

  • Dans une jointure par tri-fusion, le côté intérieur n'est jamais parallélisé et donc toujours exécuté intégralement. Ce peut être inefficace, surtout s'il faut faire un tri, car le travail et les résultats sont dupliqués dans tous les processus participants.

  • Dans une jointure par hachage (sans l'adjectif « parallélisée »), le côté intérieur est exécuté intégralement par chaque processus participant pour fabriquer des copies identiques de la table de hachage. Cela peut être inefficace si cette table est grande ou le plan coûteux. Dans une jointure par hachage parallélisée, le côté interne est un hachage parallèle qui divise le travail sur une table de hachage partagée entre les processus participants.

15.3.3. Agrégations parallélisées #

PostgreSQL procède à l'agrégation parallélisée en deux étapes. Tout d'abord, chaque processus de la partie parallélisée de la requête réalise une étape d'agrégation, produisant un résultat partiel pour chaque regroupement qu'il connaît. Ceci se reflète dans le plan par le nœud PartialAggregate. Puis les résultats partiels sont transférés au leader via le nœud Gather ou Gather Merge. Enfin, le leader réagrège les résultats partiels de tous les workers pour produire le résultat final. Ceci apparaît dans le plan sous la forme d'un nœud Finalize Aggregate.

Comme le nœud Finalize Aggregate s'exécute sur le processus leader, les requêtes produisant un nombre relativement important de groupes en comparaison du nombre de lignes en entrée apparaîtront comme moins favorables au planificateur de requêtes. Par exemple, dans le pire scénario, le nombre de groupes vus par le nœud Finalize Aggregate pourrait être aussi grand que le nombre de lignes en entrée traitées par les processus workers à l'étape Partial Aggregate. Dans de tels cas, au niveau des performances il n'y a clairement aucun intérêt à utiliser l'agrégation parallélisée. Le planificateur de requêtes prend cela en compte lors du processus de planification et a peu de chances de choisir un agrégat parallélisé sur ce scénario.

L'agrégation parallélisée n'est pas supportée dans toutes les situations. Chaque agrégat doit être à parallélisation sûre et doit avoir une fonction de combinaison. Si l'agrégat a un état de transition de type internal, il doit avoir des fonctions de sérialisation et de désérialisation. Voir CREATE AGGREGATE pour plus de détails. L'agrégation parallélisée n'est pas supportée si un appel à la fonction d'agrégat contient une clause DISTINCT ou ORDER BY ainsi que pour les agrégats d'ensembles triés ou quand la requête contient une clause GROUPING SETS. Elle ne peut être utilisée que si toutes les jointures impliquées dans la requête sont dans la partie parallélisée du plan.

15.3.4. Parallel Append #

Quand PostgreSQL a besoin de combiner des lignes de plusieurs sources dans un seul ensemble de résultats, il utilise un nœud Append ou MergeAppend. Cela arrive souvent en implémentant un UNION ALL ou en parcourant une table partitionnée. Ces nœuds peuvent être utilisés dans des plans parallélisés aussi bien que dans n'importe quel autre plan. Cependant, dans un plan parallélisé, le planificateur peut utiliser un nœud Parallel Append à la place.

Quand un nœud Append est utilisé au sein d'un plan parallélisé, chaque processus exécutera les plans enfants dans l'ordre où ils apparaissent, de manière à ce que tous les processus participants coopèrent pour exécuter le premier plan enfant jusqu'à la fin, et passent au plan suivant à peu près au même moment. Quand un Parallel Append est utilisé à la place, l'exécuteur va par contre répartir les processus participants aussi uniformément que possible entre ses plans enfants, pour que les multiples plans enfants soient exécutés simultanément. Cela évite la contention, et évite aussi de payer le coût de démarrage d'un plan enfant dans les processus qui ne l'exécutent jamais.

À l'inverse d'un nœud Append habituel, qui ne peut avoir des enfants partiels que s'il est utilisé dans un plan parallélisé, un nœud Parallel Append peut avoir à la fois des plans enfants partiels et non partiels. Les enfants non partiels seront parcourus par un seul processus, puisque les parcourir plus d'une fois provoquerait une duplication des résultats. Les plans qui impliquent l'ajout de plusieurs ensembles de résultat peuvent alors parvenir à un parallélisme « à gros grains » même si des plans partiels efficaces ne sont pas possibles. Par exemple, soit une requête sur une table partitionnée, qui ne peut être implémentée efficacement qu'en utilisant un index qui ne supporte pas les parcours parallélisés. Le planificateur peut choisir un Parallel Append de l'Index Scan habituel  chaque parcours séparé de l'index devra être exécuté jusqu'à la fin par un seul processus, mais des parcours différents peuvent être exécutés au même moment par des processus différents.

enable_parallel_append peut être utilisé pour désactiver cette fonctionnalité.

15.3.5. Conseils pour les plans parallélisés #

Si une requête ne produit pas un plan parallélisé comme attendu, vous pouvez tenter de réduire parallel_setup_cost ou parallel_tuple_cost. Bien sûr, ce plan pourrait bien se révéler plus lent que le plan sériel préféré par le planificateur, mais ce ne sera pas toujours le cas. Si vous n'obtenez pas un plan parallélisé même pour de très petites valeurs de ces paramètres (par exemple après les avoir définis tous les deux à zéro), le planificateur a peut-être une bonne raison pour ne pas le faire pour votre requête. Voir Section 15.2 et Section 15.4 pour des explications sur les causes possibles.

Lors de l'exécution d'un plan parallélisé, vous pouvez utiliser EXPLAIN (ANALYZE, VERBOSE) qui affichera des statistiques par worker pour chaque nœud du plan. Ce peut être utile pour déterminer si le travail est correctement distribué entre les nœuds du plan et plus généralement pour comprendre les caractéristiques de performance du plan.