Usar chunks en Laravel para ahorrar memoria

La memoria es siempre uno de los grandes olvidados en nuestra aplicación. Hasta que se llena...

Es muy común ver un código similar a este:

$users = User::all();

foreach ($users as $user) {
	$this->process($user);
}

Este código parece perfecto, ¿no? ¿Y qué pasa si en vez de 10-20 usuarios tenemos 10.000? ¿Y 1.000.000? ¿Funcionaría? Lo dudo mucho, probablemente la query tarde demasiado en ejecutarse y el servidor se quede sin memoria.

¿Cómo podemos evitar quedarnos sin memoria?

Afortunadamente, Laravel y Eloquent tienen estos temas bastante pensados. Por ejemplo, si estamos trabajando con una API que devuelve resultados, siempre hay que intentar devolverlos paginados. A lo mejor mientras trabajamos en local no nos damos cuenta, pero renderizar una lista de 100.000 usuarios no es trabajo agradable para el navegador ni es una descarga "ligera" para la página web.

Por otro lado, tenemos que intentar evitar cargar tantos datos en memoria a la vez. El código de arriba se puede mejorar un poco usando chunks:

User::chunk(100, function ($users) {
	foreach ($users as $user) {
		$this->process($user);
	}
});

Al ejecutar este código, se irán ejecutando queries similares a estas:

SELECT * FROM `users` LIMIT 100

SELECT * FROM `users` LIMIT 100 OFFSET 100

SELECT * FROM `users` LIMIT 100 OFFSET 200

Es decir, una query diferente por cada 100 usuarios, lo cual implica que nunca tendremos en memoria más de 100 usuarios a la vez :-)

También podemos usar el método lazy(), que nos devuelve un generador y podemos iterar por él en lugar de ejecutar un callback:

$chunks = User::lazy(100);
foreach ($chunks as $users) {
	foreach ($users as $user) {
		$this->process($user);
	}
}

Teniendo estas dos cosas en mente, será mucho más complicado que nuestra aplicación se quede sin memoria ejecutando queries de SQL.