La introducción de capacidades de consultas paralelas en PostgreSQL 9.6 trajo consigo el nodo Gather, que combina resultados de trabajadores en segundo plano en el proceso líder. Sin embargo, el nodo Gather destruye cualquier orden de tuplas producido por los trabajadores paralelos, lo que requiere un costoso paso final de Sort en el líder para restablecer la secuencia. Para eliminar esta redundancia al procesar flujos de datos inherentemente ordenados, la versión 10 introdujo el nodo Gather Merge, que realiza una fusión k-vía de entradas ordenadas de los trabajadores, evitando la necesidad de materializar y ordenar todas las tuplas del lado del líder.
El planificador decide inyectar Gather Merge exclusivamente cuando el subplan paralelo garantiza una salida ordenada de acuerdo con una propiedad requerida, que generalmente es generada por Index Scans o Merge Joins que preservan la secuencia de tuplas. Si el subplan pierde el orden a través de operaciones como Hash Joins o agregaciones desordenadas, Gather Merge se vuelve inelegible, lo que obliga al optimizador a elegir entre un Gather seguido de un costoso Sort o abandonar completamente el paralelismo para mantener el orden con un solo proceso.
Cuando el subplan garantiza una salida ordenada, Gather Merge permite al líder realizar una fusión de transmisión usando buffers de memoria mínimos en lugar de materializar y ordenar todas las tuplas. La estrategia de memoria cambia de una sola gran asignación para ordenar en el líder a un mantenimiento más pequeño por trabajador de ejecuciones ordenadas, reduciendo significativamente el riesgo de agotamiento de work_mem y derrames de disco durante recuperaciones ordenadas a gran escala.
Nuestro equipo gestionó una plataforma de análisis de series temporales que almacenaba lecturas de sensores en una tabla de PostgreSQL particionada por hora, que contenía más de 2 mil millones de filas. Un panel crítico requería mostrar las últimas 1000 lecturas en todas las particiones ordenadas por timestamp de forma descendente, con un presupuesto de latencia inferior a 500 milisegundos. El plan de consulta inicial de un solo hilo no cumplió con estos requisitos, creando un cuello de botella en la experiencia del usuario durante cargas analíticas máximas.
Índice de escaneo de un solo proceso: Inicialmente consideramos utilizar un Index Scan hacia atrás en cada partición seguido de un nodo Limit ejecutado secuencialmente. Este enfoque ofrecía simplicidad de implementación y un orden determinista sin coordinación paralela compleja. Sin embargo, no logró saturar el ancho de banda de E/S de nuestro arreglo de almacenamiento NVMe y consistentemente excedía 2 segundos durante la carga máxima, lo que lo hacía inaceptable para actualizaciones de panel en tiempo real.
Escaneo secuencial paralelo con Gather y Sort: El segundo enfoque implicó habilitar max_parallel_workers_per_gather y utilizar un Parallel Seq Scan con un nodo Gather estándar, recopilando todas las filas en el líder para un Sort y Limit finales. Esto aprovechó el paralelismo de la CPU y mejoró significativamente el rendimiento del escaneo. Sin embargo, hizo que el proceso líder asignara más de 4GB de work_mem para ordenar millones de filas, provocando con frecuencia derrames de disco y errores de OutOfMemory en nuestro nodo líder restringido, lo que comprometió la estabilidad del sistema.
Escaneo de índice paralelo con Gather Merge: Finalmente seleccionamos un plan donde los trabajadores realizaron Parallel Index Scans en orden de timestamp descendente, alimentando un nodo de Gather Merge. Los trabajadores escanearon páginas de hoja de índice en la secuencia requerida, transmitiendo tuplas ordenadas al líder, que realizó una fusión k-vía liviana para extraer las 1000 filas principales. Esta arquitectura eliminó la necesidad de un último orden en el líder, reduciendo drásticamente la presión sobre la memoria mientras mantenía la eficiencia de transmisión.
Elegimos el enfoque de Gather Merge porque satisfacía de manera única tanto las limitaciones de latencia como de memoria al aprovechar la estructura de índice existente en lugar de enfrentarse a ella con operaciones basadas en hash. Esta solución redujo la huella de memoria del líder a menos de 64 MB para los buffers de fusión y logró tiempos de respuesta consistentes por debajo de 300 ms. El sistema ahora maneja cargas máximas sin agotamiento de memoria, validando la elección arquitectónica de preservar el orden a través de una ejecución paralela.
¿Por qué colocar un Hash Aggregate debajo de un nodo Gather Merge causa que el planificador de PostgreSQL rechace el plan o inserte un paso de Sort explícito, y cómo difiere esto del comportamiento de GroupAggregate?
Hash Aggregate construye una tabla hash desordenada para agrupar tuplas, lo que destruye inherentemente cualquier secuencia de entrada producida por los escaneos subyacentes. Dado que Gather Merge requiere flujos de entrada estrictamente ordenados de todos los trabajadores paralelos para realizar su fusión k-vía por transmisión, la salida desordenada de la agregación bloquea su uso directo. Por el contrario, GroupAggregate puede operar sobre entradas preordenadas y preservar el orden de tuplas cuando las claves GROUP BY coinciden con el orden de clasificación, haciéndola compatible con Gather Merge sin requerir un paso de orden intermedio.
¿Cómo influye el GUC parallel_tuple_cost en el umbral en el que el planificador cambia de un plan Gather a un plan Gather Merge al estimar el costo de fusionar flujos ordenados de ocho trabajadores paralelos?
parallel_tuple_cost agrega un costo de CPU por tupla para transferir filas entre trabajadores paralelos y el proceso líder. Para Gather Merge, este costo es ligeramente más alto que para un nodo Gather estándar debido a la lógica de comparación adicional requerida para mantener el montón de fusión. Cuando el conjunto de resultados estimado es pequeño, el planificador puede favorecer un nodo Gather acoplado con un Sort barato en el líder sobre Gather Merge, porque el costo acumulado de ocho flujos de fusión puede exceder el costo de ordenar un pequeño lote de tuplas de forma central.
¿Qué limitación específica surge al usar DECLARE CURSOR con la opción SCROLL sobre un plan de consulta que contiene un nodo Gather Merge, y por qué podría el ejecutor materializar silenciosamente el conjunto de resultados completo a pesar de la naturaleza de transmisión de la fusión?
Los cursores SCROLL requieren la capacidad de moverse hacia atrás a través del conjunto de resultados, lo que requiere materializar filas en work_mem o derramar en disco para admitir la recuperación hacia atrás. Aunque Gather Merge produce una salida ordenada por transmisión de manera eficiente, la opción SCROLL obliga al ejecutor a insertar un nodo Materialize por encima del Gather Merge para almacenar en búfer filas para una posible navegación inversa. Esta materialización consume memoria proporcional al tamaño del conjunto de resultados, negando efectivamente los beneficios de eficiencia de memoria de la estrategia de fusión por transmisión y potencialmente causando derrames de disco idénticos a aquellos evitados al elegir inicialmente Gather Merge.