{"id":190,"date":"2026-03-01T11:01:31","date_gmt":"2026-03-01T15:01:31","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=190"},"modified":"2026-03-03T21:03:40","modified_gmt":"2026-03-04T01:03:40","slug":"guia-practica-de-explain-analyze-en-postgresql","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/03\/guia-practica-de-explain-analyze-en-postgresql\/","title":{"rendered":"Entiende qu\u00e9 est\u00e1 haciendo realmente tu base de datos: Gu\u00eda pr\u00e1ctica de EXPLAIN ANALYZE en PostgreSQL"},"content":{"rendered":"\n<p>Si alguna vez has lanzado una consulta y te has quedado mirando la relojito de carga pensando \u201c\u00bfpero por qu\u00e9 tanto?\u201d, es hora de dejar de adivinar.<\/p>\n\n\n\n<p>Casi siempre que una query va lenta no es culpa de <a href=\"https:\/\/juredev.com\/blog\/2026\/02\/postgresql-como-hacer-que-consultas-en-tablas-de-millones-de-filas\/\">PostgreSQL<\/a>. Es que no sabemos qu\u00e9 est\u00e1 haciendo internamente.<\/p>\n\n\n\n<p>Y ah\u00ed es donde brilla <code><a href=\"https:\/\/www.postgresql.org\/docs\/current\/sql-explain.html\">EXPLAIN ANALYZE<\/a><\/code>.<\/p>\n\n\n\n<p>Es lo m\u00e1s parecido a abrir el cap\u00f3 del carro mientras el motor sigue encendido: ves exactamente qu\u00e9 pasa, cu\u00e1nto tarda cada parte y d\u00f3nde se est\u00e1 atascando todo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\u00bfQu\u00e9 hace exactamente <code>EXPLAIN ANALYZE<\/code>?<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>EXPLAIN ANALYZE SELECT ...<\/code><\/pre>\n\n\n\n<p>Son dos comandos en uno:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>EXPLAIN<\/code> &#8211;&gt; te ense\u00f1a el plan estimado que cre\u00f3 el optimizador (el planner). Es la apuesta te\u00f3rica de cu\u00e1l va a ser la forma m\u00e1s barata de ejecutar tu consulta.<\/li>\n\n\n\n<li><code>ANALYZE<\/code> &#8211;&gt; ejecuta de verdad la consulta y te a\u00f1ade datos reales:\n<ul class=\"wp-block-list\">\n<li>tiempos que tard\u00f3 cada paso<\/li>\n\n\n\n<li>cu\u00e1ntas filas se procesaron realmente<\/li>\n\n\n\n<li>cu\u00e1ntas veces se ejecut\u00f3 cada nodo (loops)<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<p>Resumiendo:<\/p>\n\n\n\n<p><code>EXPLAIN<\/code> = teor\u00eda<br><code>ANALYZE<\/code> = realidad<\/p>\n\n\n\n<p>Y en rendimiento\u2026 la realidad siempre gana.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Cuidado importante (no te lo saltes)<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>EXPLAIN ANALYZE DELETE FROM usuarios WHERE ...<\/code><\/pre>\n\n\n\n<p><strong>\u00a1Esto borra de verdad!<\/strong> Igual con UPDATE, INSERT \u2026 SELECT, etc.<\/p>\n\n\n\n<p>La forma segura de probar consultas de escritura es:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>BEGIN;\nEXPLAIN ANALYZE DELETE FROM usuarios WHERE email ILIKE '%@prueba.com';\nROLLBACK;<\/code><\/pre>\n\n\n\n<p>Siempre dentro de una transacci\u00f3n que puedas deshacer. Es un h\u00e1bito que te ahorra muchos sustos.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">C\u00f3mo leer el plan sin volverte loco<\/h2>\n\n\n\n<p>El plan de ejecuci\u00f3n es un \u00e1rbol invertido (se lee de abajo hacia arriba).<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Los nodos de abajo producen filas<\/li>\n\n\n\n<li>Los nodos de arriba las consumen \/ filtran \/ ordenan \/ agrupan<\/li>\n\n\n\n<li>El nodo de m\u00e1s arriba (el ra\u00edz) muestra el tiempo total real de toda la consulta<\/li>\n<\/ul>\n\n\n\n<p>Piensa en \u00e9l como una cadena de montaje: si una m\u00e1quina de abajo est\u00e1 lenta, todo el proceso se resiente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Los indicadores que realmente importan (m\u00edralos en este orden)<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Actual Time &#8211;&gt; el que manda<\/h3>\n\n\n\n<p>Aparece as\u00ed:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Actual Time=0.015..45.872<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Primer n\u00famero: tiempo hasta que el nodo empieza a devolver la primera fila<\/li>\n\n\n\n<li>Segundo n\u00famero: tiempo total hasta que termina de entregar todas las filas<\/li>\n<\/ul>\n\n\n\n<p>El segundo valor del nodo ra\u00edz es el tiempo real que tard\u00f3 tu consulta.<br>Si un nodo intermedio tiene un <code>actual time<\/code> muy alto &#8211;&gt; ah\u00ed est\u00e1 casi seguro tu problema.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Filas estimadas vs. filas reales (<code>Rows<\/code> vs <code>actual rows<\/code>)<\/h3>\n\n\n\n<p>Ejemplo cl\u00e1sico de alarma:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Rows=10000  (estimated)\nActual rows=12<\/code><\/pre>\n\n\n\n<p>El planner pens\u00f3 que ibas a sacar 10.000 filas\u2026 y solo salieron 12. Cuando las estimaciones fallan tanto, el plan que elige suele ser p\u00e9simo.<\/p>\n\n\n\n<p>Causas frecuentes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Estad\u00edsticas desactualizadas &#8211;&gt; <code>ANALYZE tabla<\/code>;<\/li>\n\n\n\n<li>Datos muy sesgados (90% de los valores son \u201cactivo\u201d, pero filtras \u201cinactivo\u201d)<\/li>\n\n\n\n<li>Correlaciones entre columnas que el planner no ve<\/li>\n\n\n\n<li>Funciones en el <code>WHERE<\/code> que impiden buenas estimaciones<\/li>\n<\/ul>\n\n\n\n<p>Muchas veces un simple <code>ANALYZE<\/code> arregla el desastre. Otras veces necesitas \u00edndices parciales, estad\u00edsticas extendidas o reescribir el <code>JOIN<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">3. Buffers \u2013 usa siempre <code>EXPLAIN (ANALYZE, BUFFERS)<\/code><\/h3>\n\n\n\n<p>Te dice si el cuello de botella es I\/O o CPU:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>shared hit<\/code> &#8211;&gt; la p\u00e1gina ya estaba en memoria (r\u00e1pido)<\/li>\n\n\n\n<li><code>shared read<\/code> &#8211;&gt; tuvo que leer del disco (lento)<\/li>\n\n\n\n<li><code>temp read\/write<\/code> &#8211;&gt; derram\u00f3 a disco (sorts o hash joins muy grandes)<\/li>\n<\/ul>\n\n\n\n<p>Patrones t\u00edpicos:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Mucho <code>shared read <\/code>&#8211;&gt; problema de I\/O &#8211;&gt; quiz\u00e1s falta \u00edndice o la tabla no cabe en memoria<\/li>\n\n\n\n<li>Casi todo <code>hit<\/code> y sigue lento &#8211;&gt; CPU, mala estrategia de join, o funciones caras en cada fila<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">4. Loops \u2013 el multiplicador silencioso<\/h3>\n\n\n\n<pre class=\"wp-block-code\"><code>Actual rows=1  Loops=45000<\/code><\/pre>\n\n\n\n<p>Esto significa que ese nodo se ejecut\u00f3 45.000 veces. Casi siempre es se\u00f1al de un <code>Nested Loop <\/code>mal elegido (el lado externo produce muchas filas y el interno no tiene \u00edndice r\u00e1pido).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Los tres sospechosos de siempre<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Seq Scan<\/h3>\n\n\n\n<p>Lee toda la tabla secuencialmente. <strong>No es malo por definici\u00f3n<\/strong>. Es la mejor opci\u00f3n si:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Vas a leer gran parte de la tabla<\/li>\n\n\n\n<li>La tabla es peque\u00f1a<\/li>\n\n\n\n<li>Un \u00edndice ser\u00eda m\u00e1s caro que leer todo<\/li>\n<\/ul>\n\n\n\n<p><strong>Es sospechoso cuando<\/strong>:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Esperas pocas filas<\/li>\n\n\n\n<li>La tabla tiene millones de registros<\/li>\n\n\n\n<li>Hay un filtro muy selectivo sin \u00edndice &#8211;&gt; ah\u00ed casi siempre es un error<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Index Scan vs Bitmap Index Scan<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Index Scan<\/strong> &#8211;&gt; perfecto para pocas filas. Va directo al grano.<\/li>\n\n\n\n<li><strong>Bitmap Index Scan<\/strong> &#8211;&gt; mejor cuando esperas muchas filas dispersas. Construye un mapa en memoria y luego lee bloques completos &#8211;&gt; menos saltos aleatorios en disco.<\/li>\n<\/ul>\n\n\n\n<p>El planner elige seg\u00fan cu\u00e1ntas filas cree que va a devolver.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Nested Loop<\/h3>\n\n\n\n<p>Muy com\u00fan en JOINs peque\u00f1os.<\/p>\n\n\n\n<p><strong>Funciona genial cuando:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>El lado izquierdo (outer) devuelve pocas filas<\/li>\n\n\n\n<li>Hay \u00edndice bueno en la tabla derecha (inner)<\/li>\n<\/ul>\n\n\n\n<p><strong>Se vuelve pesadilla cuando:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>El lado outer devuelve miles\/millones de filas<\/li>\n\n\n\n<li>No hay \u00edndice en el inner &#8211;&gt; se convierte en miles de Seq Scan<\/li>\n\n\n\n<li>Loops alt\u00edsimos<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Ejemplo real (de los que duelen)<\/h2>\n\n\n\n<p><strong>Antes \u2013 consulta lenta<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Seq Scan on transacciones  \n  (actual time=10.542..120.181 rows=50 loops=1)\n  Filter: (monto &gt; 5000)\n  Rows Removed by Filter: 999950<\/code><\/pre>\n\n\n\n<p>Qu\u00e9 pas\u00f3:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Ley\u00f3 ~1 mill\u00f3n de filas<\/li>\n\n\n\n<li>Descart\u00f3 999.950<\/li>\n\n\n\n<li>Solo 50 eran \u00fatiles<\/li>\n\n\n\n<li>Much\u00edsimo I\/O in\u00fatil<\/li>\n<\/ul>\n\n\n\n<p><strong>Despu\u00e9s \u2013 con \u00edndice en monto<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Index Scan using idx_transacciones_monto on transacciones  \n  (actual time=0.048..0.117 rows=50 loops=1)<\/code><\/pre>\n\n\n\n<p>Resultado: de ~120 ms &#8211;&gt; 0.12 ms<\/p>\n\n\n\n<p>De buscar una aguja en un pajar &#8211;&gt; ir directo a la aguja.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><\/h2>\n\n\n\n<p><code>EXPLAIN ANALYZE<\/code> no opina, no especula, no te vende humo: te muestra lo que de verdad ocurri\u00f3.<\/p>\n\n\n\n<p>Cuando una consulta va lenta, la respuesta casi siempre est\u00e1 en uno (o varios) de estos:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Diferencia grande entre filas estimadas y reales<\/li>\n\n\n\n<li>Nodos con <code>actual time<\/code> muy alto en la mitad del \u00e1rbol<\/li>\n\n\n\n<li>Muchos <code>shared read<\/code> o <code>temp write<\/code><\/li>\n\n\n\n<li>Loops rid\u00edculamente altos<\/li>\n<\/ul>\n\n\n\n<p>Antes de salir corriendo a crear \u00edndices \u00abpor si acaso\u00bb, mira el plan.<\/p>\n\n\n\n<p>Muchas veces el problema no es falta de \u00edndice, sino:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Estad\u00edsticas desactualizadas<\/li>\n\n\n\n<li>JOINs mal ordenados<\/li>\n\n\n\n<li>Estimaciones de cardinalidad rotas<\/li>\n\n\n\n<li>Funciones en columnas que impiden usar \u00edndices<\/li>\n\n\n\n<li>Filtros escritos de forma que confunden al planner<\/li>\n<\/ul>\n\n\n\n<p>Optimizar a ciegas es tirar dados. Optimizar con <code>EXPLAIN ANALYZE<\/code> es ingenier\u00eda de verdad.<\/p>\n\n\n\n<p>\u00bfCu\u00e1l fue la \u00faltima consulta lenta que tuviste? \u00bfYa le pasaste <code>EXPLAIN (ANALYZE, BUFFERS)<\/code>?<\/p>\n\n\n\n<p>Pru\u00e9balo hoy mismo en esa query que te tiene pensando como mejorarla\u2026 te sorprender\u00e1 lo que descubres. Sigamos codificando.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Si alguna vez has lanzado una consulta y te has quedado mirando la relojito de carga pensando \u201c\u00bfpero por qu\u00e9 tanto?\u201d, es hora de dejar de adivinar. Casi siempre que una query va lenta no es culpa de PostgreSQL. Es que no sabemos qu\u00e9 est\u00e1 haciendo internamente. Y ah\u00ed es donde brilla EXPLAIN ANALYZE. Es [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18],"tags":[20],"class_list":["post-190","post","type-post","status-publish","format-standard","hentry","category-guia","tag-sql"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/190","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=190"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/190\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=190"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=190"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=190"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}