Optimización de Consultas Eloquent: Técnicas Reales para Reducir N+1 y Escalar Laravel con Millones de Registros

24 May 2026 · 6 min · Laravel

Uno de los mayores problemas de rendimiento en aplicaciones Laravel no suele estar en PHP, ni en Blade, ni siquiera en el servidor web. En la mayoría de casos, el verdadero cuello de botella está en las consultas SQL generadas por Eloquent.

Laravel ofrece una experiencia de desarrollo extraordinaria gracias a Eloquent ORM. Sin embargo, esa comodidad puede convertirse rápidamente en un problema cuando una aplicación crece y empieza a trabajar con cientos de miles o millones de registros.

He visto proyectos donde simplemente optimizando consultas Eloquent el tiempo de respuesta pasó de varios segundos a menos de 200 milisegundos, sin necesidad de cambiar infraestructura.

El problema es que muchos desarrolladores utilizan Eloquent como si fuera magia negra, sin entender realmente qué consultas SQL se están ejecutando detrás.

Eloquent no es lento por naturaleza. Lo peligroso es usarlo sin observar el SQL que realmente produce.

El problema clásico: N+1 Queries

El problema N+1 es probablemente el error de rendimiento más común dentro del ecosistema Laravel.

Por ejemplo:

$posts = Post::all();foreach ($posts as $post) { echo $post->user->name; }

A simple vista parece correcto. Sin embargo, esto genera:

  • 1 query para obtener posts.

  • N queries adicionales para usuarios.

Si existen 1000 posts, podrían ejecutarse 1001 consultas.

En producción, esto destruye rendimiento rápidamente.

Eager Loading: la primera línea de defensa

La solución básica es usar eager loading:

$posts = Post::with('user')->get();

Ahora Laravel ejecuta únicamente:

  • 1 query para posts.

  • 1 query para usuarios relacionados.

La diferencia puede ser enorme.

Sin embargo, muchos desarrolladores creen que eager loading resuelve todos los problemas de rendimiento. La realidad es más compleja.

El nuevo problema: Overfetching

Después de aprender eager loading, algunos proyectos empiezan a cargar demasiadas relaciones:

Post::with([ 'user', 'comments', 'tags', 'likes', 'media', 'category' ])->get();

Esto puede consumir enormes cantidades de memoria y generar queries pesadas.

Optimizar no significa cargar todo anticipadamente. Significa cargar exactamente lo necesario.

Seleccionar únicamente columnas necesarias

Uno de los errores más frecuentes es usar SELECT * implícitamente.

Por ejemplo:

User::all();

En tablas grandes, esto puede transferir muchísima información innecesaria.

Una mejor estrategia:

User::select('id', 'name', 'email')->get();

Y en relaciones:

Post::with('user:id,name')->get();

Reducir columnas mejora:

  • Uso de memoria.

  • Tiempo de transferencia.

  • Serialización JSON.

  • Hydration de modelos.

Debugging real de consultas

Uno de los hábitos más importantes en proyectos grandes es observar constantemente las queries reales.

Herramientas extremadamente útiles:

  • Laravel Debugbar.

  • Telescope.

  • Clockwork.

  • EXPLAIN en MySQL.

Por ejemplo:

DB::listen(function ($query) { logger($query->sql); });

Esto permite detectar queries repetitivas o lentas.

La mayoría de problemas de rendimiento no se descubren leyendo código. Se descubren observando métricas reales.

withCount y agregaciones eficientes

Otro error frecuente es contar relaciones dentro de loops:

foreach ($posts as $post) { echo $post->comments->count(); }

Esto puede disparar múltiples consultas adicionales.

Una mejor estrategia:

Post::withCount('comments')->get();

Laravel genera una subquery optimizada directamente en SQL.

Esto suele ser mucho más eficiente que hidratar relaciones completas solamente para contar registros.

Subqueries: una herramienta subestimada

Muchos desarrolladores intentan resolver todo usando relaciones complejas cuando una subquery puede ser mucho más eficiente.

Por ejemplo:

User::addSelect([ 'latest_order_date' => Order::select('created_at') ->whereColumn('user_id', 'users.id') ->latest() ->limit(1) ])->get();

Esto evita cargar relaciones completas innecesariamente.

En proyectos grandes, las subqueries bien diseñadas pueden reducir considerablemente uso de memoria.

Chunking y procesamiento masivo

