Este artículo resume cinco técnicas prácticas para consultar millones de filas en Laravel sin agotar memoria ni tiempo CPU. Cada técnica incluye una explicación breve y ejemplos de código listos para usar.
Aplica estas prácticas en procesos de reporting, ETL o consultas masivas para mantener tiempos de respuesta y uso de recursos controlados.
Introducción
En consultas a tablas masivas, prácticas comunes como SELECT * o cargar todo con get() provocan consumo excesivo de RAM y CPU. Las siguientes cinco estrategias reducen uso de recursos y aceleran procesos.
Prerrequisitos
Antes de aplicar las técnicas: asegúrate de contar con un esquema de base de datos razonable y mecanismos de cacheo/colas si vas a usar preagregados o jobs en segundo plano.
- Acceso al código de tus modelos/Eloquent.
- Permiso para crear índices y migraciones en la base de datos.
- Un sistema de cache (Redis, Memcached) y, si procede, soporte para jobs/colas.
Desarrollo
Procedimiento
A continuación se presentan las cinco técnicas: seleccionar columnas esenciales, procesar por chunks o stream, indexar y optimizar WHERE, preagregar datos y cachear resultados pesados.
1. Seleccionar sólo las columnas esenciales
Evita SELECT * cuando sólo necesitas unas pocas columnas. Reducir columnas reduce CPU, memoria y ancho de banda.
<?php
// ❌ Evitar: carga todas las columnas
$orders = Order::all();
// ✅ Seleccionar sólo lo necesario
$orders = Order::select('id', 'amount', 'created_at')
->whereBetween('created_at', [$start, $end])
->get();
Lenguaje del código: PHP (php)
En el ejemplo base, reducir de 20 columnas a 3 puede bajar el consumo de memoria de ~1.2 GB a ~200 MB en un conjunto grande de filas.
2. Procesar por chunks o con cursor (stream)
No uses get() para millones de filas. chunk() y cursor() procesan en partes y mantienen baja la memoria.
<?php
// Procesar en bloques de 10.000
Order::where('status', 'completed')
->select('id', 'amount')
->chunk(10000, function ($orders) {
foreach ($orders as $order) {
// generar fila de reporte
}
});
// O procesar con cursor para stream perezoso
$orders = Order::cursor()->filter(fn ($order) => $order->amount > 1000);
Lenguaje del código: PHP (php)
En pruebas citadas, procesar 5M de filas con cursor/chunks reduce consumo de memoria a decenas de MB frente a cientos o más.
3. Índices y cláusulas WHERE eficientes
Filtros y ordenamientos sin índices provocan full table scans. Indexa columnas usadas en WHERE, JOIN y ORDER BY y evita patrones ineficientes como LIKE ‘%…%’.
<?php
// Ejemplo de migración para agregar índices
Schema::table('orders', function (Blueprint $table) {
$table->index('status');
$table->index('created_at');
});
Lenguaje del código: PHP (php)
Un ejemplo reportado mostró una reducción de tiempo de consulta de 120 s a 0.8 s tras indexar columnas utilizadas en filtros.
4. Preagregar datos para reportes pesados
Evita agrupar o sumar decenas de millones de filas en tiempo real. Precalcula resúmenes con jobs nocturnos o usa vistas/materialized views si la BD lo permite.
<?php
class GenerateOrderSummary implements ShouldQueue {
public function handle() {
$summary = Order::selectRaw('
DATE(created_at) AS date,
COUNT(*) AS total_orders,
SUM(amount) AS revenue
')
->whereDate('created_at', today()->subDay())
->groupBy('date')
->first();
Report::updateOrCreate(['date' => $summary->date], (array)$summary);
}
}
Lenguaje del código: PHP (php)
Con preagregados, la carga de un reporte puede pasar de minutos a decenas de milisegundos; en el ejemplo base, un reporte cargó en ~50 ms.
5. Cachear consultas costosas
Para consultas repetidas, guarda el resultado en cache con expiración y/o tags. Invalida o refresca la cache cuando los datos subyacentes cambien.
<?php
$report = Cache::remember("daily_sales_report:{$date}", 3600, fn() =>
Order::whereDate('created_at', $date)
->selectRaw('COUNT(*) AS orders, SUM(amount) AS revenue')
->first()
);
Lenguaje del código: PHP (php)
Usa Redis o Memcached según tu infraestructura y define políticas claras de invalidación para evitar datos obsoletos.
Ejemplos
Ejemplos concretos ya mostrados: selección de columnas, chunk/cursor, migración para índices, job para preagregados y cacheo con Cache::remember.
Adapta los tamaños de chunk y la expiración del cache a la carga real y al patrón de acceso de tu sistema.
Checklist
- Revisar consultas y eliminar SELECT *.
- Usar chunk() o cursor() para procesado masivo.
- Indexar columnas usadas en WHERE, JOIN y ORDER BY.
- Preagregar resultados pesados con jobs o vistas.
- Cachear consultas repetidas y definir estrategia de invalidación.
Conclusión
Combinando selección precisa de columnas, procesamiento por partes, buenos índices, preagregados y cacheo, puedes consultar millones de registros en Laravel con uso de recursos razonable y tiempos de respuesta aceptables.
Empieza por los cambios menos invasivos (select, chunk, cache) y mide impacto antes de introducir cambios más estructurales como materialized views o rediseño de esquemas.
