Eager loading en Laravel 13: cómo eliminar el problema N+1 de Eloquent
El eager loading en Laravel 13 puede reducir un listado de 100 posts de 101 consultas SQL a solo 2 o 3. La diferencia aparece cuando dejas de cargar relaciones dentro del bucle y le dices a Eloquent qué datos debe traer desde el principio.
Qué es el problema N+1
Imagina una vista que obtiene posts y muestra el nombre de su autor. Post::latest()->take(100)->get() ejecuta una consulta para los posts, pero cada acceso a $post->user->name puede disparar otra. El resultado es una consulta inicial más una por cada registro: N+1. La aplicación parece correcta, pero aumenta la latencia y la carga de la base de datos.
Medir antes de cambiar código
Activa temporalmente el registro de consultas o usa Laravel Debugbar y Telescope en desarrollo. También puedes observar el tiempo de respuesta con una herramienta externa. No te quedes con una página rápida en tu máquina: prueba un listado real, con relaciones y paginación.
Carga ansiosa básica con with()
La solución habitual es declarar la relación antes de ejecutar la consulta:
$posts = Post::with('user')
->latest()
->paginate(20);Eloquent hace una consulta para los posts y otra para los usuarios relacionados, relacionando los resultados en memoria. El número deja de crecer con cada fila. La palabra “ansiosa” describe que la relación se prepara antes de acceder a ella, no que se cargue de forma descontrolada.
Relaciones anidadas
Para un blog donde cada post tiene comentarios y cada comentario pertenece a un usuario, usa notación de puntos:
$posts = Post::with('comments.user')->latest()->get();Así puedes pasar de consultas repetidas a una consulta para posts, otra para comentarios y otra para usuarios. Declara solo las ramas que la vista necesita; cargar un árbol completo también puede consumir memoria.
Restricciones y columnas
Filtrar dentro de with()
Las relaciones aceptan una función para aplicar filtros, orden o límites apropiados:
$posts = Post::with(['comments' => function ($query) {
$query->where('approved', true)->latest();
}])->get();En Laravel moderno también puedes escribir una flecha corta cuando la expresión sea simple. Recuerda que un límite aplicado a una relación para muchos registros puede no comportarse como un límite por padre; para casos complejos revisa la consulta generada.
Seleccionar solo lo necesario
Reducir columnas baja el volumen transferido, pero conserva las claves que Eloquent necesita:
$posts = Post::with('user:id,name')
->select('id', 'user_id', 'title')
->get();Si omites user_id, Eloquent no podrá asociar correctamente cada autor. Este detalle es una causa frecuente de relaciones vacías después de optimizar una consulta.
Carga diferida: load() y loadMissing()
load() es útil cuando ya tienes modelos y una condición decide qué relación necesita la respuesta:
$posts = Post::latest()->get();
if ($showAuthors) {
$posts->load('user');
}Si trabajas con una colección, load() carga la relación para todos sus modelos en una consulta agrupada. En un modelo individual funciona igual. loadMissing() tiene la misma idea, pero no repite una relación que ya está cargada:
$post->loadMissing(['user', 'comments.user']);Agregados sin cargar colecciones
Para mostrar el número de comentarios no necesitas traer cada comentario. withCount() añade un atributo calculado:
$posts = Post::withCount('comments')->latest()->get();
foreach ($posts as $post) {
echo $post->comments_count;
}También puedes combinarlo con una condición usando un alias. Es una opción especialmente buena para tarjetas y listados donde solo se muestra una cifra.
Detectar N+1 antes de producción
preventLazyLoading en desarrollo
En el proveedor de servicios puedes activar la protección fuera de producción:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}Cuando una vista acceda a una relación no preparada, recibirás una excepción en vez de esconder el problema. Ajusta el manejo en tests según tu equipo y deja la regla activa durante el desarrollo.
La propiedad $with
Si una relación se necesita prácticamente en cada consulta, el modelo puede declararla:
protected $with = ['user'];Úsala con criterio: cargar siempre relaciones grandes puede empeorar endpoints que no las muestran. A veces es mejor ser explícito con with() en cada caso de uso.
Ejemplo completo: de 101 consultas a 3
Define Post::with('user', 'comments.user')->withCount('comments')->paginate(20) para una página que muestra autor, comentarios y contador. Mide la versión ingenua, aplica la carga ansiosa y confirma que el total se mantiene estable al cambiar 20 posts por 100. Después revisa índices sobre las claves foráneas y pagina: eager loading reduce consultas, pero no sustituye un diseño de base de datos sano.
Conclusión
El eager loading en Laravel 13 no consiste en cargarlo todo, sino en planificar las relaciones que realmente usa cada respuesta. Empieza detectando N+1, aplica with(), reserva loadMissing() para flujos condicionales y usa withCount() para agregados. Con esas decisiones, Eloquent sigue siendo expresivo sin convertir cada bucle en una tormenta de SQL.