Optimizar Consultas con Eloquent en Laravel

Video thumbnail

Optimizar las consultas a la base de datos en Laravel no es un “nice to have”, es una necesidad real cuando una aplicación empieza a crecer. En proyectos pequeños todo parece funcionar bien, pero cuando entran más usuarios, listados grandes o APIs consumidas desde móviles, los problemas de rendimiento aparecen rápido.

Después de trabajar con operaciones transaccionales y relaciones complejas entre modelos, el siguiente paso lógico fue aprender cómo consultar mejor, no más. Y Eloquent, bien usado, nos da muchas herramientas para hacerlo.

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.

Por qué es importante optimizar consultas en Laravel

Laravel facilita muchísimo el trabajo con bases de datos, pero esa facilidad puede jugar en contra si no prestamos atención a lo que realmente se está ejecutando por debajo.

En desarrollo local muchas veces no notamos problemas porque todo es rápido. Sin embargo, cuando pasas a producción y tienes múltiples usuarios consultando listados al mismo tiempo, cada consulta innecesaria suma.

El impacto real en producción

Un listado mal optimizado puede ejecutar decenas o cientos de consultas SQL sin que lo notes a simple vista. Esto se traduce en:

  • Mayor carga en la base de datos.
  • Respuestas más lentas.
  • Peor experiencia de usuario.
  • Problemas de escalabilidad.

El problema N+1 explicado de forma simple

El problema N+1 ocurre cuando:

  • Haces una consulta principal.
  • Por cada registro, Laravel ejecuta una o más consultas adicionales para relaciones.

El resultado: 1 consulta inicial + N consultas extra… o peor aún, 2N o 3N si encadenas relaciones.

Relaciones entre modelos y consultas eficientes

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>

Si no usas $p->category->title, no necesitas cargar la relación, evitando consultas innecesarias.

Esto es algo que aprendí rápido al trabajar con listados grandes: cada relación innecesaria es una consulta extra esperando suceder.

Ejemplo real: Book, Post y Category

En un caso real, tengo una relación donde:

  • Book pertenece a Post
  • Post pertenece a Category

La categoría no se asigna directamente al Book, sino que llega a través del Post. Esto evita redundancia y mantiene la integridad de los datos.

class Book extends Model
{
    public function post()
    {
        return $this->belongsTo(Post::class)
            ->select(['id', 'url_clean', 'title', 'category_id']);
    }
}

class Post extends Model
{
    public function category()
    {
        return $this->belongsTo(Category::class)
            ->select(['id', 'url_clean', 'title']);
    }
}

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.

Eloquent vs Query Builder

Laravel nos ofrece dos caminos claros para optimizar consultas: Eloquent y Query Builder. Ninguno es mejor por defecto, todo depende del contexto.

  • 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.

Uso correcto de with() para evitar N+1

Cuando trabajas con relaciones, with() es tu mejor aliado:

$books = Book::with('post', 'post.category')
    ->select('id', 'title', 'subtitle', 'date', 'posted', 'post_id')
    ->orderBy($this->sortColumn, $this->sortDirection);

Aquí Laravel hace:

  • Una consulta para books
  • Una para posts
  • Una para categories

Sin with(), cada acceso a $b->post->category dispara consultas adicionales, generando el famoso N+1 (o peor, 2N+1).

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.

Usar with o joins: Cuándo conviene usar joins o leftJoin

En algunos casos, sobre todo en listados complejos o APIs, prefiero usar leftJoin directamente:

Book::select(
   'books.title',
   'books.subtitle',
   'books.date',
   'books.posted',
   'file_payments.payments'
)
->leftJoin('file_payments', function ($join) use ($user) {
   $join->on('books.id', 'file_payments.file_paymentable_id')
        ->where('file_paymentable_type', Book::class)
        ->where('file_payments.user_id', $user->id);
})
->where('posted', 'yes')
->get();

Aquí controlo exactamente qué datos entran en la consulta y evito cargar modelos completos que no necesito.

Seleccionar solo las columnas necesarias

Uno de los errores más comunes es usar SELECT * en listados.

Evitar SELECT * en listados

Campos como content, body o textos largos no deberían cargarse si no se usan:

$books = Book::select(
   'title',
   'subtitle',
   'date',
   'url_clean',
   'posted',
   'price'
)->get();

Esto reduce:

  • Tamaño de la respuesta.
  • Uso de memoria.
  • Tiempo de ejecución.

Optimización de consultas en APIs REST

En dashboards administrativos, donde se cargan muchos registros, este punto marca una diferencia enorme. En mi experiencia, solo con limitar columnas ya se nota una mejora clara.

Todo lo anterior aplica todavía más cuando hablamos de APIs.

Qué datos enviar y cuáles no

Una API debería:

  • Enviar solo lo que el cliente necesita.
  • Evitar campos pesados.
  • Reducir la cantidad de consultas.

Si un endpoint es para listados, no tiene sentido devolver el contenido completo del recurso.

Rendimiento en dispositivos móviles

En móviles, cada byte cuenta:

  • Menos datos → menos tiempo de carga.
  • Menos consultas → mejor batería y experiencia.
  • Respuestas más rápidas → apps más fluidas.

Herramientas para detectar consultas lentas

Antes de optimizar, hay que ver qué está pasando realmente.

  • Laravel Telescope
  • Telescope permite ver:
    • Consultas ejecutadas.
    • Duplicados.
    • Tiempo de ejecución.
  • Es ideal para detectar N+1 rápidamente en desarrollo.
  • Debugbar y Clockwork
    • Laravel Debugbar y Clockwork son excelentes alternativas:
      • Debugbar muestra consultas directamente en la vista.
      • Clockwork funciona como extensión del navegador.

En más de una ocasión, gracias a estas herramientas, descubrí consultas innecesarias que no eran evidentes a simple vista.

Buenas prácticas finales para optimizar Eloquent

  • Checklist rápida de optimización
    • Usa with() para relaciones.
    • Evita relaciones que no usas.
    • Selecciona solo las columnas necesarias.
    • Usa joins cuando necesites máximo control.
    • Optimiza listados y APIs.
    • Revisa siempre las consultas con herramientas de debug.

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.

Preguntas frecuentes sobre optimización en Laravel

  • ¿Es mejor usar Eloquent o Query Builder?
    • Depende del caso. Para relaciones, Eloquent con with() es ideal. Para consultas complejas o muy optimizadas, Query Builder puede ser mejor.
  • ¿Siempre debo usar with()?
    • Solo cuando realmente vas a usar la relación. Cargar relaciones innecesarias también es un error.
  • ¿Dónde se nota más el problema N+1?
    • En listados grandes y en producción, especialmente con muchos usuarios concurrentes.
  • ¿Esto aplica también a APIs?
    • Sí, incluso más. Las APIs deben ser ligeras y eficientes.

Conclusión

Optimizar consultas con Eloquent en Laravel no es complicado, pero sí requiere disciplina. En especial en listados y APIs, pequeñas decisiones marcan una gran diferencia.

En mi experiencia, entender bien las relaciones, evitar redundancias y traer solo los datos necesarios mejora:

  • Rendimiento.
  • Escalabilidad.
  • Organización del proyecto.
  • 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.

Y una vez que tienes esto claro, el siguiente paso natural es aprender a depurar: herramientas como Debugbar o Telescope se vuelven indispensables.

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.

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.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english