Haz grep del código de datos escrito por IA para cazar el N+1
Hay un bug de una línea que hace tu API cincuenta veces más lenta.
Pasa el review porque cada línea parece razonable. Traes una lista de cosas y luego iteras sobre ellas para enriquecer cada una, y en algún punto de ese bucle hay una llamada a la base de datos. Trae 50 órdenes y luego consulta el cliente por cada orden: eso son 1 + 50 = 51 viajes de ida y vuelta donde 1 o 2 bastarían. Este es el problema de la consulta N+1, y los asistentes de IA lo producen constantemente porque escribir "consulta dentro del bucle" es la forma más obvia de expresar "obtén los datos relacionados".
Por qué la IA escribe N+1 tan seguido
El patrón N+1 es el código localmente más simple. Pide "adjunta el autor de cada post" y la completación natural es un bucle con una búsqueda dentro:
posts = db.query("SELECT * FROM posts WHERE blog_id = ?", blog_id) # 1 consulta
for post in posts:
# +1 consulta por post — este es el N
post.author = db.query("SELECT * FROM users WHERE id = ?", post.author_id)
Cada línea es correcta de forma aislada, así que nada se ve mal en el diff. El costo solo aparece a escala: con 500 posts son 501 consultas, cada una con su propio viaje de red, y el endpoint que era ágil en tu base de datos de prueba de 10 filas colapsa con datos reales.
Haz grep antes de mergear
Conviértelo en un reflejo de review. Antes de mergear código de obtención de datos escrito por IA, busca una llamada a base de datos o red que esté dentro de un bucle.
# heurística aproximada: una llamada query/find/fetch cerca de cuerpos de bucle
grep -rnE '(for |forEach|map\()' src | ... # luego revisa los cuerpos de los bucles
grep -rnE '\.(query|find|fetch|get|select)\(' src
Ningún regex atrapa todos los casos, así que la verdadera comprobación es leer cada bucle y preguntar: ¿el cuerpo de este bucle habla con la base de datos u otro servicio? Si es así, ese es tu N+1.
Arréglalo con un solo viaje
Reemplaza la búsqueda por fila con una sola consulta que traiga los datos relacionados de una vez: un JOIN, un WHERE id IN (...), o el hook de carga anticipada (eager loading) de tu ORM:
# SQL: una consulta con un join
rows = db.query("""
SELECT posts.*, users.name AS author_name
FROM posts JOIN users ON users.id = posts.author_id
WHERE posts.blog_id = ?""", blog_id)
# ORMs: carga anticipada de la relación
# SQLAlchemy
posts = session.query(Post).options(selectinload(Post.author)).all()
# Django
posts = Post.objects.select_related("author").all()
# Prisma
posts = await prisma.post.find_many(include={"author": True})
Un viaje en vez de cientos. El endpoint deja de caerse con datos reales, y lo atrapaste en review en lugar de en un incidente de producción.
Movidas avanzadas
- Registra el conteo de consultas en dev. Herramientas como Django Debug Toolbar,
nplusoneo Laravel Telescope cuentan consultas por request y gritan cuando una página dispara cientos. Conéctalas para que el N+1 sea visible antes del review. - Prueba con conteos de filas realistas. El N+1 es invisible con un fixture de 10 filas. Siembra suficientes datos para que un mal bucle aparezca como latencia en las pruebas.
- Prefiere
INsobre N búsquedas. Cuando no puedas hacer join, junta los IDs y tráelos en una sola consultaWHERE id IN (...), luego únelos en memoria. - Vigila el N+1 oculto en serializers y resolvers de GraphQL. Un resolver de campo que carga una relación por objeto es el mismo bug con otro sombrero; agrúpalo con un DataLoader.
Recursos
- Qué es el problema de la consulta N+1 — documentación de Prisma
- SQLAlchemy — técnicas de carga de relaciones (eager loading)
- Django —
select_relatedyprefetch_related - GraphQL DataLoader — batching para resolver el N+1
¿Desarrollas sistemas de IA o en la nube? Yeda AI audita y refuerza pipelines de LLM y de nube en producción. Hablemos · Lee el blog