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).
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.
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.
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.
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é.
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.