← Volver a los artículos
Rendimiento y bases de datos

No es tu base de datos la que es lenta, es tu consulta: fundamentos de optimización de MySQL en Laravel

Una guía práctica de optimización de MySQL en Laravel: corrige consultas N+1, añade los índices correctos, evita bucles y convierte una página de 3 segundos en 150 ms sin actualizar el hardware.

No es tu base de datos la que es lenta, es tu consulta: fundamentos de optimización de MySQL en Laravel
Artículo destacado ↗

No es tu base de datos la que es lenta, es tu consulta: fundamentos de optimización de MySQL en Laravel que todo desarrollador debería conocer

Todo desarrollador de Laravel ha pasado por esto. Una página que solía cargar en 200 ms de repente tarda 3 segundos. Tu primer instinto es culpar a la base de datos: «MySQL es lento», «necesitamos un servidor más grande», «cambiemos a PostgreSQL». Pero aquí está la incómoda verdad — tu base de datos casi nunca es el problema. Tus consultas lo son.

MySQL es una de las piezas de software más probadas que existen. Puede manejar millones de filas y miles de conexiones concurrentes sin despeinarse. Lo que no puede hacer es arreglar mágicamente consultas mal escritas, índices faltantes y el famoso problema N+1.

En este artículo repasaremos las técnicas esenciales de optimización de MySQL que todo desarrollador de Laravel debería conocer — las que convierten una página de 3 segundos en una de 150 ms sin actualizar una sola pieza de hardware.

1. El problema N+1: el asesino silencioso de Laravel

Esta es la causa más común de aplicaciones Laravel lentas, y casi siempre es invisible hasta que la buscas deliberadamente.

Considera este código de aspecto inocente:

<?php $posts = Post::all(); foreach ($posts as $post) { echo $post->author->name; }

Se ve bien, ¿verdad? Excepto que esto genera N+1 consultas. Una consulta para obtener todos los posts, luego una consulta adicional por cada post para obtener su autor. Con 100 posts, son 101 consultas. Con 1.000 posts, son 1.001 consultas.

La solución es la carga anticipada (eager loading):

<?php $posts = Post::with('author')->get(); foreach ($posts as $post) { echo $post->author->name; }

Ahora son solo 2 consultas, sin importar cuántos posts tengas.

Cómo detectar consultas N+1

Laravel tiene excelentes herramientas para esto:

  • Laravel Debugbar — muestra cada consulta y resalta los duplicados.
  • Laravel Telescope — un compañero completo de depuración con perfilado de consultas.
  • DB::listen() — registra manualmente cada consulta.
<?php DB::listen(function ($query) { Log::info($query->sql, $query->bindings); });

También puedes activar el modo estricto en desarrollo para lanzar excepciones en la carga diferida:

<?php // En AppServiceProvider::boot() Model::preventLazyLoading(!app()->isProduction());

Esto te obliga a detectar problemas N+1 durante el desarrollo en lugar de en producción.

2. Los índices: la mayor ganancia individual

Si solo haces una cosa después de leer este artículo, que sea esta: añade índices a tus claves foráneas y columnas consultadas frecuentemente.

Laravel lo hace fácil con migraciones:

