Cuando una query va lenta, el primer instinto es añadir un Ãndice o reescribir el SQL. Pero sin saber qué está haciendo realmente el planificador, estás adivinando. EXPLAIN ANALYZE te muestra exactamente cómo ejecuta PostgreSQL tu consulta, cuánto tiempo tarda cada paso y dónde están los cuellos de botella.
EXPLAIN vs EXPLAIN ANALYZE
EXPLAIN muestra el plan estimado sin ejecutar la consulta. Usa las estadÃsticas de la tabla (actualizadas por VACUUM/ANALYZE) para estimar cuántas filas producirá cada nodo y qué coste tendrá. Es rápido pero solo muestra estimaciones.
EXPLAIN ANALYZE ejecuta la consulta de verdad y muestra los tiempos reales junto a las estimaciones. Esto es lo que necesitas para diagnosticar problemas reales. Ojo: si la query hace modificaciones (UPDATE, DELETE, INSERT), las ejecuta de verdad, asà que úsala dentro de una transacción que luego hagas rollback si no quieres afectar datos.
-- Plan estimado sin ejecutar EXPLAIN SELECT * FROM pedidos WHERE usuario_id = 42; -- Plan real con tiempos (ejecuta la query) EXPLAIN ANALYZE SELECT * FROM pedidos WHERE usuario_id = 42; -- Con mas detalle: buffers de cache, formato JSON... EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM pedidos WHERE usuario_id = 42;
Los nodos principales del plan
El plan de ejecución es un árbol de nodos. Cada nodo representa una operación, y los nodos hijos alimentan a los padres. Los más habituales son:
- Seq Scan: escaneo secuencial de toda la tabla, fila a fila. No usa Ãndice. En tablas pequeñas puede ser más rápido que un Index Scan, pero en tablas grandes es señal de que falta un Ãndice.
- Index Scan: usa un Ãndice para localizar filas y luego accede al heap (la tabla) para obtener el resto de columnas.
- Index Only Scan: usa un Ãndice y no necesita acceder al heap porque todas las columnas necesarias están en el Ãndice. El más eficiente.
- Bitmap Heap Scan: primero crea un bitmap de páginas a leer usando un Ãndice, y luego accede al heap en orden. Se usa cuando hay muchas filas a recuperar y el acceso aleatorio al heap serÃa demasiado costoso.
- Hash Join / Nested Loop / Merge Join: las tres estrategias de join. Hash Join construye una tabla hash de la relación más pequeña; Nested Loop itera por el exterior y busca en el interior; Merge Join necesita ambas relaciones ordenadas.
Cómo leer la salida
EXPLAIN ANALYZE SELECT u.nombre, COUNT(p.id) AS total FROM usuarios u LEFT JOIN pedidos p ON p.usuario_id = u.id GROUP BY u.nombre;
La salida tiene este aspecto para cada nodo:
HashAggregate (cost=1250.00..1280.00 rows=500 width=64)
(actual time=45.123..46.789 rows=482 loops=1)
-> Hash Left Join (cost=450.00..1100.00 rows=10000 width=32)
(actual time=12.345..38.901 rows=10000 loops=1)
En cada nodo hay dos lÃneas:
cost=inicio..total: coste estimado (en unidades arbitrarias). El inicio es cuándo empieza a devolver filas; el total es cuando termina.rows: filas estimadas.actual time=inicio..total: tiempos reales en milisegundos.rows=X loops=Y: filas reales devueltas y veces que se ejecutó el nodo (en joins el nodo interior se ejecuta una vez por fila exterior).
Las señales de alarma
Hay tres patrones que indican un problema claro:
- Seq Scan en tabla grande: si ves un Seq Scan sobre una tabla con millones de filas y hay un predicado WHERE, probablemente falta un Ãndice o el que existe no se está usando.
- rows estimadas muy diferentes de rows reales: si el planificador estima 10 filas y el plan real devuelve 100.000, las estadÃsticas están desactualizadas. Ejecuta
ANALYZE nombre_tablay vuelve a mirar. - loops muy altos en Nested Loop: si un nodo interior se ejecuta 50.000 veces, el join está haciendo un bucle sobre muchas filas. Quizás falta un Ãndice en la columna de join del lado interior.
Herramientas para visualizarlo
El texto plano de EXPLAIN puede ser difÃcil de leer en planes complejos. Hay varias herramientas online que lo visualizan como árbol interactivo. La más usada es explain.dalibo.com: pegas la salida de EXPLAIN (ANALYZE, FORMAT JSON) y te muestra el árbol con los nodos coloreados por coste.
Para usar el formato JSON:
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) SELECT * FROM pedidos WHERE usuario_id = 42;
Este artÃculo complementa directamente el de Ãndices en PostgreSQL de esta misma serie: crear el Ãndice correcto es un paso, pero verificar con EXPLAIN ANALYZE que el planificador lo usa es el paso siguiente. Y si tu query usa CTEs y sospechas que la materialización está afectando el rendimiento, el artÃculo sobre CTEs con WITH y WITH RECURSIVE explica el comportamiento de optimization fence que puede sorprenderte en el plan de ejecución.
Imagen: Pexels / cottonbro studio
