Le problème n+1
Si vous êtes développeur et que vous travaillez régulièrement avec une base de données, vous avez très certainement déjà été confronté à des problèmes de performance liés à des relations de type parent/enfant. L’anti-pattern que l’on retrouve le plus fréquemment consiste à exécuter une requête pour obtenir la relation parente puis à récupérer les enfants un à un. C’est un cas qui se produit souvent lorsque l’on travaille avec des ORM. On parle alors du problème N+1 (“N+1 problem” en anglais).
Prenons un exemple concret : une base de données permettant de répertorier des auteurs de livres ainsi que les livres écrits par ces derniers. Nous souhaitons récupérer la liste des auteurs avec les livres qu’ils ont écrits. Le code permettant d’afficher ce résultat pourrait ressembler à cela :
Avec le code ci-dessous, on constate que les performances de l’application vont se dégrader de manière exponentielle au fur et à mesure que l’on va renseigner des auteurs et ajouter des livres. Dans le cas présent, l’on récupère 10 auteurs, 11 requêtes vont être exécutées pour récupérer l’ensemble des informations voulues (1 pour récupérer les 10 auteurs, puis 10 pour récupérer les livres de chacun des auteurs).
Cela vous paraît aberrant ? Vous ne pensez ne jamais écrire un code comme cela ? Prenons le même exemple en utilisant un ORM (Doctrine par exemple).
Vous pensez peut-être que cette solution est plus performante. Pourtant le code
ci-dessus se comporte exactement de la même façon que le précédent. Par défaut
Doctrine (comme de nombreux ORM) génère des classes Proxy afin de ne récupérer
les relations enfant que lorsqu’elles sont demandées. Dans ce cas, Doctrine va
exécuter une requête à chaque appel de la méthode getBooks
pour récupérer les
informations correspondantes.
La solution pour éviter cela est bien évidemment d’utiliser des jointures SQL afin de récupérer les informations des auteurs avec les livres qu’ils ont écrit dans la même requête. La modification du premier exemple conduirait à ce code :
Les ORM permettent également d’écrire des requêtes en utilisant les jointures afin de récupérer l’ensemble des données voulu. On peut également configurer le mapping des relations pour effectuer une récupération agressive des données (mais cela n’est pas forcément recommandé car il se peut que la récupération des données de la relation ne soit pas toujours utile) :