L'introduzione delle capacità di query parallele in PostgreSQL 9.6 ha portato il nodo Gather a combinare i risultati dai lavoratori di background nel processo principale. Tuttavia, il nodo Gather standard distrugge qualsiasi ordinamento delle tuple prodotto dai lavoratori paralleli, necessitando un costoso passaggio finale di Sort nel leader per ristabilire la sequenza. Per eliminare questa ridondanza quando si elaborano flussi di dati intrinsecamente ordinati, la versione 10 ha introdotto il nodo Gather Merge, che esegue una fusione k-way di input ordinati dai lavoratori, bypassando la necessità di materializzazione e ordinamento dal lato del leader.
Il pianificatore decide di iniettare Gather Merge esclusivamente quando il sottopiano parallelo garantisce l'output ordinato secondo una proprietà richiesta, tipicamente generata da Index Scans o Merge Joins che preservano la sequenza delle tuple. Se il sottopiano perde l'ordinamento attraverso operazioni come Hash Joins o aggregazioni non ordinate, Gather Merge diventa inidoneo, costringendo l'ottimizzatore a scegliere tra un Gather seguito da un costoso Sort o abbandonare completamente il parallelismo per mantenere l'ordine con un singolo processo.
Quando il sottopiano garantisce un output ordinato, Gather Merge consente al leader di eseguire una fusione in streaming utilizzando buffer di memoria minimi piuttosto che materializzare e ordinare tutte le tuple. La strategia di memoria passa da una grande allocazione unica per l'ordinamento nel leader a una manutenzione più piccola per lavoratore delle esecuzioni ordinate, riducendo significativamente il rischio di esaurimento di work_mem e fuoriuscite su disco durante recuperi ordinati su larga scala.
Il nostro team gestiva una piattaforma di analisi di serie temporali che archiviava letture di sensori in una tabella PostgreSQL suddivisa per ora, contenente oltre 2 miliardi di righe. Un dashboard critico richiedeva di mostrare le ultime 1000 letture attraverso tutte le partizioni ordinate per timestamp in ordine decrescente, con un budget di latenza inferiore a 500 millisecondi. Il piano di query iniziale a singolo thread non riusciva a soddisfare questi requisiti, creando un collo di bottiglia nell'esperienza dell'utente durante i picchi di carico analitico.
Single-process Index Scan: Inizialmente, abbiamo considerato di utilizzare una scansione backward Index Scan su ciascuna partizione seguita da un nodo Limit eseguito sequenzialmente. Questo approccio offriva semplicità di implementazione e ordinamento deterministico senza coordinazione parallela complessa. Tuttavia, non riusciva a saturare la larghezza di banda I/O della nostra array di archiviazione NVMe e superava costantemente i 2 secondi durante i carichi di picco, rendendolo inaccettabile per aggiornamenti in tempo reale del dashboard.
Parallel Seq Scan con Gather e Sort: Il secondo approccio ha comportato l'abilitazione di max_parallel_workers_per_gather e l'utilizzo di una Parallel Seq Scan con un nodo Gather standard, raccogliendo tutte le righe nel leader per un finale Sort e Limit. Questo ha sfruttato il parallelismo della CPU e migliorato significativamente il throughput della scansione. Tuttavia, ha fatto sì che il processo leader allocasse oltre 4 GB di work_mem per ordinare milioni di righe, attivando frequentemente fuoriuscite su disco e errori di OutOfMemory nel nostro nodo leader vincolato, compromettendo la stabilità del sistema.
Parallel Index Scan con Gather Merge: Alla fine, abbiamo selezionato un piano in cui i lavoratori eseguivano Parallel Index Scans in ordine decrescente di timestamp, alimentando un nodo Gather Merge. I lavoratori hanno scansionato le pagine foglia dell'indice nella sequenza richiesta, trasmettendo tuple ordinate al leader, che ha eseguito una leggera fusione k-way per estrarre le prime 1000 righe. Quest'architettura ha eliminato la necessità di un ordinamento finale nel leader, riducendo drasticamente la pressione sulla memoria mantenendo l'efficienza dello streaming.
Abbiamo selezionato l'approccio Gather Merge perché soddisfaceva unicamente sia i vincoli di latenza che di memoria sfruttando la struttura dell'indice esistente piuttosto che combatterci con operazioni basate su hash. Questa soluzione ha ridotto l'impronta di memoria del leader a meno di 64 MB per i buffer di fusione e ha raggiunto tempi di risposta costantemente inferiori a 300 ms. Il sistema ora gestisce carichi di picco senza esaurimento di memoria, convalidando la scelta architettonica di preservare l'ordinamento attraverso l'esecuzione parallela.
Perché posizionare un Hash Aggregate sotto un nodo Gather Merge fa sì che il pianificatore di PostgreSQL rifiuti il piano o inserisca un passaggio di Sort esplicito, e in che modo questo differisce dal comportamento di GroupAggregate?
Hash Aggregate costruisce una tabella hash non ordinata per raggruppare le tuple, il che distrugge inerentemente qualsiasi sequenza di input prodotta dalle scansioni sottostanti. Poiché Gather Merge richiede flussi di input strettamente ordinati da tutti i lavoratori paralleli per eseguire la sua fusione k-way in streaming, l'output non ordinato dall'aggregazione blocca il suo uso diretto. Al contrario, GroupAggregate può operare su input pre-ordinati e preservare l'ordinamento delle tuple quando le chiavi GROUP BY corrispondono all'ordine di ordinamento, rendendolo compatibile con Gather Merge senza richiedere un passaggio di ordinamento intermedio.
In che modo il GUC parallel_tuple_cost influenza la soglia alla quale il pianificatore passa da un piano Gather a un piano Gather Merge nel stimare il costo della fusione di flussi ordinati da otto lavoratori paralleli?
parallel_tuple_cost aggiunge un sovraccarico CPU per tupla per il trasferimento delle righe tra lavoratori paralleli e il processo leader. Per Gather Merge, questo costo è leggermente superiore rispetto a un nodo Gather standard a causa della logica di confronto aggiuntiva necessaria per mantenere il montante di fusione. Quando il set di risultati stimato è piccolo, il pianificatore potrebbe favorire un nodo Gather insieme a un Sort economico nel leader rispetto a Gather Merge, poiché l'onere cumulativo di otto flussi di fusione può superare il costo di ordinare un piccolo gruppo di tuple centralmente.
Quale specifica limitazione sorge quando si utilizza DECLARE CURSOR con l'opzione SCROLL su un piano di query contenente un nodo Gather Merge, e perché l'esecutore potrebbe materializzare silenziosamente l'intero set di risultati nonostante la natura di streaming della fusione?
I cursori SCROLL richiedono la possibilità di muoversi all'indietro attraverso il set di risultati, il che richiede la materializzazione delle righe in work_mem o la fuoriuscita su disco per supportare il recupero all'indietro. Sebbene Gather Merge produca un output ordinato e in streaming in modo efficiente, l'opzione SCROLL costringe l'esecutore a inserire un nodo Materialize sopra il Gather Merge per memorizzare le righe per un potenziale attraversamento inverso. Questa materializzazione consuma memoria proporzionale alla dimensione del set di risultati, negando effettivamente i benefici di efficienza della memoria della strategia di fusione in streaming e potenzialmente causando fuoriuscite su disco identiche a quelle evitate scegliendo inizialmente Gather Merge.