Optimizar Consultas con Eloquent en Laravel
Índice de contenido
- Relaciones entre modelos
- Modelos y relaciones
- Consultas optimizadas
- Eloquent vs Query Builder
- Recomendaciones de optimización
- 1. Usar with o joins
- 2. Seleccionar únicamente las columnas necesarias
- 3. Evitar errores con select y relaciones
- Rest API y Optimización de Consultas
- Diferencias y Consideraciones
- Conclusión
Vamos a hablar sobre la importancia de optimizar las consultas a la base de datos en Laravel y cómo hacerlo de manera eficiente, ahora que ya conocemos como hacer operaciones transaccionales, es importante conocer ahora como podemos realizar consultas eficientes.
Relaciones entre modelos
Partimos de una relación entre Post y Category. Es decir, los Post heredan la categoría que les corresponde. Esto es importante para entender qué datos necesitamos cargar y qué debemos optimizar en nuestras consultas, sobre todo en los listados (por ejemplo, en el método index).
Si en tu tabla de listado no estás utilizando la categoría, no hace falta traerla desde la base de datos. Por ejemplo:
<td>
{{ $p->id }}
</td>
<td>
{{ $p->title }}
</td>
<td>
{{ $p->posted }}
</td>
<td>
{{ $p->category->title }}
</td>Si no usas $p->category->title, no necesitas cargar la relación, evitando consultas innecesarias.
Modelos y relaciones
class Book extends Model
{
use HasFactory;
protected $fillable = ['title', 'subtitle', 'date', 'url_clean', 'description', 'content', 'image', 'path', 'page', 'posted', 'price', 'price_offers', 'post_id', 'user_id'];
public function post()
{
return $this->belongsTo(Post::class)
->select(array('id', 'url_clean', 'title', 'category_id'));
}
}
class Post extends Model
{
protected $fillable = ['title', 'url_clean', 'content', 'category_id', 'posted', 'description', 'final_content', 'aux_content', 'web_content', 'image', 'path', 'date', 'type', 'post_id_language', 'language'];
public function category()
{
return $this->belongsTo(Category::class)
->select(array('id', 'url_clean', 'title'));
}
}
class Category extends Model
{
protected $fillable = ['title', 'url_clean', 'image', 'body', 'body_en'];
}En este caso, la categoría del Book no se asigna directamente porque ya la trae a través del Post. Esto evita redundancia en la base de datos y mantiene la integridad de la información.
Consultas optimizadas
Para armar esta consulta, hacemos la siguiente operación:
$books = Book::with('post', 'post.category')
->select('id', 'title', 'subtitle', 'date', 'posted', 'post_id')
->orderBy($this->sortColumn, $this->sortDirection);Eloquent vs Query Builder
- Eloquent (with): trae las relaciones definidas evitando el problema de N+1 queries.
- Query Builder (join, leftJoin): también permite optimización mediante joins, aunque la sintaxis es diferente.
Si no usamos with:
$books = Book::select('id', 'title', 'subtitle', 'date', 'posted', 'post_id')->orderBy($this->sortColumn, $this->sortDirection);A primera vista funciona, pero al habilitar Debugbar o registrar las consultas, veremos que cada registro provoca consultas adicionales al acceder a post y category. Esto es el problema N+1, que puede afectar el rendimiento en producción.
{{ $b->id }}
{{ $b->title }}
{{ $b->subtitle }}
{{ $b->date->format('d-m-Y') }}
{{ $b->post->category->title }}
{{ $b->posted }}Por lo tanto, una vez traído el post, también se traería la category, lo que genera un problema de 2 por N+1, ya que por cada registro se realizan dos consultas adicionales a la base de datos. Esto puede ser un problema serio.
En desarrollo, a menudo es difícil de notar porque el entorno local es rápido, pero al pasar a producción, cuando múltiples usuarios están conectados a la aplicación, sí se presentan problemas de rendimiento.
Por eso, lo primero que debemos hacer es optimizar las consultas basándonos en las relaciones que estamos manejando, sobre todo en los listados, donde este problema es más común. La optimización se puede hacer mediante join, left join o, preferiblemente, usando with, que es la forma recomendada en Laravel.
Recomendaciones de optimización
1. Usar with o joins
Siempre que trabajes con relaciones y listados, trae solo los datos necesarios desde la base de datos. Por ejemplo:
Post::with('category:id,url_clean,title');O en el modelo, limitando los campos de la relación:
public function category()
{
return $this->belongsTo(Category::class)
->select(['id', 'url_clean', 'title']);
}2. Seleccionar únicamente las columnas necesarias
No traigas campos grandes como content si no los vas a usar en el listado:
$books = Book::select(
'books.title', 'books.subtitle', 'books.date',
'books.url_clean', 'books.description', 'books.image',
'books.path', 'books.page', 'books.posted',
'books.price', 'books.price_offers', 'books.post_id'
)->get();Esto reduce la cantidad de datos transferidos y mejora el rendimiento.
3. Evitar errores con select y relaciones
Estas recomendaciones también aplican para REST APIs, donde es crítico:
- Traer solo los datos que el cliente necesita.
- Evitar cargar contenido pesado innecesario.
- Reducir el número de consultas para mejorar la velocidad y el consumo de recursos.
Ejemplo con leftJoin:
Book::select(
'books.title', 'books.subtitle', 'books.date', 'books.url_clean',
'books.description', 'books.image', 'books.path', 'books.page',
'books.posted', 'books.price', 'books.price_offers', 'books.post_id',
DB::raw('DATE_FORMAT(file_payments.created_at, "%d-%m-%Y %H:%i") as date_buyed'),
'file_payments.payments'
)
->leftJoin('file_payments', function ($leftJoin) use ($user) {
$leftJoin
->on('books.id', 'file_payments.file_paymentable_id')
->where("file_paymentable_type", Book::class)
->where('file_payments.user_id', $user->id)
->where('file_payments.unenroll');
})
->where('posted', 'yes')
->get();Rest API y Optimización de Consultas
Si no puedes pasar los campos limitados directamente desde la consulta, entonces puedes hacerlo desde la relación, tal como te mostré antes. Todo lo que mencionamos sobre optimización de consultas también aplica para una Rest API, y tiene mucho sentido, especialmente cuando manejamos listados de datos.
Por ejemplo, una consulta a los libros podría ser:
$books = Book::select(
'books.title',
'books.subtitle',
'books.date',
'books.url_clean',
'books.description',
'books.image',
'books.path',
'books.page',
'books.posted',
'books.price',
'books.price_offers',
'books.post_id',
DB::raw('DATE_FORMAT(file_payments.created_at, "%d-%m-%Y %H:%i") as date_buyed'),
'file_payments.payments'
)->leftJoin('file_payments', function ($leftJoin) use ($user) {
$leftJoin
->on('books.id', 'file_payments.file_paymentable_id')
->where("file_paymentable_type", Book::class)
->where('file_payments.user_id', $user->id)
->where('file_payments.unenroll');
})->where('posted', 'yes')
->get();
En este caso estoy usando leftJoin para traer datos adicionales, y luego especifico con select únicamente los campos que realmente necesito. No utilicé with porque estoy manejando los joins directamente, y la idea es traer solo los datos necesarios para el listado, sin incluir campos pesados como content, que no se usarán aquí.
Diferencias y Consideraciones
- Cuando hacemos un listado en el dashboard o para una Rest API, no necesitamos traer campos de contenido completo (content) que solo se usarían en el detalle del libro. Esto reduce la carga en la base de datos y optimiza la respuesta de la API.
- Si necesitamos mostrar contenido completo, solo entonces incluimos el campo content, por ejemplo, en el detalle del libro.
- En dispositivos móviles, cargar menos datos es crucial porque reduce el peso de la página y mejora la experiencia del usuario.
Conclusión
- Optimiza siempre tus consultas, especialmente en listados y APIs.
- Usa with o joins para reducir el problema N+1.
- Trae únicamente las columnas que vas a usar.
- Evita redundancia en tus modelos y relaciones.
- Esto mejora el rendimiento, la modularidad y la organización de tu proyecto.
Ahora, que hemos visto como hacer tantas operaciones en base de datos, vamos a conocer una herramienta con la cual, podremos hacer el debug en Laravel mediante una barra.
Acepto recibir anuncios de interes sobre este Blog.
Hablaremos sobre la importancia de optimizar consultas en Laravel con ejemplos reales viendo que nos viene mejor, si el uso del with o el join.