Eager loading y lazy loading en Laravel
Índice de contenido
- ⚙️ ¿Qué es lazy loading en Laravel?
- Cómo funciona la carga perezosa (lazy loading)
- El problema N+1 explicado con ejemplos reales
- Cómo detectar consultas N+1 con DB::listen o Debugbar
- ⚡ Qué es eager loading y cuándo usarlo
- Cómo aplicar eager loading con el método with()
- Eager Loading
- Eager Loading Multiple relaciones
- Eager Loading condiciones
- Definir el eager Loading directamente en los modelos
- Método with para cargar Relaciones con Eager Loading
- Ejemplo práctico: categorías y publicaciones en Laravel
- Eager vs Lazy loading: comparación y rendimiento
- Métodos relacionados: has(), with() y whereHas()
- 1. "Has": Filtrando modelos basados en relaciones
- 2. "With": Cargando relaciones de manera eficiente (eager loading)
- 3. "WhereHas": Filtrando basado en relaciones con condiciones adicionales
- ¿Qué es whereHas?
- Resumen: ¿Por qué usamos whereHas?
- Eager Loading (Carga ansiosa)
- ⚠️ Errores comunes y buenas prácticas en Eloquent
- Evitar consultas innecesarias
- Activar preventLazyLoading solo en producción
- Conclusión
- ❓ Preguntas frecuentes
El eager loading y lazy loading son dos técnicas que tenemos disponibles para recuperar datos relacionados al trabajar con modelos Eloquent. Y hay que conocerlas al detalle para emplear técnicas que más se ajuste a nuestras necesidades; no hay una técnica mejor que la otra, ambas se emplean para optimizar el rendimiento de la aplicación al reducir la cantidad de consultas a la base de datos necesarias para obtener datos relacionados. Vamos a conocerlas en detalle.
Cuando trabajas con Eloquent en Laravel, una de las claves para optimizar el rendimiento es entender cómo se cargan las relaciones entre modelos.
Dos técnicas dominan este terreno: lazy loading (carga perezosa) y eager loading (carga ansiosa). Ninguna es mejor que la otra: todo depende del contexto y de cómo quieras balancear eficiencia y flexibilidad.
En este artículo te explico sus diferencias, cómo aplicarlas con ejemplos reales, y cómo evitar el temido problema N+1 que puede ralentizar tu aplicación sin que te des cuenta.
⚙️ ¿Qué es lazy loading en Laravel?
También conocido como “carga bajo demanda” o “carga perezosa”; este es el comportamiento predeterminado en Eloquent por defecto que se emplea al emplear las relaciones foráneas.
El funcionamiento de esta técnica consiste en que al momento de obtener una colección de datos relacionados (entiéndase un listado de registros provenientes de una relación, por ejemplo, el listado de publicaciones dado la categoría) Eloquent solo recupera los datos de la base de datos en el momento en que los solicitas. Es decir, que para cada acceso a un registro relacionado, se ejecuta una consulta separada a la base de datos. Esto usualmente trae consigo el famoso problema de tipo N+1, en donde se ejecutan N+1 consultas a la base de datos en una misma tarea.
Cómo funciona la carga perezosa (lazy loading)
Imagina que estás mostrando una lista de publicaciones y, por cada una, necesitas el nombre de su categoría:
$posts = Post::paginate(10);
@foreach ($posts as $p)
{{ $p->category->title }}
@endforeachAunque parezca inocente, este código ejecuta una consulta por cada acceso a la relación, generando el clásico problema N+1:
una consulta principal + una por cada registro relacionado.
El problema N+1 explicado con ejemplos reales
En uno de mis proyectos, un dashboard de publicaciones, este comportamiento causaba más de 15 consultas para una sola página paginada.
El rendimiento se desplomaba.
Para detectar el origen, habilité en el AppServiceProvider la función:
Model::preventLazyLoading(app()->isProduction());Gracias a esto, Laravel lanza una excepción cuando intenta cargar relaciones de forma perezosa en producción.
El mensaje típico es:
Attempted to lazy load [category] on model [App\Models\Post] but lazy loading is disabled.
Cómo detectar consultas N+1 con DB::listen o Debugbar
Si quieres auditar tus consultas, puedes usar:
DB::listen(function ($query) {
echo $query->sql;
});O, si prefieres una interfaz visual, instala Laravel Debugbar, ideal para entornos locales.
Así sabrás cuántas consultas ejecuta cada vista y podrás actuar antes de que los tiempos de carga se disparen.
⚡ Qué es eager loading y cuándo usarlo
El eager loading o carga ansiosa permite recuperar todas las relaciones necesarias en una sola consulta.
De esta forma, Laravel prepara los datos desde el principio, evitando el N+1.
Cómo aplicar eager loading con el método with()
La forma más común es usando el método with():
$posts = Post::with('category')->paginate(10);Con esta línea, Eloquent carga los posts junto con sus categorías en una sola operación.
Al acceder a las relaciones de Eloquent como propiedades, los datos de la relación se "cargan de forma diferida" o las conocidas como lazy loading o carga perezosa.
Esto significa que los datos de la relación no se cargan realmente hasta que se accede por primera vez a la propiedad.
Sin embargo, Eloquent puede "cargar ansiosamente" relaciones en el momento en que consulta el modelo principal. La carga ansiosa alivia el problema de las consultas N + 1.
Para ilustrar el problema de las consultas N + 1, considere los siguientes modelos:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\URL;
class Tutorial extends Model
{
use HasFactory;
protected $fillable = [***];
public function sections()
{
return $this->hasMany(TutorialSection::class)->orderBy('orden');
}
}
class TutorialSection extends Model
{
use HasFactory;
protected $fillable = [***];
public function tutorial()
{
return $this->belongsTo(Tutorial::class);
}
public function classes()
{
return $this->hasMany(TutorialSectionClass::class)->orderBy('orden');
}
}
class TutorialSectionClass extends Model
{
use HasFactory;
protected $fillable = [***];
public function tutorialSection()
{
return $this->belongsTo(TutorialSection::class);
}
public function comments()
{
return $this->hasMany(TutorialSectionClassComment::class);
}
}La consulta de:
$tutorials = Tutorial->get();
foreach ($tutorials as $key => $t)
foreach ($t->sections as $key => $s)Ejecutará una consulta para recuperar todos los tutoriales:
$tutorials = Tutorial->get();Luego, iteramos todos los tutoriales y a su vez, obtenemos las secciones por cada tutorial; si tenemos 25 tutoriales, sería una consulta adicional a la base de datos por cada tutorial para obtener las secciones; serían 26 consultas, 1 para el tutorial con las secciones y 25 más por cada sección; aquí tenemos el problema conocido como N+1 ya que, según la cantidad de tutoriales que tengamos, tenemos que hacer la misma cantidad de consultas a la base de datos para obtener sus secciones; pero, la cosa se puede complicar más; si ahora, queremos obtener las clases:
luego, si queremos obtener las clases:
foreach ($s->classes->get() as $k => $c)El resultado sería catastrófico; por suerte, en Laravel podemos solventar este problema para que devuelva haga una única consulta a la base de datos.
Eager Loading
En Laravel, el Eager Loading es una técnica de optimización para reducir el número de consultas a la base de datos. Por defecto, cuando se recuperan datos con relaciones en Laravel, se utiliza el Lazy Loading, lo que puede resultar en el problema de la consulta N+1 como se explicó anteriormente.
El Eager Loading es una técnica de carga de datos en la cual, los datos relacionados, se cargan de manera anticipada para evitar la necesidad de cargarlos más tarde cuando se accede a ellos en una única consulta, por lo tanto, con esta técnica se soluciona el problema anterior del N+1; para ello, debemos de usar la función de with indicando el nombre de la relación en el modelo:
$tutorial = Tutorial::with('sections');Eager Loading Multiple relaciones
Si queremos colocar relaciones adicionales, podemos especificarlas en un array; si las relaciones son anidadas, podemos hacer lo siguiente:
$tutorial = Tutorial::with(['sections','sections.classes' ])Tal cual puedes ver, colocamos la primera relación:
sectionsY luego la relación más interna:
sections.classes
Eager Loading condiciones
Muchas veces es necesario indicar condiciones adicionales, para ello, podemos especificar una función callback en la cual, colocamos las condiciones internas, en el siguiente ejemplo, queremos que las clases estén posteadas o publicadas:
$tutorial = Tutorial::with('sections')->with(['sections.classes' => function ($query) {
$query->where('posted', 'yes');
}])->find($tutorial->id);Y con esto, puedes crear fácilmente tus relaciones al momento de cargar la relación padre; esto es particularmente útil cuando necesitas construir una Api Rest y necesitas de manera inicial todos los datos, incluyendo la relación padre e hijos.
Definir el eager Loading directamente en los modelos
En los modelos, podemos dejar de manera predefinida el uso del eager loading como vimos anteriormente; para ello, usamos la propiedad de with:
class Tutorial extends Model
{
***
protected $with = ['sections'];
}Método with para cargar Relaciones con Eager Loading
Cuando trabajamos con relaciones en Eloquent, a menudo necesitamos cargar todas las relaciones de un modelo específico o de varios, estamos hablando a que tenemos un modelo principal y otro u otros relacionamos mediante una clave foránea y deseamos cargar todos estos datos en una sola consulta, por supuesto, empleando Eloquent y evitando emplear los Joins para facilidad de la consulta; imagina que tienes una tienda en línea con productos, categorías y etiquetas. Quieres obtener todos los productos junto con sus categorías y etiquetas relacionadas. Aquí es donde entra en juego la carga ansiosa (eager loading) de Laravel, ya que, mediante el método de with, podemos especificar las relaciones desde la consulta principal, obteniendo los datos completamente organizados y sin registros repetidos como sucedería si empleamos los JOINs.
La carga ansiosa permite cargar todas las relaciones necesarias al realizar la consulta inicial. En lugar de ejecutar una consulta por cada modelo en la colección, Eloquent realiza una sola consulta adicional para cargar todas las relaciones. Esto mejora significativamente la eficiencia y la velocidad de tu aplicación.
Supongamos que tenemos los siguientes modelos: Product, Category y Tag. Queremos obtener todos los productos junto con sus categorías y etiquetas:
$products = Product::with(['category', 'tags'])->get();Y ahora, podemos acceder a TODAS sus relaciones sin necesidad de realizar consultas adicionales:
foreach ($products as $product) {
echo "Product: {$product->name}\n";
echo "Category: {$product->category->name}\n";
foreach ($product->tags as $tag) {
echo "Tag: {$tag->name}\n";
}
echo "\n";
}De esta forma, podemos optimizar las consultas evitando problemas como el de N+1 en Laravel y con esto, tener consultas eficientes; de esta forma, podemos realizar operaciones adicionales muy fácilmente como la de guardar en cache los datos realizados en una sola consulta al emplear consultas SQLs optimizadas.
Ejemplo práctico: categorías y publicaciones en Laravel
En lugar de hacer esto:
$categories = Category::paginate(10);
@foreach ($categories as $c)
{{ $c->posts }}
@endforeachPodemos optimizarlo así:
$categories = Category::with('posts')->paginate(10);Sin embargo, cuidado: si tus posts contienen campos grandes como content, podrías sobrecargar la consulta.
En mi caso, solucioné esto seleccionando solo las columnas necesarias:
$posts = Post::with('category:id,title')->paginate(10);Eager loading anidado y con condiciones
Eloquent permite cargar relaciones anidadas o filtradas fácilmente:
Tutorial::with('sections')
->with(['sections.classes' => function ($query) {
$query->where('posted', 'yes')->orderBy('orden');
}])
->find($tutorial->id);Así, evitas consultas redundantes incluso en estructuras de varios niveles.
Eager vs Lazy loading: comparación y rendimiento
Técnica Consultas generadas Rendimiento Uso recomendado
Lazy loading 1 + N (una por cada relación) Más lento si hay muchas relaciones Cuando solo accedes a una o pocas relaciones puntuales
Eager loading 1 o pocas consultas Más rápido en colecciones grandes Cuando necesitas mostrar varias relaciones al mismo tiempo
La diferencia es abismal cuando se trabaja con relaciones complejas.
Métodos relacionados: has(), with() y whereHas()
En Laravel, la capa del modelo con Eloquent es una de las capas del MVC que incluye Laravel más ricas y con más opciones, y que no está demás, ya que se trata de la capa de datos, la cual se conecta a la base de datos para gestionar la misma; hay muchos métodos disponibles en Eloquent, pero, vamos a conocer 3 que pueden confundirse que con los “Has”, “With” y “WhereHas”.
1. "Has": Filtrando modelos basados en relaciones
El método has() se utiliza para filtrar los modelos seleccionados basándose en una relación. Funciona de manera similar a una condición normal WHERE pero con una relación.
Si usas
has('relation'), significa que solo deseas obtener los modelos que tienen al menos un modelo relacionado en esta relación.
Por ejemplo, consideremos un sistema de blog con dos tablas: “posts” y “comments”. Si queremos obtener todos los usuarios que tienen al menos un comentario, podemos hacerlo de la siguiente manera:
$users = User::has('comments')->get();
// Solo se incluirán los usuarios que tienen al menos un comentario en la colecciónEn pocas palabras, viene siendo una especie de condicional en la cual, se obtienen los usuarios que tengan al menos un comentario; en este ejemplo, comments viene siendo una relación de los usuarios.
2. "With": Cargando relaciones de manera eficiente (eager loading)
El método with() se utiliza para cargar relaciones junto con la relación principal, anteriormente vimos el problema de N+1 en Laravel que se solventa mediante el método with.
Básicamente, junto con el modelo principal, Laravel cargará las relaciones que especifiques. Esto es especialmente útil cuando tienes una colección de modelos y deseas cargar una relación para todos ellos.
$users = User::with('posts')->get();
foreach ($users as $user) {
// Las publicaciones ya están cargadas y no se ejecuta una consulta adicional
$user->posts;
}3. "WhereHas": Filtrando basado en relaciones con condiciones adicionales
El método whereHas() funciona de manera similar a has(), pero te permite especificar filtros adicionales para el modelo relacionado. Puedes agregar condiciones personalizadas para verificar en el modelo relacionado.
Por ejemplo, si queremos obtener todos los usuarios que tienen publicaciones creadas después de una fecha específica, podemos hacerlo así:
$users = User::whereHas('posts', function ($query) {
$query->where('created_at', '>=', '2026-01-01 00:00:00');
})->get();
// Solo se incluirán los usuarios que tienen publicaciones desde 2026 en adelante¿Qué es whereHas?
En resumidas cuentas, el has (que creo que es la primera vez que lo empleo en un curso) es la forma de hacer consultas sobre relaciones. Así de simple.
En pocas palabras, mediante un where no podemos hacer consultas a relaciones. Aunque hay una pequeña consideración: si estuvieras empleando un join, perfectamente podrías usar where, que sería como quien dice, la manera tradicional de toda la vida.
Pero en este caso estamos trabajando con with, y por lo tanto, no podemos hacerlo de esa forma:
Book::with(['post', 'post.category'])
->when($this->category_id, function (Builder $query, $category_id) {
$query->whereHas('post', function ($q) use ($category_id) {
$q->where('category_id', $category_id);
});
})Esta es la relación con los modelos:
class Book extends Model
{
***
public function post()
{
return $this->belongsTo(Post::class);
}
}
class Post extends TaggableModel
{
***
public function category()
{
return $this->belongsTo(Category::class);
//->select('id', 'title', 'slug');
}
}Resumen: ¿Por qué usamos whereHas?
En resumidas cuentas, whereHas es el mecanismo que tenemos para hacer una condición where sobre una relación.
Quédate con eso: para eso usamos whereHas.
Para todo lo demás, existe Mastercard… perdón, existe Eloquent.
Ya en nuestro módulo de dashboard tenemos este problema, por una parte, tenemos las consulta principal:
app\Http\Controllers\Dashboard\PostController.php
public function index()
{
if(!auth()->user()->hasPermissionTo('editor.post.index')){
return abort(403);
}
$posts = Post::paginate(10);
return view('dashboard/post/index', compact('posts'));
}Y desde la vista, referenciamos la categoría, por defecto, Laravel emplea la técnica de lazy loading para obtener los datos relacionados por lo tanto, cada vez que se realice una consulta, se va a realizar una consulta adicional, desde el listado, estamos obteniendo la categoría y con esto una consulta adicional por cada post en la página:
resources\views\dashboard\post\index.blade.php
@foreach ($posts as $p)
****
<td>
{{ $p->category->title }}
***Lo cual, es el problema del N+1, en donde N en nuestro ejemplo es el tamaño de la página, unos 10 que representan las categorías obtenidas desde el post y el 1 es la consulta principal para obtener los datos paginados.
Por suerte, Laravel en versiones modernas permite detectar este problema muy fácilmente mediante la siguiente configuración:
app\Providers\AppServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Database\Eloquent\Model;
***
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Model::preventLazyLoading(app()->isProduction());
}
}Con el AppServiceProvider podemos cargar clases esenciales de nuestro proyecto para integrarlos en la aplicación.
Así que, si ahora intentamos acceder a la página anterior, veremos un error por pantalla como el siguiente:
Attempted to lazy load [category] on model [App\Models\Post] but lazy loading is disabled.El sistema de detección del problema N+1 en Laravel no es perfecto, ya que si solamente tuviéramos una paginación de 1 nivel, no ocurriría la excepción anterior.
Con truco adicional, podemos ver las consultas realizadas para resolver una petición del cliente:
routes/web.php
DB::listen(function ($query){
echo $query->sql;
// Log::info($query->sql, ['bindings' => $query->bindings, 'time' => $query->time]);
});También podemos emplear la extensión de debugbar, pero esto lo veremos en el siguiente capítulo, si habilitas el script anterior, verás que ocurren más de 15 consultas, una de ellas para la sesión del usuario autenticado, permisos y roles la de los post y 10 para las categorías si tienes una paginación de 10 niveles, Esto es estupendo para detectar el problema pero con el inconveniente de que nuestra página de detalle para la categoría ya no funciona, para corregirla, vamos a introducir el siguiente tema.
Vamos a crear otro ejemplo, vamos a emplear la relación de posts que tenemos en la categoría:
app\Models\Category.php
class Category extends Model
{
***
function posts() {
return $this->hasMany(Post::class);
}
}Si desde la vista obtenemos la relación:
resources\views\dashboard\category\index.blade.php
@foreach ($categories as $c)
***
<td>
{{ $c->posts }}Veremos la excepción anterior, así que, obtenemos los posts junto con las categorías:
app\Http\Controllers\Dashboard\CategoryController.php
$categories = Category::with('posts')->paginate(10);El problema de este esquema es que va a traer todo los posts asociados a una categoría, y un post es una relación algo pesada, ya que contiene la columna de content con todo el contenido HTML, y si esto sumamos las 10 categorías en el listado el problema se multiplica.
Existen varias formas en las cuales podemos especificar las columnas que queremos obtener de la relación secundaria:
$posts = Post::with('category:id,title')->paginate(10);
$posts = Post::with(['category' => function($query){
// $query->where('id',1);
$query->select('id','title');
}])->paginate(10);Aunque estos esquemas no son soportados por la relación de posts de las categorías:
$categories = Category::with('posts:id,title')->paginate(10);
$categories = Category::with(['posts' => function($query){
// $query->where('id',1);
$query->select('id','title');
}])->paginate(10);De momento, no podemos solucionar el problema y con esto la excepción ya que para ello necesitamos o cambiar la petición para que emplee los JOINs, o presentar el siguiente tema que es el de Eager Loading que veremos a continuación.
Eager Loading (Carga ansiosa)
Con este proceso podemos realizar todas las operaciones en una sola consulta, si nos vamos al ejemplo anterior, que tenemos N+1 consultas a la base de datos, solamente realizaremos una sola consulta y con esto, mejorar el rendimiento de la aplicación, para ello, debemos de especificar la relación al momento de realizar la consulta principal:
app\Http\Controllers\Dashboard\PostController.php
$posts = Post::with(['category'])->paginate(10);Si vamos a nuestra página de detalle de categorías, veremos que funciona correctamente.
Esta función tiene muchas implementaciones, por ejemplo, si tenemos una relación anidada:
class Tutorial extends Model
{
***
}También podemos definir en el modelo el uso de esta técnica por defecto:
class Post extends Model
{
protected $with = ['category'];
}El método de with() lo podemos extender en relaciones más complejas, como la siguiente que tenemos una relación de dos niveles:
class Tutorial extends Model
{
***
public function sections()
{
return $this->hasMany(Tutorial::class);
}
}
class TutorialSection extends Model
{
***
public function tutorial()
{
return $this->belongsTo(Tutorial::class);
}
public function classes()
{
return $this->hasMany(Tutorial::class);
}
}
class TutorialSectionClass extends Model
{
***
public function tutorialSection()
{
return $this->belongsTo(TutorialSection::class);
}
}Podemos hacer consultas de la siguiente forma, indicando más de una relación a obtener:
$posts = Post::with(['categories','tags'])->get();O si quieres colocar alguna condición sobre algunas de las relaciones, puedes implementar un callback de la siguiente forma:
Tutorial::with('sections')->with(['sections.classes' => function ($query) {
$query->where('posted', 'yes');
$query->orderBy('orden');
}])->where('posted', 'yes')->find($tutorial->id);
}⚠️ Errores comunes y buenas prácticas en Eloquent
Evitar consultas innecesarias
Usa with() solo cuando realmente necesites cargar relaciones.
No abuses del eager loading con relaciones pesadas o poco usadas.
Cargar solo las columnas necesarias
Puedes limitar las columnas con:
Post::with('category:id,title');Así reduces el peso de cada consulta.
Activar preventLazyLoading solo en producción
Model::preventLazyLoading() ayuda a detectar errores en desarrollo,
pero solo actívalo en producción si estás seguro de que tus relaciones están bien optimizadas.
Conclusión
Es importante mencionar que no hay una técnica mejor que la otra, ya que, todo depende de lo que quieras realizar pero, lo podemos simplificar de la siguiente manera, si tenemos una colección de registros relacionados mediante una FK como en el ejemplo anterior y no vas a emplear la relación foránea, la técnica que deberías de emplear sería la de carga perezosa, pero, si vas a emplear la colección con los registros relacionados, debes de emplear la carga ansiosa.
Finalmente, por cada relación especificada en el with() solamente suma una consulta adicional, también recuerda especificar la columnas en la medida de lo posible al momento de obtener las relaciones.
No existe una técnica mejor que la otra.
- Usa lazy loading cuando no necesites todas las relaciones.
- Usa eager loading cuando tu vista o API dependa de múltiples datos relacionados.
Laravel te da la flexibilidad de combinar ambas y obtener el mejor rendimiento según el contexto.
❓ Preguntas frecuentes
- ¿Cuál es más rápido: eager o lazy loading?
- Eager loading, porque agrupa las consultas en una sola operación SQL.
- ¿Puedo combinar ambas técnicas?
- Sí, Eloquent permite usar eager loading en algunas relaciones y lazy en otras, según tu necesidad.
- ¿Cómo solucionar el error de “lazy loading is disabled”?
- Activa el modo de carga perezosa temporalmente en desarrollo o ajusta tus consultas con with() para evitar el error.
El siguiente paso, es el uso de las pruebas unitarias en Laravel.
Acepto recibir anuncios de interes sobre este Blog.
El eager loading y lazy loading son dos técnicas que tenemos disponibles para recuperar datos relacionados al trabajar con modelos Eloquent. Y hay que conocerlas al detalle para emplear técnicas que más se ajuste a nuestras necesidades.