Tutoriels

Améliorer ses requêtes en base de données

Laravel Jutsu avatar
Publié le 16 juillet 2024
Couverture de l'article Améliorer ses requêtes en base de données

Optimiser les Requêtes avec Laravel

Lors du développement d'une application, l'optimisation des requêtes est essentielle pour assurer des performances élevées et une expérience utilisateur fluide. Laravel, avec son ORM Eloquent, propose diverses méthodes ingénieuses permettant de gagner de précieuses (milli)secondes.

WhereIn

L'utilisation de la méthode whereIn via le QueryBuilder ou un modèle Eloquent peut considérablement ralentir les performances. Ce problème n'est pas propre à Laravel, mais résulte d'une mauvaise pratique courante lors de l'utilisation de PDO pour MySQL, où un whereIn accumule des statements, ce qui entraîne une lenteur naturelle.

Imaginons une application avec un grand nombre de produits. Pour une raison quelconque, vous souhaitez charger certains produits en fonction de leurs identifiants. Une des méthodes courantes pour y parvenir est d'utiliser whereIn :

1$productIds = [1, 2, 3, ..., 9 999, 10 000];
2 
3Product::query()->whereIn('id', $productIds)->get();

Nous passons ici 10 000 éléments. Lorsque vous exécutez la requête brute, elle est effectivement très rapide.

Cependant, en utilisant ce code dans un contrôleur, cela prend déjà plusieurs secondes dans un environnement local. Et le temps d'exécution augmente de manière exponentielle !

Ce souci peut facilement être éliminé depuis Laravel 5.7 en utilisant la méthode whereIntegerInRaw sur la requête. Cette méthode n'utilise pas les liaisons PDO, mais insère directement le tableau d'entiers dans la requête.

1Product::query()->whereIntegerInRaw('id', range(0, 9999))->get();

La méthode whereIntegerInRaw s'assure que les valeurs du tableau sont des entiers, évitant ainsi les vulnérabilités d'injection SQL. Cette solution fonctionne uniquement avec des entiers, et n'est pas applicable si vous utilisez des UUID comme clé primaire pour votre table products.

Le compte est bon

Calculer le total des utilisateurs connectés aujourd'hui ou afficher la somme des produits en promotion sont des tâches courantes. Il existe plusieurs façons d'obtenir les résultats désirés.

Par exemple, pour compter le nombre de produits dans chaque catégorie, vous pouvez utiliser la méthode withCount.

1$categories = Category::query()->withCount('products')->get()->keyBy('name');
2 
3return [
4 'Electronics' => $categories['Electronics']->products_count,
5 'Health' => $categories['Health']->products_count,
6 'Home' => $categories['Home']->products_count,
7];

L'objectif ici est de retourner un jeu de résultats sans invoquer ni hydrater nos modèles. Pour éviter un travail inutile de la part de l'ORM, nous pouvons utiliser une requête brute (raw) SQL. Le code ci-dessous sera plus rapide et renverra les mêmes informations.

1$db = DB::select("SELECT
2 COUNT(CASE WHEN category_id = 1 THEN 1 END) AS electronics_count,
3 COUNT(CASE WHEN category_id = 2 THEN 1 END) AS health_count,
4 COUNT(CASE WHEN category_id = 3 THEN 1 END) AS home_count
5FROM category_product")[0];
6 
7return [
8 'Electronics' => $db->electronics_count,
9 'Health' => $db->health_count,
10 'Home' => $db->home_count,
11];

Manipuler les requêtes avec le QueryBuilder

Comme nous avons vu précédemment, il est parfois souhaitable de ne pas instancier et hydrater nos modèles car nous sommes intéressés par l'éxécution pure et simple de la requête SQL (affichage, insertion, mise à jour, destruction de données).

J'ai récemment tweeté à ce sujet et ai préféré une approche avec le QueryBuilder.

Cela permet de manipuler la requête à un niveau plus bas, offrant ainsi plus de flexibilité, plus de contrôle et surtout d'aller droit au but : l’exécution de la requête.

Prenons un second exemple où vous avez une commande Order fraîchement créée à laquelle vous souhaitez rattacher plusieurs LineItems :

1$items = [
2 [
3 'product' => 'Product n°1',
4 'qty' => 2,
5 ],
6 [
7 'product' => 'Product n°2',
8 'qty' => 4,
9 ],
10 [
11 'product' => 'Product n°3',
12 'qty' => 1,
13 ],
14 
15];
16 
17$order = Order::create([
18 'name' => 'My super order',
19]);
20 
21collect($items)->each(
22 fn (array $item) => $order->items()->create([
23 'name' => $item['product'],
24 'qty' => $item['qty'],
25 ]);
26);

Ce code effectue une requête d'insertion à chaque itération. Cela peut poser problème si vous enregistrez un grand nombre d'items ou plusieurs commandes simultanément.

Il serait plus efficace de réorganiser le code pour insérer tous les items de commande en une seule opération d'insertion.

1$items = collect($items)->map(function (array $item) use ($order) => {
2 $item['order_id'] = $order->id;
3 return $item;
4});
5 
6DB::table('order_line_items')->insert($items->toArray());

Manipuler les requêtes avec toBase

Parfois, nous avons besoin de charger une grande quantité de données en mémoire, comme tous les modèles que nous avons dans la base de données.

Donc, les pratiques générales que nous utilisons dans Laravel consistent à écrire le code suivant :

1$products = Product::all();

Imaginons que j'aie 10 000 produits en base de données et que je les charge tous à la fois. L'ORM Eloquent nécessite beaucoup de mémoire pour initialiser chaque instance de la classe Product, même si je n'utilise pas toutes les fonctionnalités avancées comme les mutateurs ou les relations.

Pour éviter ce gaspillage de mémoire, Laravel propose la méthode toBase qui permet de récupérer les données brutes de la base de données sans instancier les modèles Eloquent complets. Cela est particulièrement utile lorsque l'on souhaite simplement accéder aux données sans utiliser les fonctionnalités avancées de l'ORM.

1$products = Product::toBase()->get();

Conclusion

En comprenant les impacts des différentes pratiques sur la vitesse et l'efficacité des requêtes, vous pouvez facilement éviter les ralentissements et améliorer significativement les performances de votre application. Adopter des approches efficaces et bien conçues permet de tirer le meilleur parti de Laravel et de ses capacités. ⚡

A lire

Autres articles de la même catégorie