De introductie van parallelle querymogelijkheden in PostgreSQL 9.6 bracht het Gather-knooppunt om resultaten van achtergrondwerkers samen te voegen in het leidersproces. Echter, het standaard Gather-knooppunt vernietigt elke tuple-volgorde die door de parallelle werkers wordt geproduceerd, waardoor een kostbare laatste Sort-stap in de leider nodig is om de volgorde opnieuw vast te stellen. Om deze redundantie te elimineren bij het verwerken van inherent gesorteerde gegevensstromen, introduceerde versie 10 het Gather Merge-knooppunt, dat een k-weg merge van gesorteerde invoer van werkers uitvoert, waarbij de behoefte aan materialisatie en sorteren aan de leidingzijde wordt omzeild.
De planner kiest ervoor om Gather Merge uitsluitend in te voeren wanneer het parallelle subplan de uitvoer garandeert die is geordend volgens een vereiste eigenschap, typisch gegenereerd door Index Scans of Merge Joins die de tuplevolgorde behouden. Als het subplan de volgorde verliest door operaties zoals Hash Joins of ongeordende aggregaties, wordt Gather Merge niet meer in aanmerking genomen, waardoor de optimizer gedwongen wordt te kiezen tussen een Gather gevolgd door een kostbare Sort of het volledig opgeven van parallelisme om de volgorde met een enkel proces te behouden.
Wanneer het subplan een geordende uitvoer garandeert, stelt Gather Merge de leider in staat om een streaming merge uit te voeren met minimale geheugenbuffers in plaats van alle tuples te materialiseren en te sorteren. De geheugenstrategie verschuift van een enkele grote toewijzing voor sorteren in de leider naar kleinere, per-werker onderhoud van gesorteerde runs, wat het risico van work_mem-uitputting en schijfspills tijdens grootschalige geordende ophalen aanzienlijk vermindert.
Ons team beheerde een tijdreeksanalyseplatform dat sensorwaarden opsloeg in een PostgreSQL-tabel gepartitioneerd op uur, met meer dan 2 miljard rijen. Een kritisch dashboard moest de laatste 1000 metingen uit alle partitions tonen, gesorteerd op timestamp in dalende volgorde, met een latentiebudget van minder dan 500 milliseconden. Het initiële single-threaded queryplan voldeed niet aan deze eisen, waardoor er een knelpunt ontstond in de gebruikerservaring tijdens piek analytische lasten.
Single-process Index Scan: We overwoogen aanvankelijk het gebruik van een achterwaartse Index Scan op elke partition, gevolgd door een Limit-knooppunt dat sequentieel werd uitgevoerd. Deze benadering bood implementatiegemak en deterministische ordening zonder complexe parallelle coördinatie. Het slaagde er echter niet in om de I/O-bandbreedte van onze NVMe-opslagarray te verzadigen en overschrijdde consequent 2 seconden tijdens piekbelasting, waardoor het onaanvaardbaar werd voor real-time dashboardupdates.
Parallel Seq Scan met Gather en Sort: De tweede aanpak bestond uit het inschakelen van max_parallel_workers_per_gather en het gebruik van een Parallel Seq Scan met een standaard Gather-knooppunt, dat alle rijen verzamelde in de leider voor een laatste Sort en Limit. Dit maakte gebruik van CPU-parallelisme en verbeterde de scanthroughput aanzienlijk. Desondanks zorgde het ervoor dat het leidersproces meer dan 4GB work_mem toewies om miljoenen rijen te sorteren, wat vaak leidde tot schijfspills en OutOfMemory-fouten op onze beperkte leidersnode, wat de stabiliteit van het systeem in gevaar bracht.
Parallel Index Scan met Gather Merge: We kozen uiteindelijk een plan waarbij werkers Parallel Index Scans uitvoerden in dalende volgorde van timestamp, die voeding gaven aan een Gather Merge-knooppunt. Werkers scanden indexbladpagina's in de vereiste volgorde en stroomden gesorteerde tuples naar de leider, die een lichte k-weg merge uitvoerde om de top 1000 rijen te extraheren. Deze architectuur elimineerde de noodzaak voor een laatste sortering in de leider, waardoor de geheugendruk aanzienlijk werd verminderd, terwijl de streamingefficiëntie behouden bleef.
We kozen de Gather Merge-benadering omdat deze uniek aan zowel de latentie- als geheugeneisen voldeed door gebruik te maken van de bestaande indexstructuur in plaats van ervoor te vechten met hash-gebaseerde operaties. Deze oplossing reduceerde de geheugenvoetafdruk van de leider tot onder de 64MB voor de merge-buffers en bereikte consistente responstijden van minder dan 300 ms. Het systeem kan nu pieklasten aan zonder geheugenuitputting, wat de architectonische keuze valideert om de volgorde te behouden door middel van parallelle uitvoering.
Waarom zorgt het plaatsen van een Hash Aggregate onder een Gather Merge-knooppunt ervoor dat de planner van PostgreSQL het plan afwijst of een expliciete Sort-stap invoegt, en hoe verschilt dit van het gedrag van GroupAggregate?
Hash Aggregate bouwt een ongeordende hashtabel om tuples te groeperen, wat inherent de inputsequentie die door onderliggende scans wordt geproduceerd vernietigt. Aangezien Gather Merge strikt geordende invoerstromen van alle parallelle werkers vereist om zijn streaming k-weg merge uit te voeren, blokkeert ongeordende uitvoer van aggregatie het directe gebruik ervan. Omgekeerd kan GroupAggregate werken met vooraf gesorteerde invoer en de tuple-volgorde behouden wanneer de sleutels voor GROUP BY overeenkomen met de sorteer volgorde, waardoor het compatibel is met Gather Merge zonder een tussentijdse sorteerstap te vereisen.
Hoe beïnvloedt de parallel_tuple_cost GUC de drempel waarboven de planner overschakelt van een Gather-plan naar een Gather Merge-plan bij het schatten van de kosten van het samenvoegen van gesorteerde stromen van acht parallelle werkers?
parallel_tuple_cost voegt een CPU-overhead per tuple toe voor het overdragen van rijen tussen parallelle werkers en het leidersproces. Voor Gather Merge is deze kosten iets hoger dan voor een standaard Gather-knooppunt vanwege de extra vergelijkingslogica die vereist is om de merge heap te behouden. Wanneer de geschatte resultaatset klein is, kan de planner de voorkeur geven aan een Gather-knooppunt in combinatie met een goedkope Sort in de leider boven Gather Merge, omdat de cumulatieve overhead van acht merge-stromen de kosten van het sorteren van een kleine batch tuples centraal kan overschrijden.
Welke specifieke beperking ontstaat er bij het gebruik van DECLARE CURSOR met de SCROLL-optie over een queryplan dat een Gather Merge-knooppunt bevat, en waarom zou de executor stilletjes de gehele resultaatset materialiseren ondanks de streamingnatuur van de merge?
SCROLL-cursors vereisen de mogelijkheid om achteruit door de resultaatset te bewegen, wat vereist dat rijen in work_mem worden gematerialiseerd of naar schijf worden gelekt om achteruit ophalen te ondersteunen. Hoewel Gather Merge efficiënt een streaming, geordende uitvoer produceert, dwingt de SCROLL-optie de executor om een Materialize-knooppunt boven de Gather Merge in te voegen om rijen te bufferen voor potentiële reverse traversie. Deze materialisatie verbruikt geheugen dat evenredig is aan de grootte van de resultaatset, waardoor de voordelen van geheugenefficiëntie van de streaming merge-strategie effectief worden tenietgedaan en mogelijk schijfspills kan veroorzaken die identiek zijn aan die vermeden zijn door aanvankelijk voor Gather Merge te kiezen.