Etiqueta: jobs

  • Laravel a escala: 5 trucos para consultar millones de registros

    Laravel a escala: 5 trucos para consultar millones de registros

    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

    1. Revisar consultas y eliminar SELECT *.
    2. Usar chunk() o cursor() para procesado masivo.
    3. Indexar columnas usadas en WHERE, JOIN y ORDER BY.
    4. Preagregar resultados pesados con jobs o vistas.
    5. 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.