L'introduction des capacités de requête parallèle dans PostgreSQL 9.6 a amené le nœud Gather pour combiner les résultats des travailleurs de fond dans le processus principal. Cependant, le nœud Gather standard détruit tout ordre de tuple produit par les travailleurs parallèles, nécessitant une coûteuse étape de Sort finale dans le leader pour rétablir la séquence. Pour éliminer cette redondance lors du traitement de flux de données intrinsèquement ordonnés, la version 10 a introduit le nœud Gather Merge, qui effectue une fusion k-voies d'entrées triées provenant des travailleurs, évitant ainsi la nécessité de matérialiser et de trier tous les tuples du côté du leader.
Le planificateur choisit d'injecter Gather Merge exclusivement lorsque le sous-plan parallèle garantit une sortie ordonnée selon une propriété requise, généralement générée par des Index Scans ou des Merge Joins qui préservent la séquence des tuples. Si le sous-plan perd de l'ordre à travers des opérations comme des Hash Joins ou des agrégations non ordonnées, Gather Merge devient inéligible, forçant l'optimiseur à choisir entre un Gather suivi d'un coûteux Sort ou à abandonner complètement le parallélisme pour maintenir l'ordre avec un seul processus.
Lorsque le sous-plan garantit une sortie ordonnée, Gather Merge permet au leader d'effectuer une fusion en continu en utilisant des tampons de mémoire minimaux plutôt que de matérialiser et de trier tous les tuples. La stratégie de mémoire passe d'une seule grande allocation pour trier dans le leader à une maintenance plus petite par travailleur de courses triées, réduisant considérablement le risque d'épuisement de work_mem et de débordements sur disque lors de récupérations ordonnées à grande échelle.
Notre équipe gérait une plateforme d'analytique de séries temporelles stockant des lectures de capteurs dans une table PostgreSQL partitionnée par heure, contenant plus de 2 milliards de lignes. Un tableau de bord critique devait afficher les 1000 dernières lectures de toutes les partitions triées par timestamp en descendant, avec un budget de latence inférieur à 500 millisecondes. Le plan de requête initial à un seul fil n'a pas réussi à répondre à ces exigences, créant un goulot d'étranglement dans l'expérience utilisateur pendant les charges analytiques de pointe.
Scan d'index à processus unique : Nous avons d'abord envisagé d'utiliser un Index Scan en arrière sur chaque partition suivi d'un nœud Limit exécuté séquentiellement. Cette approche offrait une simplicité d'implémentation et un ordonnancement déterministe sans coordination parallèle complexe. Cependant, elle n'a pas réussi à saturer la bande passante d'E/S de notre réseau de stockage NVMe et a systématiquement dépassé 2 secondes pendant les charges de pointe, la rendant inacceptable pour des mises à jour en temps réel du tableau de bord.
Scan séquentiel parallèle avec Gather et Sort : La deuxième approche impliquait d'activer max_parallel_workers_per_gather et d'utiliser un Parallel Seq Scan avec un nœud Gather standard, collectant toutes les lignes dans le leader pour un Sort et un Limit finaux. Cela a tiré parti du parallélisme CPU et amélioré considérablement le débit du scan. Néanmoins, cela a causé au processus leader d'allouer plus de 4 Go de work_mem pour trier des millions de lignes, déclenchant fréquemment des débordements sur disque et des erreurs OutOfMemory sur notre nœud leader contraint, compromettant la stabilité du système.
Scan d'index parallèle avec Gather Merge : Nous avons finalement sélectionné un plan où les travailleurs ont effectué des Parallel Index Scans dans l'ordre descendant des timestamps, alimentant un nœud Gather Merge. Les travailleurs ont scanné les pages de feuilles de l'index dans la séquence requise, diffusant des tuples triés vers le leader, qui a effectué une légère fusion k-voies pour extraire les 1000 premières lignes. Cette architecture a éliminé le besoin d'un tri final dans le leader, réduisant considérablement la pression mémoire tout en maintenant l'efficacité de diffusion.
Nous avons choisi l'approche Gather Merge car elle satisfaisait à la fois les contraintes de latence et de mémoire en tirant parti de la structure d'index existante plutôt qu'en luttant avec des opérations basées sur des hachages. Cette solution a réduit l'empreinte mémoire du leader à moins de 64 Mo pour les tampons de fusion et a atteint des temps de réponse cohérents en dessous de 300 ms. Le système gère désormais des charges de pointe sans épuisement de mémoire, validant le choix architectural de préserver l'ordre par une exécution parallèle.
Pourquoi le placement d'un Hash Aggregate sous un nœud Gather Merge amène-t-il le planificateur de PostgreSQL à rejeter le plan ou à insérer une étape Sort explicite, et comment cela diffère-t-il du comportement de GroupAggregate ?
Hash Aggregate construit une table de hachage non ordonnée pour regrouper les tuples, ce qui détruit intrinsèquement toute séquence d'entrée produite par les scans sous-jacents. Puisque Gather Merge nécessite des flux d'entrée strictement ordonnés de tous les travailleurs parallèles pour effectuer sa fusion k-voies en continu, la sortie non ordonnée des agrégations bloque son utilisation directe. En revanche, GroupAggregate peut travailler sur des entrées pré-triées et préserver l'ordre des tuples lorsque les clés GROUP BY correspondent à l'ordre de tri, le rendant compatible avec Gather Merge sans nécessiter une étape de tri intermédiaire.
Comment le GUC parallel_tuple_cost influence-t-il le seuil auquel le planificateur passe d'un plan Gather à un plan Gather Merge lors de l'estimation du coût de fusion des flux triés de huit travailleurs parallèles ?
parallel_tuple_cost ajoute un coût CPU par tuple pour le transfert des lignes entre les travailleurs parallèles et le processus leader. Pour Gather Merge, ce coût est légèrement plus élevé que pour un nœud Gather standard en raison de la logique de comparaison supplémentaire requise pour maintenir le tas de fusion. Lorsque le jeu de résultats estimé est petit, le planificateur peut privilégier un nœud Gather associé à un Sort peu coûteux dans le leader plutôt qu'un Gather Merge, car le surcoût cumulatif de huit flux de fusion peut dépasser le coût de tri d'un petit lot de tuples de manière centralisée.
Quelle limitation spécifique se pose lors de l'utilisation de DECLARE CURSOR avec l'option SCROLL sur un plan de requête contenant un nœud Gather Merge, et pourquoi l'exécuteur pourrait-il matérialiser silencieusement l'ensemble du jeu de résultats malgré la nature de diffusion de la fusion ?
Les curseurs SCROLL nécessitent la possibilité de se déplacer en arrière à travers le jeu de résultats, ce qui nécessite de matérialiser les lignes dans work_mem ou de débordement sur disque pour soutenir la récupération arrière. Bien que Gather Merge produise un flux de sortie ordonné de manière efficace, l'option SCROLL oblige l'exécuteur à insérer un nœud Materialize au-dessus du Gather Merge pour mettre en tampon les lignes pour un éventuel passage inversé. Cette matérialisation consomme de la mémoire proportionnellement à la taille de l'ensemble de résultats, annulant effectivement les bénéfices d'efficacité mémoire de la stratégie de fusion en continu et pouvant potentiellement entraîner des débordements sur disque identiques à ceux évités en choisissant Gather Merge initialement.