<?php Schema::table('posts', function (Blueprint $table) { $table->index('user_id'); $table->index('status'); $table->index(['status', 'created_at']); // índice compuesto });

Una consulta que escanea 1.000.000 de filas puede reducirse a escanear 100 con el índice correcto. Eso es una aceleración de 10.000 veces gracias a una sola línea de código.

Cómo saber qué índices necesitas

Usa EXPLAIN para ver qué está haciendo MySQL:

<?php $posts = Post::where('status', 'published') ->where('user_id', 42) ->orderBy('created_at', 'desc') ->get(); // Ver el plan de ejecución DB::select('EXPLAIN ' . $posts->toSql(), $posts->getBindings());

Busca estas señales de advertencia:

  • type: ALL — escaneo completo de la tabla, ningún índice utilizado.
  • rows: 500000 — MySQL espera escanear medio millón de filas.
  • Extra: Using filesort — ordenamiento en memoria o disco en lugar de usar un índice.
  • Extra: Using temporary — creación de una tabla temporal, generalmente para GROUP BY.

¿La solución? Añade un índice que coincida con el patrón de la consulta — normalmente las columnas en WHERE, luego las columnas en ORDER BY.

3. Selecciona solo lo que necesitas

Otro error clásico:

<?php $users = User::all(); // obtiene cada columna para cada usuario

Si solo necesitas nombres y correos, dilo:

<?php $users = User::select('id', 'name', 'email')->get();

Esto importa más de lo que crees. Obtener grandes columnas TEXT o JSON que no usas puede multiplicar tanto el uso de memoria como el tiempo de consulta. Y en Laravel, cada modelo Eloquent tiene sobrecarga — construir cientos de modelos completos cuando solo necesitas unos pocos campos es un desperdicio.

Para datos verdaderamente de solo lectura, considera saltarte Eloquent por completo:

<?php $stats = DB::table('orders') ->select('status', DB::raw('COUNT(*) as total')) ->groupBy('status') ->get();

Query Builder es significativamente más rápido que Eloquent cuando no necesitas modelos.

4. Usa chunk() y cursor() para grandes conjuntos de datos

Cargar un millón de filas en memoria hará que tu aplicación se caiga — incluso si la consulta en sí es rápida. Laravel proporciona dos herramientas para transmitir grandes conjuntos de datos:

<?php // chunk() — carga en lotes User::chunk(1000, function ($users) { foreach ($users as $user) { // procesar } }); // cursor() — usa un generador, una fila a la vez foreach (User::cursor() as $user) { // procesar }

cursor() usa generadores de PHP y es eficiente en memoria, pero mantiene una sola conexión a la base de datos abierta durante toda la iteración. chunk() ejecuta múltiples consultas, lo que a menudo es más seguro en producción.

5. Evita consultas en bucles

Si te encuentras escribiendo esto:

<?php foreach ($userIds as $id) { $user = User::find($id); // ... }

Detente. Reemplázalo con una sola consulta:

<?php $users = User::whereIn('id', $userIds)->get();

Esto también aplica a actualizaciones y eliminaciones:

<?php // Malo foreach ($userIds as $id) { User::where('id', $id)->update(['active' => false]); } // Bueno User::whereIn('id', $userIds)->update(['active' => false]);

La segunda versión es una consulta en lugar de cientos — a menudo la diferencia entre 500 ms y 5 ms.

6. Ten cuidado con whereHas y withCount

whereHas() es conveniente pero puede generar subconsultas correlacionadas costosas:

<?php $users = User::whereHas('orders', function ($q) { $q->where('total', '>', 1000); })->get();

Para verificaciones simples de existencia, prefiere whereIn con una subconsulta:

<?php $userIds = Order::where('total', '>', 1000) ->distinct() ->pluck('user_id'); $users = User::whereIn('id', $userIds)->get();

De manera similar, withCount() ejecuta una subconsulta por relación. Está bien para un par de relaciones, pero si estás contando cinco relaciones en la misma consulta, considera reestructurar.

7. Usa caché agresivamente

A veces la consulta más rápida es la que no ejecutas en absoluto. La capa de caché de Laravel es una de las herramientas de rendimiento más subutilizadas del framework.

<?php $topPosts = Cache::remember('top_posts', 3600, function () { return Post::orderByDesc('views')->limit(10)->get(); });

Para datos que cambian raramente — categorías, configuraciones, listas estáticas — el caché es esencialmente rendimiento gratuito. Usa Redis o Memcached en producción y recuerda invalidar las claves cuando los datos cambien.

8. Analiza consultas lentas en producción

MySQL tiene una herramienta incorporada para encontrar consultas lentas:

-- Activar el log de consultas lentas SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 1; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';

Después de un día en producción, revisa el log. Encontrarás a los verdaderos culpables — las consultas que nunca habrías pensado que eran lentas, ejecutándose miles de veces por minuto.

En Laravel, también puedes registrar consultas lentas por petición usando un service provider:

<?php // En AppServiceProvider::boot() if (app()->isProduction()) { DB::listen(function ($query) { if ($query->time > 1000) { // ms Log::warning('Slow query', [ 'sql' => $query->sql, 'time' => $query->time, ]); } }); }

9. Usa tipos de datos adecuados

Los pequeños detalles importan más de lo que crees:

  • Usa INT UNSIGNED para IDs, no VARCHAR.
  • Usa DECIMAL para dinero, nunca FLOAT.
  • Usa ENUM o TINYINT para estados, no VARCHAR(255).
  • Usa CHAR(36) para UUIDs, no VARCHAR(255).
  • Prefiere DATETIME o TIMESTAMP sobre fechas en string.

Columnas más pequeñas significan índices más pequeños, lo que significa búsquedas más rápidas. Se acumula.

10. Optimiza relaciones y restricciones

Las migraciones de Laravel facilitan la definición de claves foráneas:

<?php Schema::table('posts', function (Blueprint $table) { $table->foreignId('user_id')->constrained()->cascadeOnDelete(); });

Las claves foráneas no solo preservan la integridad de los datos — también crean automáticamente un índice, que a menudo es exactamente lo que necesitas para el rendimiento de JOIN.

Un flujo de trabajo práctico de depuración

Cuando una página es lenta, sigue esta secuencia:

  1. Instala Laravel Debugbar o Telescope. Mira cuántas consultas se ejecutan.
  2. Busca patrones N+1. Añade with() dondequiera que se acceda a una relación en un bucle.
  3. Revisa el conteo de consultas. Una página típicamente debería ejecutar menos de 20 consultas. Más de 100 es una bandera roja.
  4. Ejecuta EXPLAIN en las consultas más lentas. Busca escaneos completos de tablas.
  5. Añade índices. Haz coincidir el índice con las columnas usadas en WHERE y ORDER BY.
  6. Cachea lo que no cambia. Especialmente agregados y datos de configuración.
  7. Vuelve a medir. Repite hasta que la página sea rápida.

Conclusión

Nueve de cada diez veces, «la base de datos es lenta» en realidad significa «las consultas son lentas». Antes de recurrir a un servidor más grande, a otro motor de base de datos o a un servicio en la nube gestionado, pasa una tarde optimizando tus consultas. Casi siempre descubrirás que las ganancias ya están ahí — esperando en el código.

La carga anticipada, los índices correctos, evitar bucles y el caché resolverán la mayoría de los problemas de rendimiento en aplicaciones Laravel. Ninguna de estas técnicas requiere nueva infraestructura. Solo requieren conciencia.

Empieza con tu página más lenta. Lanza un log de consultas. Corrige el N+1. Añade un índice faltante. Vuelve a medir. Luego repite — y observa cómo tu base de datos se vuelve mágicamente «más rápida» sin cambiar nada en ella.

Tecnologías y temas

Etiquetas del artículo

No hay artículos que coincidan con estos filtros.

¿Tienes un proyecto o una idea para discutir?

Hablemos ↗