Prompts para Optimizar Queries SQL Lentas con IA
10 prompts para encontrar y optimizar queries lentas con IA: EXPLAIN, índices, N+1, planes de ejecución y optimización en PostgreSQL y MySQL.
Tabla de contenidos
La query del listado de pedidos funcionaba bien con mil filas. Cuando la tabla pasó a tres millones, el endpoint pasó de 40 ms a once segundos. El código no cambió ni una línea. El EXPLAIN muestra un Seq Scan sobre 3.200.000 filas y un Sort que se lleva casi todo el tiempo.
Ese es el día que llega en todos los proyectos. La base de datos crece, el volumen de datos deja de ser un detalle y aparece un número en el panel que nadie sabe explicar. Y aquí está lo incómodo: saber SQL no significa saber leer un plan de ejecución. Casi todo el mundo sabe escribir la query, muy poca gente sabe leer cómo la ejecuta de verdad el motor.
Lo que la IA hace muy bien con las queries lentas es algo que a los humanos les cuesta: leer un EXPLAIN ANALYZE de cuarenta líneas y decirte exactamente dónde se van los ocho de los once segundos. También es buena diagnosticando la causa raíz (un N+1 camuflado, una función sobre la columna indexada, un OFFSET en la página 4000) cuando le pasas los datos suficientes.
Y aquí viene el requisito que importa: sin el plan de ejecución no hay nada. Una IA a la que solo le dices “esta query va lenta” te va a recomendar crear un índice, que es lo que ya hiciste y por eso sigues igual. Los prompts de este artículo empiezan por darte las herramientas para reunir los datos correctos, y solo después llegan a las soluciones.
La base de prompts del blog está en los 20 mejores prompts para programar con IA, y si quieres ir por el diagnóstico antes que por la query, en los 15 mejores prompts para depurar código hay varios que se aplican directamente.
Antes de pegar el prompt: el kit de datos que hay que tener
Una query lenta no se optimiza con intuición, se optimiza con datos. Si pegas cualquier cosa, recibes superstición con formato SQL.
- La salida completa de
EXPLAIN (ANALYZE, BUFFERS), no un resumen. El campoactual timees el que te dice dónde se va el tiempo real, y elloopsexplica por qué una fila que parece barata se multiplica. - El DDL de las tablas:
CREATE TABLEcon sus índices, constraints y tipos exactos. Sin esto la IA no puede razonar sobre cardinalidad. - El número real de filas de cada tabla. Un índice sobre una tabla de 500 filas no cambia nada aunque la query lo use.
- El ORM o cliente que lanza la query, si la query viene de Prisma, Hibernate o un ORM equivalente. El origen del N+1 casi siempre está ahí.
- Qué motor y qué versión: PostgreSQL 17 y MySQL 8 tienen planificadores distintos. El mismo texto de plan se lee diferente.
Si tienes el stack que uso en Prisma desde cero, el punto 4 es el que más se olvida, y es justo donde se esconden las peores sorpresas.
Leer el plan de ejecución
1. Lectura guiada del EXPLAIN
Base de datos: [PostgreSQL 17 / MySQL 8.4].
Query:
[pegar query]
EXPLAIN (ANALYZE, BUFFERS) — salida completa:
[pegar plan completo, sin recortar]
DDL de las tablas e índices implicados:
[pegar CREATE TABLE y CREATE INDEX]
Filas reales por tabla: [tabla = N filas].
Explícame el plan nodo a nodo:
1. Qué hace cada nodo del árbol (en lenguaje llano)
2. Cuánto tiempo real se va en cada uno, con los milisegundos
3. Qué nodos son un problema y por qué (seq scan, sort, nested loop, rows descartados)
4. La fila estimada frente a la real: si difieren mucho, hay un problema de estadística
5. Qué cambiarías primero y por qué ese cambio es el de mejor ratio
No me des índices todavía. Primero el diagnóstico.
Por qué funciona: pedir nodo a nodo con los milisegundos obliga a que la IA lea el plan en lugar de recitar reglas genéricas sobre índices. Y el paso de “no me des índices todavía” evita que salte a la respuesta fácil.
Ejemplo real de por qué el plan no se lee solo:
Seq Scan on orders (cost=0.00..15532.11 rows=250000 width=64)
(actual time=0.04..214.88 rows=248913 loops=1)
Filter: (status = 'paid' AND created_at > '2025-01-01')
Rows Removed by Filter: 951087
Buffers: shared hit=240 read=9121
Planning Time: 0.12 ms
Execution Time: 9132.44 ms
Lo importante no es que hay un Seq Scan: es que rows=250000 estimados frente a 248913 reales acertan, así que no es un problema de estadísticas. Y que Rows Removed by Filter: 951087 dice que el filtro descarta el 79% de las filas después de leerlas. Eso apunta directo a un índice sobre (status, created_at) o al menos (created_at). Un modelo que solo mira “Seq Scan” te dice “pon un índice” sin decir cuál ni por qué.
2. Tabla de catálogo del plan
| Nodo del plan | Qué hace | Cuándo es un problema | Qué mirar |
|---|---|---|---|
| Seq Scan | Lee toda la tabla | Tablas grandes con filtro poco selectivo | Rows Removed by Filter alto |
| Index Scan | Recorre el índice y accede a la fila | Casi nunca | El número de Heap Fetches (lecturas a la tabla) |
| Index Only Scan | Todo sale del índice | No: es lo mejor, si el índice cubre todo | Que el índice incluya las columnas del SELECT |
| Bitmap Index Scan + Bitmap Heap Scan | Acceso aleatorio ordenado | Muchas filas devueltas, pocas coincidencias | La relación entre bits leídos y filas |
| Nested Loop | Bucle: por cada fila de fuera, recorre la de dentro | Con muchas filas de entrada | loops grande y filas de entrada no estimadas |
| Hash Join | Join por tabla hash en memoria | Se va a memoria o hace spill a disco | Batches mayor de 1 = spill a disco |
| Merge Join | Join ordenado | Ya tiene los datos ordenados | Casi nunca es el culpable |
| Sort | Ordena el resultado | Sort Method: external merge | Si hay external, se va a disco |
| Limit | Corta filas | No | Si hay loops alto, mira los nodos hijos |
Por qué funciona: esta tabla es el mapa mental que hace falta para leer el plan. Con ella, la IA puede hablar el mismo idioma que tú y responder preguntas concretas en vez de repetirte el plan entero.
Índices: los que faltan y los que sobran
3. Índice compuesto con el orden de columnas correcto
Query:
[pegar query con WHERE, ORDER BY y LIMIT]
DDL de la tabla:
[pegar]
Índices actuales:
[pegar]
Datos: [N filas totales, cuántas cumple el filtro, cuántas por página]
Dame la definición de los índices que faltan. Para cada uno:
- Columnas exactas en el orden exacto, con el porqué de ese orden
- Por qué ese orden y no el inverso (regla de igualdad, rango, orden)
- Qué queries cubre
- Cuánto costará mantenerlo en los INSERT
Incluye el caso "no hace falta ningún índice nuevo" si es la respuesta correcta.
Añade el SELECT a la lista de columnas si una Index Only Scan es viable.
Por qué funciona: el orden de columnas en un índice compuesto no es un detalle menor, es casi todo el diseño. La regla (igualdad, luego rango, luego orden) es la que decide si el índice se usa o no, y la IA la aplica bien si le das el WHERE y el ORDER BY por separado.
Ejemplo real del caso que más se repite:
-- ❌ Rango antes que igualdad: el índice no sirve para el filtro
CREATE INDEX idx_orders ON orders (created_at, status);
-- Query: WHERE status = 'paid' AND created_at > '2025-01-01'
-- El índice ordena por created_at, así que para cada fecha tendría que recorrer
-- TODAS las filas con ese status. El motor acaba prefiriendo un Seq Scan.
-- ✅ Igualdad primero, luego rango
CREATE INDEX idx_orders ON orders (status, created_at);
Ejemplo real de lo que hace un índice bien colocado:
-- Con índice sobre (status, created_at):
-- Index Scan using idx_orders on orders
-- Index Cond: (status = 'paid' AND created_at > '2025-01-01')
-- Rows Removed by Filter: 0 <-- ya no se descartan 951.087 filas
-- Execution Time: 8.31 ms
4. Detectar índices inútiles que ralentizan los INSERT
Motor: [PostgreSQL 17].
Uso real de índices (últimos 30 días):
[pegar pg_stat_user_indexes con idx_scan, o el equivalente en MySQL]
Volumen de escrituras: [INSERT/UPDATE/DELETE por minuto en esas tablas]
Identifica:
1. Índices con idx_scan = 0 hace más de [N] días: candidatos a borrar
2. Índices duplicados o que uno es prefijo de otro (redundantes)
3. Índices que cubren casi lo mismo que otro: cuál borrar
4. Coste estimado en escritura de cada índice
Para cada uno: el SQL para borrarlo y qué riesgo real tiene.
Incluye los índices de primary key y unique aunque su uso sea 0.
Por qué funciona: los índices inútiles son el coste oculto. Nadie los ve porque no ralentizan las lecturas: ralentizan las escrituras, ocupan espacio y ensucian el plan. Esta es la auditoría que la gente pospone y siempre encuentra algo.
Los problemas funcionales que matan los índices
5. Funciones sobre columnas indexadas
Query lenta:
[pegar query]
Índices existentes:
[pegar]
Datos: [ejemplo real de valor, p.ej. fecha '2025-03-14']
Revisa cada predicado de la WHERE y dime:
1. ¿Alguna función envuelve una columna indexada? -> el índice queda inutilizable
2. ¿Hay un cast implícito o una comparación entre tipos distintos?
3. ¿Hay un OR entre columnas que impide el uso del índice?
4. ¿Hay un LIKE '%algo' que impide el índice?
Para cada problema: la versión indexable equivalente que da exactamente
el mismo resultado, con su SQL, y el índice que necesitaría.
Verifica que la semántica no cambia (nulos, fechas, zonas horarias).
Por qué funciona: WHERE DATE(created_at) = '2025-03-14' es un clásico: la función se aplica antes de comparar, así que el índice no sirve. La solución (created_at >= '2025-03-14' AND created_at < '2025-03-15') parece un detalle y puede ser la diferencia entre 200 ms y 9 segundos.
6. LEFT JOIN frente a INNER JOIN
Query:
[pegar]
Modelo de datos: [pegar CREATE TABLE]
Distribución real: [cuántas filas de la tabla derecha, cuántas con NULL]
Resultado esperado por negocio: [qué filas debe devolver sí o sí]
Analiza cada JOIN del plan y dime:
1. Qué tipo de join ha elegido el motor y por qué
2. Cuántas filas produce en cada paso del árbol
3. Qué cambia si convierto los LEFT JOIN en INNER JOIN
4. Cuántas filas se descartan por el join en cada variante
5. Si el LEFT JOIN es realmente necesario o es un LEFT JOIN por costumbre
Si el LEFT JOIN no es necesario, dame las dos versiones con sus diferencias
en número de filas para el dataset real.
Por qué funciona: el LEFT JOIN por costumbre es uno de los desperdicios más caros que hay, porque obliga al planificador a elegir un join más caro aunque los filtros de la derecha siempre descarten alguna fila. La IA puede razonar bien si le das la distribución real de los datos.
N+1: el problema que no se ve en el plan
7. Detectar y eliminar N+1
ORM / cliente: [Prisma 6 / Hibernate 6 / Knex / SQLAlchemy].
Endpoint: [pegar código de la ruta o del servicio].
Logs de queries para UNA petición:
[pegar las queries repetidas]
Detecta el patrón N+1 y dame:
1. Cuántas queries se ejecutan y cuántas deberían (con el número)
2. Cuál es la consulta principal y dónde se lanza el N+1
3. La versión corregida con eager loading / DataLoader / join
4. El SQL que genera cada versión, para comparar el plan
5. Qué pasa con el N+1 anidado (3 niveles = 1 + N + N² queries)
6. Cuál es el riesgo de la corrección: datos más grandes de lo esperado
No uses includes indiscriminados: si eso trae tablas que no necesito, dilo.
Por qué funciona: el N+1 no aparece en ningún plan de ejecución porque cada query es rápida por separado. Solo se ve contando repeticiones. Y la corrección tiene una trampa real (el eager loading carga demasiados datos), que el prompt pide explícitamente controlar.
Ejemplo real del antes y después, con el impacto medido:
// ❌ N+1: 1 + N queries. Con 200 pedidos, 201 queries en paralelo
const orders = await prisma.order.findMany({ take: 200 });
return orders.map((o) => ({
...o,
customer: await prisma.customer.findUnique({ where: { id: o.customerId } }),
items: await prisma.orderItem.findMany({ where: { orderId: o.id } }),
}));
// → 401 queries, ~1.8 s
// ✅ 3 queries con include. Mismo dato, 1/200 del tiempo
const orders = await prisma.order.findMany({
take: 200,
include: { customer: true, items: { where: { qty: { gt: 0 } } } },
});
// → 3 queries, ~12 ms
Paginación y agregaciones
8. Paginación sin OFFSET
Query actual:
[pegar con LIMIT y OFFSET]
Número total de páginas en producción: [N]
Página que más tarda: [número y milisegundos]
Datos: [N filas totales]
Índices: [pegar]
Analiza la paginación y dame:
1. Cuánto trabajo hace el motor en cada OFFSET (explica el mecanismo)
2. La versión keyset/seek con WHERE y el índice que necesita
3. Cómo implementarlo cuando hay varios criterios de orden (empate incluido)
4. Cómo mantener el "ir a la página N" sin volver a OFFSET
5. Cuándo compensa realmente cada enfoque, con números
Incluye el coste de memoria del enfoque antiguo si el ORDER BY es grande.
Por qué funciona: casi nadie mide el problema con datos concretos. Con el número de páginas reales y los milisegundos, la IA puede decirte “tu OFFSET no es un problema todavía” o “lo es y desde la página 800”, y eso decide si cambias hoy o en tres meses.
9. COUNT contra EXISTS
Comparo dos queries sobre la misma tabla:
A) [pegar con COUNT]
B) [pegar con EXISTS]
Datos: [N filas, cuántas cumplen la condición]
Contexto: la query se llama [diaria por usuario / por cada request de API]
Analiza:
1. Cuál de las dos puede cortarse antes y por qué
2. Si COUNT(*) sin WHERE sobre tabla grande usa sequential scan
3. Cuándo es aceptable usar COUNT y cuál es el límite real
4. La versión con una tabla de contadores precalculados si el volumen lo pide
5. Diferencia entre EXISTS, COUNT y LIMIT 1 en coste real
Dame la recomendación con el coste en ms estimado para nuestro volumen.
Por qué funciona: EXISTS corta en el primer match y COUNT recorre todo. Con una condición que coincide en el 1% de las filas la diferencia es enorme; si coincide en el 99%, se invierte. Sin saber la distribución no se puede recomendar, y el prompt la pide.
10. Cuando la tabla crece demasiado: particionamiento
Tabla: [nombre].
Datos actuales: [N filas, tamaño total].
Proyección a 12 meses: [N filas].
Patrón de acceso: [qué consultas se hacen, con qué filtro y qué orden]
Crece por inserción: [solo INSERT o también UPDATE/DELETE]
Dime:
1. ¿Necesita particionamiento hoy, en 6 meses o nunca? Con los números
2. Si lo necesita: particionado por rango (fecha) o hash (id) y por qué
3. Cuál es el beneficio exacto en tu caso, no en general
4. Qué se rompe: índices, claves foráneas, autovacuum, consultas sin clave
5. Qué NO mejora (a menudo se espera que sí)
6. El coste de mantenimiento: más tablas, más índices, migraciones
Solo propón particionado si los números lo justifican. Dime si la respuesta
honesta es "no lo necesitas todavía".
Por qué funciona: el particionamiento es la respuesta que todo el mundo da y casi nadie usa bien. Este prompt está diseñado para que la respuesta correcta sea a menudo “todavía no”, que es la que de verdad te ahorra el mes de trabajo.
Escala con ORM y el día a día
11. Optimización sin tocar el SQL (capa de aplicación)
Query lenta identificada: [pegar].
La query viene de: [Prisma / Hibernate / TypeORM / SQLAlchemy].
Código de la aplicación:
[pegar el repository/service donde se ejecuta]
Antes de tocar el SQL, dime qué se puede ganar en la capa de aplicación:
1. Select solo los campos necesarios (nada de SELECT *)
2. Evitar N+1 con eager loading o DataLoader (ya visto)
3. Cachear con TTL corto para lecturas repetidas
4. Paginación por defecto en el repository para que nadie haga SELECT * sin límite
5. Batch operations donde el ORM hace N inserts
Para cada uno: el antes y el después en código, y el impacto esperado.
Dime también qué NO se puede arreglar en esta capa.
Por qué funciona: la mitad de las optimizaciones no son de SQL. Pedir explícitamente la capa de aplicación evita que toda la conversación termine en “añade un índice”, que no siempre es la respuesta.
12. Verificar que la optimización funcionó
Optimización aplicada: [pegar diff].
Plan antes: [pegar].
Plan después: [pegar].
Mismo dataset, [número de filas].
Verifica la optimización:
1. ¿El plan cambió de verdad o solo el tiempo medido?
2. ¿El índice nuevo se usa o el motor lo ignoró?
3. ¿Apareció un problema nuevo (índice bloated, es decir, con muchas columnas que nunca se consultan)?
4. ¿La mejora se mantiene al crecer los datos, según los números del plan?
5. ¿Qué métrica concreta hay que vigilar en producción para confirmar?
6. ¿Hay riesgo de que esta query se use en un camino caliente que se degrade?
Por qué funciona: es tentador declarar victoria porque el tiempo bajó un 90%. Comparar los dos planes y proyectar el comportamiento con más datos es lo que separa una optimización real de un accidente afortunado con pocos datos.
Anti-patrones de optimización con IA
❌ Aceptar índices indiscriminados
-- ❌ Lo que sale de "hazla más rápida"
CREATE INDEX idx_1 ON orders(status);
CREATE INDEX idx_2 ON orders(created_at);
CREATE INDEX idx_3 ON orders(user_id);
CREATE INDEX idx_4 ON orders(total);
-- 4 índices que ralentizan los INSERT y probablemente 3 no se usan
-- ✅ Un índice pensado para una query concreta, con el orden justificado
CREATE INDEX idx_orders_status_created_at ON orders (status, created_at DESC);
❌ Optimizar la sintaxis sin medir
-- ❌ Reescribir la query porque "parece más eficiente"
SELECT u.name, u.email, u.created_at, u.last_login
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE o.total > 100;
-- ✅ Medir primero: quizá el problema era el índice que faltaba
❌ Optimizar un caso y romper el otro
// ❌ Optimizar el listado a costa de la página de detalle
const orders = await prisma.order.findMany({
include: { items: { include: { product: true } } }, // siempre todo
});
// ✅ Elegir la query según lo que pide cada endpoint
const listQuery = prisma.order.findMany({ select: orderListSelect });
const detailQuery = prisma.order.findUnique({ include: orderDetailInclude });
❌ Dar por hecho que las filas estimadas del EXPLAIN son la realidad
-- ❌ Con filas estimadas lejos de las reales, el plan puede estar equivocado
ANALYZE orders; -- a veces las estadísticas están caducadas
-- ✅ Comprobar que estimado vs real no diverge
EXPLAIN (ANALYZE, BUFFERS) SELECT ...; -- mira rows estimadas vs (actual rows=...)
Errores frecuentes y cómo salir de ellos
- “El
EXPLAINdice que usa el índice pero sigue lento”: miraactual timeen cada nodo, nocost. El plan es correcto pero el índice está fragmentado o la tabla tiene muchísimos dead tuples:VACUUM FULLo reconstruye. - “PostgreSQL no usa mi índice”: mira las filas estimadas. Si el planificador cree que devuelve un millón de filas, prefiere el Seq Scan porque piensa que el índice le costará más.
ANALYZEla tabla y comprueba. - “Añado el índice y los INSERT se rompen”: es el coste esperado. Compruébalo con
pg_stat_user_indexesa una semana; si no se usa, bórralo. En tablas de solo lectura no te afecta. - “La query es rápida en local y lenta en producción”: casi siempre es que no hay índices de índice; local tiene 200 filas y el optimizador elige un plan distinto. Compara los dos planes, no los dos tiempos.
- “La IA me recomienda cosas para otra versión de PostgreSQL”: incluye siempre la versión exacta.
MERGE, índices parciales y las diferencias del planificador cambian de comportamiento entre 13 y 17. - “No encuentro la query lenta”: sin query log no hay optimización posible. Actívalo una tarde, deja correr el tráfico real y trabaja con datos, no con intuiciones.
El flujo completo
┌────────────────────────────────────────────────────────────┐
│ 1. Medir: EXPLAIN (ANALYZE, BUFFERS) + DDL + nº de filas │
│ Sin datos reales no hay optimización, hay suerte │
└────────────────────────────┬───────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 2. Prompt 1: leer el plan nodo a nodo (diagnóstico) │
└────────────────────────────┬───────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 3. ¿Es N+1? → Prompt 7 (contar queries, no leer planes) │
│ Si no, sigue ↓ │
└────────────────────────────┬───────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 4. ¿Falta índice? → Prompt 3 (orden de columnas) │
│ ¿Índices inútiles? → Prompt 4 (auditoría) │
└────────────────────────────┬───────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 5. ¿Función sobre columna o LEFT JOIN? → Prompts 5 y 6 │
└────────────────────────────┬───────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 6. ¿Paginación o agregaciones? → Prompts 8 y 9 │
│ ¿La tabla ya no cabe? → 10 (particionar, o no) │
└────────────────────────────┬───────────────────────────────┘
▼
┌────────────────────────────────────────────────────────────┐
│ 7. Prompt 12: comparar plan antes/después y verificar │
└────────────────────────────────────────────────────────────┘
El orden importa menos de lo que parece, pero hay una regla dura: nunca escribas el índice antes de leer el plan. Es tentador porque el plan es un texto aburrido y el índice es una línea. Por eso escribí el prompt 1 con la instrucción explícita de no proponer índices todavía.
Conclusión
La diferencia entre “optimizar SQL” y “leer SQL” es que leer es un problema de información y optimizar es un problema de criterio. Las herramientas de IA han dado un salto enorme en la primera parte: interpretar un plan de ejecución de cuarenta líneas y señalar el nodo concreto que se come el tiempo es algo que antes exigía años de experiencia. En la segunda parte siguen sin poder decidir por ti, y no deberían.
La trampa más común no es técnica, es de proceso: pides optimización sin el EXPLAIN, recibes la receta de siempre, aplicas un índice más, y la base de datos sigue igual pero ahora escribe más despacio. Por eso este artículo insiste tanto en el kit de datos inicial. Sin ANALYZE, BUFFERS, DDL y número de filas, cualquier prompt de optimización es ruido con formato SQL.
Y el segundo error, el que cuesta más: optimizar la query que aparece en el log y no la que devuelve el endpoint lento. Un N+1 de doscientos registros es de esos problemas: no se ve en ningún plan porque cada query es rápida. Solo se ven contando.
Si el problema no es la query sino el modelo de datos completo, empieza por Prisma desde cero. Si lo que tarda es la capa de aplicación alrededor, hay por qué usar NestJS en un SaaS. Y para el motor que vas a elegir, la comparativa está en PostgreSQL vs MySQL. Si los datos que vas a pegarle ocupan mucho, mira cómo aprovechar la context window para meter el plan sin recortarlo.
El generador de prompts está en herramientas/generador-prompts, y la documentación de por qué existe cada índice de tu esquema la tienes resuelta en prompts para generar documentación.
Sigue aprendiendo: