Novedades del planificador de consultas de Postgres 16
(citusdata.com)- PostgreSQL 16 agrega 10 mejoras al planificador/optimizador de consultas, ampliando las opciones de planes de ejecución para
DISTINCT, agregaciones, joins, funciones de ventana y consultas sobre tablas particionadas - En
SELECT DISTINCT, agregaciones conORDER BY/DISTINCTy procesamiento posterior aMerge Join, aprovecha de forma más activa las entradas parcialmente ordenadas para producir resultados con menos memoria que un ordenamiento completo - Memoize dentro de
UNION ALL, soporte paraRight Anti Joiny soporte de hash join paralelo en joinsFULL/RIGHTse enfocan en reducir el costo de consultas repetidas y de crear tablas hash grandes - Las funciones de ventana reducen el procesamiento innecesario de
RANGEy losWindowAggque deben ejecutarse hasta el final; algunas funciones ahora pueden detenerse anticipadamente según ciertas condiciones - Todas las mejoras están activadas por defecto, por lo que vale la pena comparar el
EXPLAINy los tiempos de ejecución de cargas de trabajo reales antes y después de actualizar a PostgreSQL 16
Alcance de las mejoras del planificador en PostgreSQL 16
- PostgreSQL 16 introduce varias mejoras en el planificador de consultas que permiten ejecutar muchas consultas SQL más rápido que en versiones anteriores de PostgreSQL
- Se explican con más detalle las mejoras del planificador incluidas en las notas de la versión de PG16, junto con comparaciones de la salida de
EXPLAINentre PG15 y PG16 y ejemplos de prueba reproducibles - Aquí, el planificador es el componente que en otras bases de datos relacionales suele llamarse optimizador
Optimización de ordenamiento y DISTINCT
-
Uso de Incremental Sort en
SELECT DISTINCT- Incremental Sort se agregó por primera vez en PostgreSQL 13 y reduce el costo ordenando solo las columnas restantes cuando el resultado ya está ordenado por las columnas iniciales
- El planificador de PostgreSQL 16 también considera Incremental Sort en consultas
SELECT DISTINCT - Por ejemplo, si hay un índice btree en la columna
ay se necesita el ordena, b, se puede obtener con el índice un resultado ordenado poray luego ordenar solobcada vez que cambia el valor dea - En el quicksort de PostgreSQL, ordenar varios grupos pequeños puede ser más eficiente que ordenar un solo grupo grande
- En la consulta de ejemplo, PG15 usó
HashAggregatey un escaneo secuencial, mientras que PG16 eligió el índicedistinct_test_a_idxeIncremental Sort Presorted Key: aen la salida de PG16 significa que se aprovechó una entrada ya ordenada pora- El método hash de PG15 derramó alrededor de 30 MB a disco, mientras que la memoria máxima de
Incremental Sorten PG16 fue de 26 KB - El tiempo de ejecución bajó de 414.226 ms en PG15 a 263.167 ms en PG16
-
Optimización de agregaciones con
ORDER BYoDISTINCT- En PostgreSQL 15 y anteriores, las funciones de agregación con cláusulas
ORDER BYoDISTINCTsiempre realizaban el ordenamiento dentro del nodoAggregate - El planificador de PostgreSQL 16 puede crear un plan de ejecución que suministre filas en el orden correcto al nodo
Aggregate, y el ejecutor omite el ordenamiento interno si la entrada ya está ordenada - En el ejemplo de
COUNT(DISTINCT b), tanto PG15 como PG16 usanGroupAggregateeIndex Only Scan, pero en la salida de PG15 aparecetemp read=4540 written=4560 - Esa E/S de archivos temporales es el resultado de que el ordenamiento implícito de PG15 se derramó a disco
- En la salida de PG16 no aparece esa E/S temporal, y el tiempo de ejecución mejora más del doble, de 302.693 ms en PG15 a 115.534 ms en PG16
- En PostgreSQL 15 y anteriores, las funciones de agregación con cláusulas
Mejoras en consultas repetidas y planes de join
-
Aplicación de Memoize dentro de
UNION ALL- El nodo de plan
Memoizese introdujo por primera vez en PostgreSQL 14 y funciona como una capa de caché entre unNested Loopparametrizado y su entrada interna - El planificador de PostgreSQL 16 considera usar
Memoizeincluso cuando hay una consultaUNION ALLdentro del lado interno de unNested Loopparametrizado - En el ejemplo, PG15 ejecutó
Append1 millón de veces, mientras que PG16 colocóMemoizeencima deAppend - El
Memoizede PG16 registróHits: 999990,Misses: 10,Memory Usage: 2kB - La cantidad de ejecuciones de
Appendbajó de 1 millón en PG15 a 10 en PG16 - El tiempo de ejecución mejoró aproximadamente 6 veces, de 1926.151 ms en PG15 a 282.120 ms en PG16
- El nodo de plan
-
Soporte para Right Anti Join
- En un
Hash JoindeINNER JOIN, normalmente conviene crear la tabla hash sobre la tabla más pequeña - Una tabla hash pequeña requiere menos trabajo de creación, es más amigable con la caché de CPU y también reduce la probabilidad de stalls de CPU esperando datos desde la memoria principal
- Antes de PostgreSQL 16, los
Anti Joinsiempre ponían la tabla mencionada enNOT EXISTSen el lado interno del join, por lo que podía ser necesario crear una tabla hash sobre la tabla más grande - PostgreSQL 16 soporta Right Anti Join, lo que permite hacer hash sobre el lado más pequeño de las dos tablas
- En el ejemplo, PG15 hizo hash sobre la tabla
largede 1 millón de filas y usó 6446 KB de memoria, mientras que PG16 hizo hash sobre la tablasmallde 100 filas y usó solo 12 KB - El tiempo de ejecución casi se redujo a la mitad, de 139.023 ms en PG15 a 77.076 ms en PG16
- En un
-
Hash join paralelo en joins FULL/RIGHT
- PostgreSQL 11 introdujo
Parallel Hash Join, en el que varios workers paralelos participan en la creación de una única tabla hash Parallel Hash Joinen PostgreSQL 16 soporta los tipos de joinFULLyRIGHT- Los planes de
FULL OUTER JOINyRight Jointambién pueden ejecutarse en paralelo - En el ejemplo de
FULL JOIN, PG15 usó un únicoHash Full Join, mientras que PG16 usóParallel Hash Full JoinyGather - En la salida de PG16 aparecen
Workers Planned: 1yWorkers Launched: 1 - El tiempo de ejecución se redujo considerablemente, de 220.677 ms en PG15 a 129.769 ms en PG16
- PostgreSQL 11 introdujo
Optimización de funciones de ventana
-
Omisión de procesamiento RANGE innecesario
- En funciones de ventana como
row_number(),rank(),dense_rank(),percent_rank(),cume_dist()yntile(), si la cláusula de ventana no tiene la opciónROWS, PostgreSQL usa por defecto la opciónRANGE - La opción
RANGEdebe revisar filas anteriores para encontrar filas pares con el mismo valor de ordenamiento, y puede volverse costosa si hay muchos valores iguales según elORDER BY - El comportamiento de las funciones anteriores no cambia según se especifique
ROWSoRANGE, pero antes de PostgreSQL 16 el ejecutor no podía distinguirlo y debía realizar la verificación de filas pares en todos los casos - El planificador de PostgreSQL 16 sabe qué funciones de ventana se ven afectadas por las opciones
ROWS/RANGEy le pasa información al ejecutor para omitir procesamiento innecesario - En el ejemplo
row_number() <= 10, PG15 leyó 50,410 filas desde el índice antes de detenerse, mientras que PG16 leyó solo 11 filas - PG16 aprovecha el hecho de que, cuando
row_numberllega a 11, ya no habrá más filas que cumplan la condición<= 10 - El tiempo de ejecución mejoró más de 500 veces, de 29.775 ms en PG15 a 0.058 ms en PG16
- En funciones de ventana como
-
Ampliación de la detención anticipada para funciones de ventana monótonamente crecientes
- PostgreSQL 15 permitió detener temprano la ejecución de
WindowAggcuando una condición de la cláusulaWHEREse volvía false una vez y ya no podía volver a ser true para ciertas funciones de ventana - PostgreSQL 16 amplía esta optimización a
ntile(),cume_dist()ypercent_rank() - En PostgreSQL 15 solo se aplicaba a
row_number(),rank(),dense_rank(),count()ycount(*) - En el ejemplo
percent_rank() <= 0.01, PG15 procesó la condición como unFilterde la subconsulta, yWindowAggprocesó las 50,000 filas - PG16 usa la misma condición como
Run Conditionpara detener anticipadamente la ejecución deWindowAgg - El tiempo de ejecución mejoró más de 4 veces, de 84.358 ms en PG15 a 19.454 ms en PG16
- PostgreSQL 15 permitió detener temprano la ejecución de
Tablas particionadas y manejo de DISTINCT trivial
-
Eliminación de LEFT JOIN en tablas particionadas
- Desde hace mucho, PostgreSQL puede eliminar un
LEFT JOINque no es necesario para la consulta y que tampoco puede duplicar filas - Antes de PostgreSQL 16 no se soportaba la eliminación de LEFT JOIN sobre tablas particionadas
- Esto se debía a que las tablas particionadas no tenían la prueba necesaria para determinar si las filas internas podían duplicar filas externas
- El planificador de PostgreSQL 16 aplica la optimización de eliminación de
LEFT JOINtambién a tablas particionadas - Esta optimización puede ser especialmente útil en vistas
- Porque, aunque una vista tenga muchas columnas, las consultas reales no siempre consultan todas las columnas
- En el ejemplo, el plan de PG15 incluye un join contra
part_tab, pero el plan de PG16 solo realiza un escaneo secuencial denormal_table
- Desde hace mucho, PostgreSQL puede eliminar un
-
Tratamiento de un DISTINCT con resultado determinado como Limit
- Si el planificador de PostgreSQL puede detectar que todas las filas tienen el mismo valor, puede omitir el nodo de plan para eliminar duplicados del resultado
- PostgreSQL 16 aprovecha que el resultado solo contiene el mismo valor cuando todas las columnas objetivo de
DISTINCTestán fijadas por condiciones de igualdad en la cláusulaWHERE, y lo procesa conLIMIT 1 - En la consulta de ejemplo
SELECT DISTINCT a,b,c FROM abc WHERE a = 5 AND b = 5 AND c = 5, cada columnaDISTINCTqueda restringida al mismo valor - PG15 lee todo el resultado y lo reduce a 1 fila con el operador
Unique - PG16 usa
Limity un escaneo secuencial para devolver solo 1 fila - El tiempo de ejecución mejoró más de 1200 veces, de 30.381 ms en PG15 a 0.025 ms en PG16
Mayor uso de Incremental Sort después de Merge Join
- Antes de PostgreSQL 16, cuando el planificador consideraba un
Merge Join, solo usaba el orden del join si coincidía exactamente con los requisitos de una operación superiorDISTINCT,GROUP BYuORDER BY - Esta regla no reflejaba lo suficiente que
Incremental Sortpuede aprovechar entradas parcialmente ordenadas en operaciones superiores - PostgreSQL 16 relaja la regla para considerar el orden de
Merge Join, pasando de “debe coincidir exactamente” a “al menos 1 columna inicial debe estar correctamente ordenada” - Con este cambio, el planificador puede usar Incremental Sort con más frecuencia para adaptar el resultado de
Merge Joina una operación superior- Incremental Sort aprovecha entradas parcialmente ordenadas y ordena en lotes pequeños, por lo que puede reducir el uso de memoria y la cantidad de comparaciones frente a un ordenamiento completo
- En el ejemplo, PG15 usó un
Sortcompleto después deMerge Join, mientras que PG16 usóIncremental Sort- La memoria máxima de
Incremental Sorten PG16 fue de 26 KB - El tiempo de ejecución bajó levemente, de 1010.738 ms en PG15 a 915.589 ms en PG16, y la memoria usada para ordenar se redujo mucho
- La memoria máxima de
Forma de aplicación y verificación en la práctica
- Las 10 mejoras del planificador de PostgreSQL 16 están activadas por defecto
- Cada optimización se aplica en todos los casos posibles o se elige de forma selectiva cuando el planificador considera que será útil
- Si estás usando una versión anterior de PostgreSQL, puedes ejecutar cargas de trabajo reales en PostgreSQL 16 para ver qué consultas se vuelven más rápidas
- Los comentarios sobre uso real se pueden compartir en la lista de correo pgsql-general@postgresql.org
1 comentarios
Opiniones de Hacker News
Sería excelente que el planificador de consultas de PostgreSQL pudiera replantear una consulta a mitad de la ejecución.
Las consultas patológicamente lentas suelen ocurrir porque el planificador no tiene la información necesaria sobre la distribución de los datos y estima mal el costo; fácilmente puede haber una diferencia de 1000 veces, como que el tiempo de ejecución sea de 1 segundo en vez de 1 ms.
Como las estadísticas de las tablas no pueden ser 100% precisas, estaría bueno que, después de iniciar la consulta, si el avance es más lento de lo esperado, se vuelva a alimentar al planificador con información actual de progreso, como la cantidad de páginas escaneadas y las tuplas coincidentes, para generar un plan nuevo.
Sin embargo, como PostgreSQL no genera todo el resultado y luego lo envía, sino que lo envía por streaming, cambiar el plan a mitad de camino exigiría rastrear los resultados ya enviados al cliente, lo que implica cambios grandes de infraestructura.
Además, el cliente también puede invertir la dirección a mitad de una consulta y volver a pedir los resultados anteriores en orden inverso, lo que aumenta aún más la complejidad.
Porque ni siquiera hay garantía de que el nuevo plan devuelva las mismas tuplas. Por ejemplo, si no hay
ORDER BY, como enSELECT * FROM table LIMIT 10, qué tuplas saldrán es no determinista.Tal vez sería más fácil poner X tuplas en una cola y empezar a enviarlas recién cuando la cola se llene. Una vez llena la cola, se consideraría que ya es demasiado tarde para replantear y se fijaría el plan actual.
El usuario podría ajustar X para aceptar más memoria y más latencia hasta la primera tupla, a cambio de ampliar el tiempo disponible para cambiar el plan.
La nueva consulta no podría simplemente saltarse los primeros N resultados, sino que tendría que contrastar cada fila ya enviada contra un registro previo.
Uso esta herramienta para visualizar consultas: https://explain.dalibo.com/
También existe https://www.pgexplain.dev/; antes su salida era menos buena, pero ahora ambas parecen similares.
Me pregunto si existe alguna herramienta para depurar planes de ejecución que ayude en este tipo de situaciones.
Las mejoras al planificador de consultas siempre son bienvenidas y son una parte muy importante de una base de datos. Claro que, por lo general, se notan más cuando no hacen lo que uno quiere.
Algo que personalmente me resultó bastante frustrante es el JIT en PostgreSQL moderno. Las heurísticas para decidir cuándo usarlo no parecen nada robustas.
Lo vi en una consulta típica generada por un ORM: la consulta en sí era simple, pero arrastraba muchas tablas mediante joins. Sin JIT terminaba en unos pocos milisegundos, pero JIT agregaba 1 a 1,5 segundos, volviéndola tremendamente lenta incluso con pocos datos.
Ahora sé que simplemente se puede desactivar JIT, pero para un usuario que todavía no descubre por qué es lento, puede arruinar mucho la impresión que se lleva de PostgreSQL. Me gusta PostgreSQL, pero dejar JIT activado por defecto me parece demasiado riesgoso.
En PG16 solo mira el costo total estimado del plan y no considera la cantidad de expresiones que hay que compilar.
Compilar unas pocas expresiones es rápido, pero si se consulta una tabla particionada con cientos de particiones y todas esas particiones entran en el plan, el compilador JIT termina teniendo mucho trabajo.
Tengo, junto con un colega, código para mejorar esto, pero por ahora no está claro si entrará en PG17.
Busqué discusiones sobre JIT en la lista de correo de PostgreSQL, pero no encontré una razón convincente.
En cargas de trabajo OLTP, lo correcto es desactivar JIT.
Y como no uso ORM, tampoco es solo por patrones de consulta extraños.
En cambio, la paralelización de consultas sí puede ser útil en la práctica y, sobre todo, rara vez causa daño.
Actualicé algunos paquetes con
apty, de repente, una consulta grande que corre cada 5 minutos empezó a fallar. Más precisamente, PostgreSQL cortaba silenciosamente la conexión a mitad de la ejecución de la consulta, sin dejar logs.Al probar manualmente con
EXPLAIN, comprobé que solo se rompía la variante de la consulta que usaba JIT; la que no lo usaba estaba bien. Al desactivar JIT, todo volvió a la normalidad.Me pregunto con qué frecuencia estos cambios tienen efecto en consultas reales. En especial, el cambio de “usar
Limiten vez deUniquepara implementarDISTINCTcuando sea posible” se siente como algo que solo aplicaría a consultas muy tontasMe pregunto si los desarrolladores de PostgreSQL tienen alguna fuente de información para evaluar eso
Si la mejora de DISTINCT hace que sea más robusto ante consultas malas, hay mucho que ganar. No va a arreglar todos los problemas, pero cualquier mejora es bienvenida
pgsql-hackersCoincido en que probablemente no se aplique con frecuencia, pero lo bueno es que detectar si era aplicable fue tan simple como comprobar si un puntero era
NULLLa detección es muy sencilla y en la mayoría de los casos no aplicará, pero cuando sí aplica puede dar una mejora de rendimiento considerable
No será un problema muy común, pero no me sorprendería que aparezca de vez en cuando
select distinct email from users where email = ?No creo que hubiera más de 100 filas con el mismo correo. La mayoría eran usuarios de prueba que probablemente podían borrarse, pero me estoy desviando del tema
Me gustaría que PostgreSQL tuviera un modo estricto para pruebas de aplicaciones. Un modo que, mirando solo la consulta en sí y sin depender de las estadísticas, devuelva un error si existe un índice con el que la consulta mejoraría asintóticamente pero ese índice no está presente
También sería bueno tener un comando
CREATE INDICES FORque cree esos índices para actualizaciones de la app, y un modo de creación automática de índices para uso interactivo y de desarrolloEn general, el sistema debería estar diseñado para que nunca ocurra una ejecución asintóticamente subóptima
No entiendo por qué no implementan hints
pg_hint_plan. El riesgo de los hints es que, aunque sean correctos al momento de escribirlos, pueden empeorar las cosas si cambia el tamaño de la tabla o el sesgo de los datosCuando vi discusiones sobre hints antes, recuerdo que no había una oposición general si la forma de hacerlo no ataba demasiado al planner y le permitía adaptarse a cambios en los datos subyacentes
Por ejemplo, en vez de especificar que cierto predicado coincide con 10 filas, indicar que hay una correlación entre dos columnas
https://news.ycombinator.com/item?id=2179433 (60 comentarios, 2011)
La postura oficial en la wiki de PostgreSQL está en https://wiki.postgresql.org/wiki/OptimizerHintsDiscussion
Su postura es que “no hay interés en los hints exactamente de la forma en que suelen implementarse en otras bases de datos”
Entre los problemas de los sistemas de hints existentes se mencionan la menor mantenibilidad del código de la aplicación, los obstáculos para actualizar, el fomento de malos hábitos en los DBA y que no se adaptan al crecimiento del tamaño de los datos
No quiero culparlos por esa postura, pero es frustrante cuando PostgreSQL elige un plan tonto y no se lo puede convencer de tomar una decisión razonable
Un amigo que es DBA de Microsoft para empresas medianas dijo que con PostgreSQL no se puede hacer trabajo serio. Incluso contó que le sorprendió enterarse de que PostgreSQL ni siquiera tenía planificador de consultas
Dejando la burla de lado por un momento, me pregunto si hay algo de verosimilitud en la afirmación más grande de que MSSQL puede manejar escalas para las que PostgreSQL no es adecuado. Mi intuición me dice que es una tontería, pero no soy DBA en absoluto
Han resuelto esto a fuerza de meter dinero y hardware, es decir, más dinero, hasta que el problema se arregla. Claro que también hay tecnología inteligente ahí, pero en el fondo se les ha dedicado mucha más ingeniería durante mucho tiempo
Pueden escalar horizontalmente más allá de lo que PostgreSQL puede hacer razonablemente
Dicho eso, PostgreSQL también se está poniendo al día, y podría decirse que MySQL/MariaDB siempre han tenido una historia decente en este frente. Las opciones de escalado horizontal siguen mejorando
Hoy ya es más fácil operar clústeres PostgreSQL de varios terabytes con pocas máquinas, manejar mucho tráfico y poner el “big data” en bases de datos más especializadas. La forma antigua de meter todo en MSSQL/Oracle puede estar algo pasada de moda
Lo que tu amigo quiso decir podría ser que PostgreSQL no tiene una forma de cachear o fijar planes de consulta. PostgreSQL vuelve a planificar cada sentencia, salvo que uses sentencias preparadas manualmente, y aun así eso solo funciona por conexión
MSSQL lleva mucho tiempo cacheando y reutilizando planes, por lo que el planificador puede dedicar más tiempo a elaborar el plan. Además, tiene hints y permite fijar planes
PostgreSQL realmente necesita hints. Aunque el optimizador sea excelente, a veces yo sé más y quiero obligarlo a que me haga caso
Además, PostgreSQL no tiene verdaderos índices clustered y todas las tablas son heap. En MSSQL se usan mucho en la mayoría de los casos, normalmente se configura la clave primaria como índice clustered, de modo que la tabla misma se convierte en el índice y no hay indirección en las búsquedas por clave
Curiosamente, SQLite es lo contrario: las tablas siempre tienen un índice clustered, lo crees o no, mientras que MSSQL te deja elegir entre heap y tabla organizada por índice
Hay casos de bases de datos PostgreSQL muy grandes que funcionan bien, así que PostgreSQL definitivamente puede escalar
Dicho eso, SQL Server tiene funciones que PostgreSQL no tiene, y si esas funciones son importantes, puede encajar mejor en ciertos casos de uso. Al final son bases de datos distintas, con fortalezas y debilidades diferentes
Al principio iba a escribir que habría recomendado a la empresa migrar a PostgreSQL si no fuera por algunas aplicaciones de proveedores que requieren SQL Server
Pero luego me di cuenta de cuánto trabajo sería reemplazar cosas que Microsoft incluye, como reporting services, integration services, jobs, integración con AD y service broker.
notify/listenno tiene tipos de mensajeYa no usamos analysis services, pero cuando lo usábamos, también habría sido difícil de reemplazar
Estas cosas son las que te mantienen atado. Ni siquiera tengo idea de cuánto tardaría reemplazar todo esto, y gastar un año reemplazando lo que ya tienes no da un buen retorno de inversión
Me pregunto por qué esto se publicó en citusdata y no en postgresql.org. No sé si es una función exclusiva de pago o un agregado open source
¿Cuándo podremos usar índices para acelerar consultas con
IS NOT DISTINCT FROM? ;)