Imaginons que votre application Laravel possède une table users
contenant 1'000'000 de lignes :
1User::factory()->count(1_000_000)->create();
La notation par underscore est utile pour rendre les grands nombres explicites.
Sur cette table de grande envergure, l’utilisation d’un all
ou d’un get
consisterait à effectuer une seule et unique requête assignant l’intégralité des résultats dans une variable.
L'exécution d'une requête renvoyant 1’000’000 de lignes pourrait grandement impacter votre base de données ainsi que les performances de votre script PHP allant même jusqu’à provoquer une "fatal error" de ce dernier.
1$users = User::all();2 3// Whoops\Exception\ErrorException4// Allowed memory size of 536870912 bytes exhausted (tried to allocate 20480 bytes)
Voyons désormais ce que propose Laravel pour traiter de grandes quantités de données efficacement !
Chunk
Le chunk est probablement la méthode la plus plébiscitée pour “fetch” des tables de grande envergure en effectuant des lots, la syntaxe est la suivante :
1User::chunk(count: 1000, callback: function ($users) {2 $users->map(function ($user) {3 $user->name;4 });5});
un lot est un ensemble de lignes de la base de données.
La valeur de count
correspond au nombre de lignes que doit contenir chaque lot, la variable $users
contient tous les résultats d'un lot sur lequel vous pouvez itérer dans une closure.
En utilisant une méthode de debug SQL, nous pouvons voir les requêtes générées par l’utilisation du chunk :
1# "select * from `users` order by `users`.`id` asc limit 1000 offset 0" 2# 3.28 ms 3 4# "select * from `users` order by `users`.`id` asc limit 1000 offset 1000" 5# 1.53 ms 6 7# "select * from `users` order by `users`.`id` asc limit 1000 offset 2000" 8# 1.76 ms 9 10[...]
Effectuer une multitude de requêtes de plus petite envergure récupérant des petits lots de données aura moins de chances d'impacter négativement les performances de votre script PHP ou de votre base de données.
chunkById
Le chunkById
est une alternative intéressante au classique chunk
, la syntaxe et le résultat sont relativement équivalents.
1User::chunkById(count: 1000, callback: function ($users) {2 $users->map(function ($user) {3 //4 });5});
Une différence notable se trouve cependant au niveau des requêtes exécutées :
1# "select * from `users` order by `id` asc limit 1000"2# 3.31 ms3 4# "select * from `users` where `id` > 1000 order by `id` asc limit 1000"5# 1.46 ms6 7# "select * from `users` where `id` > 2000 order by `id` asc limit 1000"8# 1.51 ms
Comme vous pouvez le voir, le chunkById
ajoute un where
sur l’id de l’utilisateur alors que le chunk
utilise un simple orderBy
.
1## ChunkById2# "select * from `users` where `id` > 1000 order by `id` asc limit 1000"3 4## Chunk5# "select * from `users` order by `users`.`id` asc limit 1000 offset 1000"
L'utilisation du chunkById
permet de se prémunir de certains comportements hasardeux de la méthode chunk
quand vous effectuez des update dans un lot ciblant la table que vous êtes en train de traiter avec la méthode chunk
.
each
La méthode chunk
que nous venons de voir nécessite d'écrire une boucle à l'intérieur d'une closure, cette dernière n'est pas forcement très élégante et pourrait alourdir la lisibilité de votre code.
La méthode each
offre une approche plus légère et simplifié du chunk.
1User::each(function ($user) {2 echo $user->name;3}, count: 1000);
Attention, cette méthode each n’est pas celle des collections !
Cette méthode each
est tout simplement une façon plus concise d’écrire le chunk
suivant.
1User::chunk(count: 1000, callback: function ($users) {2 $users->map(function ($user) {3 $user->name;4 });5});
Attention, cette concision peut ne pas peut être au gout de tous les développeurs et s'apparenter à de la magie cachant un comportement interne et nuisant à l'expressivité de votre code.
lazy
La méthode lazy
est une façon alternative d’effectuer un chunk
en passant par une LazyCollection plutôt qu’une closure :
1/* LazyCollection $users */2$users = User::lazy(count: 1000);3 4foreach ($users as $user) {5 echo $user->name;6}
Côté base de données, le fonctionnement est équivalent aux méthodes précédentes, la méthode effectuant de manière transparente des requêtes successives de 1'000 lignes.
La LazyCollection
utilisant un generator, elle offrira probablement une meilleure gestion de la mémoire coté PHP que les méthodes précédentes.
Attention, ce gain en performances n'est pas absolu, il est nécessaire d'effectuer des benchmarks pour s'en assurer dans votre contexte d'utilisation, nous reviendrons plus en profondeur sur les LazyCollection
dans un futur article.
lazyById
La méthode lazyById
est semblable au chunkById
que nous avons vu précédemment mais en utilisant une fois encore un generator.
1/* LazyCollection $users */2$users = User::lazyById(chunkSize: 1000);3 4foreach ($users as $user) {5 echo $user->name;6}
Et les collections ?
Si vous développez en Laravel, vous connaissez probablement l'existence des collections, ces dernières proposent également des méthodes éponymes : chunk et each.
Ces deux méthodes divisent les items d’une collection en une multitude de lots de plus petite taille, par exemple, en effectuant le code suivant :
1/* Eloquent\Collection $users */ 2$users = User::all(); 3 4echo $users->count(); // 1_000_000 5 6$users = $users->chunk(1000); 7 8$users->map(function ($users) { 9 echo $users->count(); // 1_OOO10});
Vous exécuterez une seule et unique requête, récupérant l’intégralité des lignes de votre base de données dans une instance de collection $users
qui seront ensuite découpés en plusieurs lots de 1'000 items.
Ces méthodes permettent de soulager votre script PHP en manipulant des plus petites variables, elles ne permettent pas de soulager la charge de votre base de données et répondent donc à des cas d’utilisation différents.
Conclusion
En performance brute, les méthodes que nous venons de voir seront généralement plus lentes qu’un basique get
ou all
.
Pour autant, ces quelques méthodes offrent des alternatives moins gourmandes en ressources quand vous manipulerez une table contenant plusieurs millions de lignes permettant d’alléger la charge que devra supporter votre serveur ou votre base de données.
Est-il préférable d'effectuer une seule et lourde requête et de manipuler un gros lot de données ou alors d'effectuer une multitude de requêtes de plus petites tailles ? C'est à vous de décider selon les impératifs de votre application.
Dans tous les cas, vous êtes désormais armé pour manipuler des tables de plusieurs millions de lignes !
A lire
Autres articles de la même catégorie
Stratégies de performances en Laravel
Explorons trois stratégies de performance permettant d'améliorer les performances de votre application Laravel.
Quelques tips pour phpunit #1
Quelques tips pour améliorer vos performances et votre confort d’utilisation de phpunit.
Des DTO sans laravel-data
Et si vous n'aviez pas besoin de laravel-data pour vos DTO ?