Cuando existen millones de registros, cargar todo en memoria es inviable.

Error clásico:

$users = User::all();

Con millones de filas esto puede destruir el servidor.

Laravel ofrece alternativas mucho más seguras:

User::chunk(1000, function ($users) { foreach ($users as $user) { // procesar } });

O incluso:

User::cursor();

Esto reduce drásticamente consumo de memoria.

Índices SQL: el verdadero rendimiento ocurre en la base de datos

Muchos problemas atribuidos a Laravel realmente son problemas de índices inexistentes.

Por ejemplo:

User::where('email', $email)->first();

Sin índice en email, MySQL puede escanear millones de filas.

Un índice correcto cambia completamente el rendimiento.

Campos que normalmente deberían indexarse:

  • Foreign keys.

  • Emails.

  • UUIDs.

  • Campos de búsqueda frecuente.

  • Fechas usadas para sorting.

EXPLAIN: la herramienta que casi nadie usa

Una de las herramientas más importantes para optimización SQL es:

EXPLAIN SELECT * FROM users WHERE email = '[[email protected]](mailto:[email protected])';

Esto permite observar:

  • Uso de índices.

  • Table scans.

  • Costos estimados.

  • Join inefficiencies.

En proyectos grandes, aprender a interpretar EXPLAIN puede generar mejoras enormes.

Evitar accessors costosos

Otro problema común aparece con accessors complejos:

public function getFullNameAttribute() { return strtoupper($this->name . ' ' . $this->lastname); }

Esto parece inofensivo, pero ejecutado sobre miles de modelos puede impactar CPU significativamente.

Muchos proyectos terminan acumulando lógica pesada dentro de accessors sin darse cuenta.

Caching estratégico

No todas las optimizaciones deben resolverse en SQL.

Algunas consultas simplemente deberían cachearse.

Por ejemplo:

Cache::remember('popular_posts', 3600, function () { return Post::popular()->take(10)->get(); });

Redis puede reducir enorme cantidad de carga sobre la base de datos.

Sin embargo, el cache debe ser estratégico. Cachear datos incorrectamente puede generar inconsistencias difíciles de manejar.

Profiling real en producción

Muchos desarrolladores optimizan únicamente en local.

Pero el comportamiento real cambia completamente con:

  • Millones de registros.

  • Concurrencia.

  • Latencia real.

  • Infraestructura distribuida.

Herramientas como:

  • Blackfire.

  • New Relic.

  • Datadog.

  • Laravel Pulse.

permiten detectar verdaderos cuellos de botella.

Cuándo abandonar Eloquent parcialmente

Eloquent es excelente para productividad, pero no siempre es la solución óptima para queries extremadamente complejas.

En ciertos escenarios, usar:

  • Query Builder.

  • Raw SQL.

  • Views.

  • Stored procedures.

puede ser completamente válido.

La clave está en encontrar equilibrio entre mantenibilidad y rendimiento.

No todo debe convertirse en SQL manual prematuramente, pero tampoco todo debe resolverse exclusivamente con relaciones Eloquent.

Conclusión

Optimizar consultas Eloquent no significa abandonar Laravel ni escribir SQL complejo obsesivamente.

La verdadera optimización consiste en entender:

  • Qué consultas se ejecutan.

  • Cuánto cuestan.

  • Cómo escalan.

  • Qué datos realmente necesitamos.

En aplicaciones grandes, pequeñas decisiones relacionadas con queries pueden tener impactos enormes.

He visto servidores completos saturarse únicamente por problemas N+1 aparentemente pequeños.

También he visto aplicaciones manejar millones de registros eficientemente usando Eloquent correctamente.

Laravel ofrece herramientas extraordinarias para construir software escalable. Pero el rendimiento nunca aparece automáticamente. Surge cuando el desarrollador entiende profundamente cómo interactúan Eloquent, SQL y la base de datos real.

Afiliado
Curado por José Luis Luna Rubio

Acelera tu perfil
técnico con
Platzi

Más de 240 cursos y 48 carreras para fortalecer desarrollo, producto, diseño y habilidades digitales con una ruta estructurada.

  • Rutas por carrera
  • Aprendizaje práctico
  • Formación continua
240+ Cursos
48 Carreras
1mes Gratis
Obtener 1 mes gratis

Enlace de afiliado — si entras desde aquí,
apoyas este sitio sin costo extra para ti.