.. _chap-optim: ######################### Optimisation des lectures ######################### Le moment est venu de chercher à contrôler les requêtes SQL engendrées par Hibernate pour éviter de se retrouver dans la situation défavorable où de nombreuses requêtes sont exécutées pour charger, petit à petit, des parties minimes du graphe de données. Cette situation apparaît typiquement quand le graphe à matérialiser est découvert par la couche Hibernate au fur et à mesure des navigations de l'application. Or, cette dernière (ou plus exactement le concepteur de l'application: vous) sait souvent *à l'avance* à quelles données elle va accéder, et peut donc anticiper le chargement grâce à des requêtes SQL (idéalement, une seule) qui vont matérialiser l'intégralité la partie du graphe contenant ces données visitées. ***************************** S1: Stratégies de chargement ***************************** Supports complémentaires : * `Diapos pour la session "S1 : Stratégies de chargement" `_ * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2038 La *stratégie de chargement* spécifie cette anticipation. Essentiellement, elle indique quel est le voisinage d'un objet persistant qui doit être matérialisé quand cet objet est lui même chargé. La stratégie de chargement peut être paramétrée à deux niveaux. * *dans la configuration du mapping*, autrement dit sous forme d'une annotation JPA; * *à l'exécution de requêtes HQL*, grâce au mot-clé ``fetch``, déjà évoqué. Ces deux niveaux sont complémentaires. La spécification au niveau de la configuration a l'inconvénient d'être insensible aux différents contextes dans lesquels la base de données est interrogée. Pour prendre l'exemple de notre base, dans certains contextes il faudra, quand on accède à un film charger les acteurs, dans d'autre cas ce seront les notes, et enfin dans un troisième cas ni l'une ni l'autre des collections associées ne seront nécessaires. La bonne méthode consiste donc à indiquer une stratégie de chargement *par défaut* au niveau de la configuration, et à la *surcharger* grâce à des requêtes HQL spécifiques dans les contextes où elle ne fait pas l'affaire. C'est ce que nous étudions dans ce chapitre. .. note:: Nous reprenons les requêtes du contrôleur ``Requeteur`` et nous allons nous pencher sur les requêtes SQL engendrées par Hibernate. L'étude des stratégies de chargement est rendue compliquée par plusieurs facteurs défavorables. * la stratégie par défaut (si on ne spécifie rien) tend à changer d'une version Hibernate à une autre; * la stratégie par défaut n'est pas la même en Hibernate et en JPA; * la documentation n'est pas limpide, c'est le moins qu'on puisse dire. Il est donc préférable de ne pas se fier à la stratégie par défaut mais de toujours la donner explicitement, en se basant sur des principes que nous allons identifier. Et il est sans doute nécessaire de vérifier le comportement d'Hibernate sur les requêtes les plus importantes de notre application, pour éviter des accès sous-optimaux à la base sous-jacente. Nous commençons par une petite étude de cas pour bien comprendre le problème. Petite étude de cas: le problème ================================ Vous avez déjà écrit une action ``lectureParCle`` dans votre contrôleur, qui se contente de charger un film par son id avec ``load()`` ou ``get()``. Nous allons associer à cette action maintenant la vue minimale suivante. .. code-block:: jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> Accès à un film par la clé, sans navigation

Le film no 1

Nous avons bien ramené le film ${film.titre}. Et c'est tout. Nous n'accédons donc à aucun objet associé. Seul le titre est affiché. Exécutez cette action et examinez la requête SQL engendrée par Hibernate. Si vous avez utilisé les annotations JPA, cette requête devrait être la suivante (j'ai retiré la liste du ``select`` pour ne conserver que les jointures). .. code-block:: sql select [...] from Film film0_ left outer join Genre genre1_ on film0_.genre=genre1_.code left outer join Pays pays2_ on film0_.code_pays=pays2_.code left outer join Artiste artiste3_ on film0_.id_realisateur=artiste3_.id where film0_.id=? Hibernate a donc engendré une requête qui effectue une jointure (externe) pour toutes les associations de type ``@ManyToOne``. C'est clairement inutile ici puisque nous n'accédons ni au genre, ni au pays, ni au metteur en scène. Faisons la même expérience, cette fois en utilisant HQL. Vous avez dû écrire une action ``parTitre()`` qui exécute la méthode suivante: .. code-block:: java public List parTitre(String titre) { Query q = session.createQuery("from Film f where f.titre= :titre"); q.setString ("titre", titre); return q.list(); } Associez à cette action la vue suivante, qui affiche pour chaque film son titre et le nom du réalisateur. .. code-block:: jsp

Les films

Voici les films ramenés: Exécutez l'action. Vous devriez voir 4 requêtes SQL. * la première correspond à l'exécution de la requête HQL. * les trois suivantes correspondent, respectivement, à la recherche du genre, du pays et du réalisateur; soit: .. code-block:: sql select * from Genre genre0_ where genre0_.id=? select * from Pays pays0_ where pays0_.id=? select * from Artiste artiste0_ where artiste0_.id=? Les id recherchés sont bien entendu les valeurs des clés étrangères trouvées dans ``Film``. Les seconde et troisième requêtes sont ici inutiles. La quatrième ne l'est que parce que nous affichons le réalisateur, mais en fait Hibernate effectue "aveuglément" et systématiquement la recherche des objets liés au film par une association ``@ManyToOne``, dès que ce film est placé dans le cache, sans se soucier de savoir si ces objets vont être utilisés. Il s'agit d'une stratégie dite de *chargement immédiat* (ou "glouton", *eager* en anglais), en général peu efficace. Petite étude de cas: la solution ================================ Bien, nous allons remédier à cela en indiquant explicitement la stratégie de chargement à appliquer. Editez la classe ``Film.java`` et complétez toutes les annotations ``@ManyToOne`` comme suit: .. code-block:: java @ManyToOne(fetch=FetchType.LAZY) Nous souhaitons un chargement "paresseux" (*lazy*). Maintenant, exécutez à nouveau l'action de recherche par clé: la requête SQL devrait être devenue: .. code-block:: sql select [...] from Film film0_ where film0_.id=? Beaucoup mieux n'est-ce pas? On ne charge que ce qui est nécessaire, sans jointure superflue. Considérons la seconde action, celle qui effectue une recherche HQL. Si vous la ré-exécutez avec la stratégie indiquée, vous devriez voir deux requêtes SQL: .. code-block:: sql select [...] from Film film0_ where film0_.id=? select * from Artiste artiste0_ where artiste0_.id=? Pourquoi? La première requête est toujours celle correspondant à la requête HQL. La seconde est déclenchée *par navigation*: quand on veut afficher le nom du réalisateur, Hibernate déclenche l'accès à l'artiste et son chargement. Cet accès est nécessaire car l'objet n'est pas présent dans le cache, Hibernate étant maintenant en mode ``Lazy``. La stratégie indiquée est donc parfaite pour notre première action, mais pas pour la seconde où une jointure avec la table ``Artiste`` quand on recherche le film serait plus appropriée. En fait, cela montre qu'aucune stratégie générale, indiquée au niveau de la configuration, ne peut être optimale dans tous les cas. Il nous reste une solution, qui est d'adapter la stratégie de chargement au contexte particulier d'utilisation. Remplacez la requête HQL dans la méthode ``parTitre()`` par la suivante: .. code-block:: sql select film from Film as film join fetch film.realisateur where film.titre= :titre Ré-exécutez l'action de recherche par titre, et vous ne devriez plus voir qu'une seule requête SQL effectuant la jointure entre ``Film`` et ``Artiste``. Cette requête charge le film, l'artiste, *et surtout matérialise le graphe d'association entre les deux objets*. Du coup, quand on navigue du film vers son réalisateur, l'objet ``Artiste`` est trouvé dans le cache et il n'est plus nécessaire d'effectuer une requête SQL supplémentaire. Avez-vous noté le mot-clé ``fetch`` dans la requête HQL? C'est lui qui entraine une surcharge de la stratégie de chargement par défaut. Et voilà! Récapitulons. Résumé des leçons tirées ======================== Qu'avons-nous appris? Et bien, en fait, l'essentiel de ce qu'il faut comprendre pour adopter une méthode fondée de choix d'une stratégie de chargement. Pour résumer: * Hibernate (ou JPA) applique une stratégie par défaut qui tend à multiplier les requêtes SQL ou les jointures externes inutilement. * Cette stratégie par défaut peut être remplacée par une configuration explicite dans les annotations de *mapping*. * Une stratégie au niveau de la configuration ne peut être optimale dans tous les cas: on peut alors, *dans un contexte donnée*, la remplacer par un chargement adapté au contexte, exprimé en HQL avec l'option ``fetch``. J'ai pris l'hypothèse un peu simpliste que l'on cherche à minimiser le nombre de requêtes SQL. C'est une bonne approche en général, car elle laisse l'optimiseur du SGBD déterminer la bonne méthode d'exécution, en tenant compte de ses ressources propres (index, mémoire). Il se peut qu'une requête SQL engendrée soit trop complexe et pénalise au contraire les performances. Là, on tombe dans l'expertise des plans d'exécution de requête: je vous renvoie à mon cours sur les aspects systèmes des bases de données pour en savoir plus (http://sys.bdpedia.fr). ********************************************** S2: Configuration des stratégies de chargement ********************************************** Supports complémentaires : * `Diapos pour la session "S2 : Configuration des stratégies de chargement" `_ * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2040 La stratégie de chargement peut donc être spécifiée avec la configuration des classes persistantes, et "surchargée" par l'exécution de requêtes HQL indiquant explicitement une stratégie particulière, adaptée au contexte local. Dans cette section nous étudions la spécification par la configuration. Les stratégies Hibernate/JPA ============================ .. note:: La documentation Hibernate étant particulièrement confuse sur le sujet (cf. chapitre 20), je tente une simplification/clarification: si quelqu'un me démontre qu'elle ne correspond pas complètement à la réalité je serai ravi de faire un ajustement. Un petit rappel sur la terminologie avant de commencer: dans ce qui suit, étant donné un objet *x* chargé par Hibernate, désigné par *entité principale*, l'expression *entité (secondaire)* désigne un objet *y* lié à *x* via une assocation ``@ToOne``, et l'expression *collection* désigne un ensemble d'objets, liés à *x* donc via une association ``@ToMany``. Si, par exemple un objet instance de ``Film`` est l'entité principale, le réalisateur (instance de ``Artiste``) est une entité (secondaire) et les rôles du film constituent une collection. Nous avons donc essentiellement deux stratégies de chargement, s'appliquant aux entités et collections: * ``Eager`` ("glouton", "gourmand" en anglais) consiste à charger une entité ou une collection le plus tôt possible, indépendamment du fait que l'application les utilise ou non par la suite. * ``Lazy`` ("paresseux" en anglais) indique au contraire qu'une entité ou une collection est chargée le plus tard possible, au moment où l'application cherche à y accéder. Par défaut, JPA charge les entités avec une stratégie ``Eager``, et les collections avec une stratégie ``Lazy``. C'est cette stratégie qui est appliqué quand on utilise les annotations JPA, même quand le moteur implantant JPA est Hibernate. Toujours sur le même exemple, avec la stratégie par défaut JPA, le réalisateur, le pays et le genre sont des entités chargées en mode ``Eager`` quand un film est chargé dans la session; les rôles ne sont chargés que quand l'application tente de les récupérer avec la méthode *getRoles()*. Reprenez notre petite étude de cas en début de chapitre pour vérifier. .. note:: Hibernate applique une stratégie par défaut différente de JPA: tout est chargé en mode ``Lazy``. Examinons maintenant *comment* ces stratégies sont implantées. ``Eager``, le chargement glouton ================================ Dans ce mode, Hibernate va tenter d'associer le plus tôt possible les entités secondaires à l'entité principale. On peut constater en pratique que deux méthodes sont utilisées: * *Par des jointures externes*. C'est notamment le cas quand on charge un objet par *load()* ou *get()*. Hibernate engendre alors une requête comprenant des ``outer join``. * *Par des requêtes SQL complémentaires*. Si Hibernate n'a pas pu anticiper en plaçant des ``outer join``, des requêtes SQL sont effectuées, une par entité secondaire. C'est le cas par exemple quand l'entité principale est obtenue par une requête HQL, ou quand elle fait partie d'une collection chargée de manière paresseuse. Le mode ``Eager`` ne se justifie que dans deux cas * on est sûr que l'application accèdera toujours ou presque toujours aux entités secondaires quand une entité principale est chargée; * on est sûr que les entités secondaires sont dans un cache de premier ou de second niveau. Il me semble difficile d'assurer qu'une application, pour toujours et en toutes circonstances, satisfaira une des deux conditions ci-dessus. Cette stragégie a le grave inconvénient d'engendrer parfois en grande quantité des requêtes SQL élémentaires, ramenant chacune une seule ligne. Je propose donc la recommandation suivante. .. admonition:: Recommandation: toujours adopter le mode ``Lazy`` par défaut Au niveau de la configuration du *mapping*, je vous recommande d'appliquer systématiquement une stratégie ``lazy`` par défaut. Ce qui revient à compléter les annotations ``@OneToOne`` ou ``@ManyToOne`` comme suit:: @ManyToOne (fetch=FetchType.LAZY) Pour les associations de type ``@ToMany``, le mode par défaut est toujours ``Lazy`` et il est donc inutile de compléter l'annotation (mais faites-le si vous voulez privilégier la clarté). .. note:: La documentation Hibernate parle de ``Immediate`` fetching, ``Eager`` fetching et ``Join`` fetching, de manière à vrai dire assez confuse et mélangeant le *quand* et le *comment*. Il semble suffisant de considérer qu'il n'y a que deux méthodes, gloutonne (*Eager*) ou paresseuse (``Lazy``), et pour chacune plusieurs implantations possibles (par requête indépendante, par ``outer join``, etc.) selon les circonstances. Le mode ``Lazy`` ================ Le mode ``Lazy`` est le mode par défaut pour les collections. Cela semble indispensable car la taille de la collection n'est pas connue à priori, et se mettre en mode ``Eager`` ferait courir le risque de charger un très grand nombre d'objets, sans garantie sur leur utilisation, et un coût d'initialisation et de stockage en mémoire élevé. Pensez par exemple à ce que donnerait le chargement de *tous* les films d'un pays comme les USA si on associait une collection ``films`` en mode ``Eager`` à la classe ``Pays``. Par défaut, toutes les collections devraient donc être en mode ``Lazy``. Aucune solution n'étant idéale en toutes circonstances, ce mode peut donner lieu à un chargement inefficace, caractérisé par l'expression '1+n requêtes'. Le problème des 1+n requêtes ============================ Supposions que vous vouliez parcourir tous les films de la base, et pour chacun analyser les notes reçues. La structure de votre action serait la suivante: .. code-block:: java // On recherche les films Set films = session.createQuery ("from Film"); for (Film film: films) { // Pour chaque film on traite les notes for (Notation notation: films.getNotation()) { // Faire qq chose avec la notation } } Nous avons deux boucles imbriquées. *Quelles sont les requêtes SQL engendrées*? Il y en a une par boucle. * La première sélectionne tous les films et ressemble simplement à: .. code-block:: sql select * from Film * La seconde sélectionne les notes d'un film donné .. code-block:: sql select * from Notation where id_film=:film.id *Comment sont-elles exécutées*? La première est exécutée *une fois*, la seconde *autant de fois qu'il y a de films*, d'où la caractérisation par l'expression '1+n' requêtes. Le problème est que cette stratégie est inefficace car elle soumet potentiellement beaucoup de requêtes au SGBD. Si vous avez 10 000 films, vous exécuterez 10 000 fois la même requête, alors que les SGBD sont conçus, grâce à l'opération de jointure, pour ramener tous les objets en une seule requête. On peut envisager plusieurs solutions au problème. * en changeant la configuration pour accéder à la collection en mode ``Eager``: comme je l'ai déjà signalé, utiliser une configuration générale pour résoudre un problème survenant dans un contexte spécifique ne fait que transposer le problème ailleurs; * en utilisant le *chargement par lot*, décrit ci-dessous; * en utilisant une requête HQL de chargement. La troisième solution me semble de loin la meilleure. Pour votre culture Hibernate, voici une brève présentation du chargement par lot. Le chargement par lot, une variante de ``Lazy`` =============================================== Le chargement par lot (*batch fetching*) permet de factoriser les requêtes effectuées pour obtenir les collections associées aux objets de la boucle extérieure (celle sur les films dans notre exemple). Il est caractérisé par la taille d'un lot, *k*, exprimant le nombre de collections recherchées en une seule fois. La syntaxe de l'annotation est la suivante: .. code-block:: java @BatchSize(size=k) Reprenons l'exemple de nos 10 000 films, avec les 10 000 requêtes cherchant la collection *roles* de chaque film. Avec un chargement par lot de taille 10, on va grouper la recherche des collections 10 par 10. Supposons pour simplifier que les identifiants des films sont séquentiels: 1, 2, 3, ...., etc. Quand l'application cherche à accéder à la collection *roles* du *premier* film, la requête suivante sera effectuée: .. code-block:: sql select * from Notation where id_film in (1, 2, 3, 4, 5, 6, 7, 8 , 9, 10) et les 10 collections des 10 premiers films seront initialisées. L'accès aux *roles* pour les films 2, 3, 4, ..., 10 se fera donc dans le cache, sans avoir besoin d'effectuer une nouvelle requête SQL. L'accès aux roles du film 10 déclenchera la requête: .. code-block:: sql select * from Notation where id_film in (11, 12, 13, 14, 15, 16, 17, 18 , 19) et ainsi de suite. Avec ce paramétrage, on est passé du problème des 1+n requêtes au problème des 1+n/10 requêtes! C'est donc un mode intermédiaire entre ``Eager`` et ``Lazy``. Incontestablement, il va amener une amélioration des performances de l'application. Cela peut être une solution simple et satisfaisante, même si elle repose sur un paramétrage dont la valeur n'est pas évidente à fixer. Cela dit, on peut aisément faire la même critique: le paramétrage du lot dans la configuration n'est sans doute pas adapté à tous les contextes présents dans l'application. Dans la mesure où on peut, *localement*, dans un contexte donné, opter pour une stratégie ``Eager``, pourquoi se priver de le faire, ce qui est à la fois simple, efficace et compréhensible? Cela passe par l'utilisation de requêtes de chargement HQL. .. _ex-batch: .. admonition:: Exercice: appliquez le chargement par lot Ecrivez une action qui parcourt tous les films et affiche leur rôles: vous devriez constater l'effet des 1+n requêtes. Appliquer le chargement par lot pour limiter son impact, et consultez les requêtes SQL engendrées par Hibernate. *********************************** S3: Charger un sous-graphe avec HQL *********************************** Supports complémentaires : * `Diapos pour la session "S3 : Charger un sous-graphe avec HQL" `_ * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2041 Nous avons étudié HQL dans une optique de *sélection* de données à matérialiser. Dans cette section nous abordons l'option ``fetch`` qui indique que l'on veut charger les objets associés dans le cache. Si, par exemple, on sait à l'avance que l'on va traiter un film, son metteur en scène et ses acteurs, on peut décider de charger par une seule requête tout ce sous-graphe. L'option ``fetch`` ================== On ne peut pas utiliser ``fetch`` sans ``left join``. Le ``fetch`` indique que les objets de la table associée doivent être placés dans le cache. .. code-block:: sql select film from Film as film left join fetch film.roles as role Pour chaque film placé dans le cache, on initialise donc la collection des rôles. Combiner requêtes de chargement et de sélection =============================================== Attention à ne pas combiner sans précaution une requête de chargement (avec ``fetch``) et de sélection (avec ``where``). Prenons la requête suivante: on veut les films dont un des rôles est "McClane". Et on l'exprime de la manière suivante: .. code-block:: sql select film from Film as film left join fetch film.roles as role where role.nom= 'McClane' Le ``fetch`` indique que l'on veut charger dans le cache le graphe ``Film-Role`` *pour les lignes sélectionnées dans la base*. Mais ici nous avons un problème: comme nous avons exprimé une *restriction* avec la clause ``where`` sur les rôles, seuls les rôles dont le nom est McClane seront chargés. La mauvaise surprise est que quand nous naviguerons du film vers ses rôles, nous n'en trouverons qu'un, ce qui n'était pas l'objectif initial. La solution est d'utiliser deux fois la table ``Role``. La première occurence, sans restriction, apparaît dans le ``left join fetch``, la seconde dans le ``where``. Voici une expression qui me semble claire (d'autres sont possibles): .. code-block:: sql from Film as film left join fetch film.roles as role where film in (select r2.pk.film from Role as r2 where nom= 'McClane') Ce n'est pas très élégant, mais cela correspond à la satisfaction de deux besoins différents: *sélectionner* des données et les *charger* sous forme d'objet. Résoudre les 1+n requêtes avec HQL ********************************** Reprenons le problème des 1+n requêtes. Il serait préférable d'effectuer la jointure suivante en lieu et place de la requête SQL qui sélectionne simplement les films: .. code-block:: sql select * from Film as film, Notation as note where film.id=note.id_film Il se trouve que nous savons maintenant obtenir cette jointure *et le chargement du sous-graphe Film-Notation* avec la requête HQL suivante: .. code-block:: sql select distinct film from Film as film left join fetch film.notations En utilisant cette expression HQL, les notations seront placées dans le cache, et la navigation sur les notations pour chaque film ne nécessitera plus de l'exécution de la seconde requête SQL. .. code-block:: java // On recherche les films Set films = session.createQuery ("select distinct film from Film as film " + "left join fetch film.notations"); for (Film film: films) { // Pour chaque film on traite les notes for (Notation notation: films.getNotation()) { // Faire qq chose avec la notation } } .. _ex-nplus: .. admonition:: Exercice: testez la solution au problème des 1+n requêtes Reprenez l'action qui affiche un film et ses rôles. Utilisez la requête HQL *de chargement* appropriée pour avoir une seule requête et prendre tous les objets dans le cache. .. _ex-minimisation: .. admonition:: Exercice: minimisation des requêtes SQL Prenez l'application de votre projet, qui devrait consister en un ensemble d'actions consistant, chacune, à naviguer dans une partie du graphe d'objet. Le but est de minimiser le nombre de requêtes SQL, si possible avec une seule requête par action. À vous de jouer. .. _ex-test: .. admonition:: Exercice (difficile): validation des performances Nous prenons jusqu'à présent l'hypothèse implicite qu'il faut minimiser le nombre de requêtes SQL. Pour vérifier que c'est justifié, nous pouvons explorer les deux options suivantes: * Charger une base de données volumineuse, mesurer les temps de réponse avec la configuration par défaut, puis notre configuration optimisée, au moins pour quelques requêtes importantes. * Etudiez le plan d'exécution de MySQL pour quelques requêtes et vérifier qu'il est optimal. Cet exercice demande un travail conséquent et constitue un complément significatif au projet. À vous de voir si vous êtes intéressé. Pour charger une base de données avec beaucoup de lignes, vous pouvez vous reporter au site http://www.generatedata.com/. ************************* Résumé: savoir et retenir ************************* La question du chargement des objets par des requêtes SQL est importante et malheureusement assez complexe pour des raisons déjà évoquées: changements d'une version à une autre, confusion de la documentation, différences JPA/Hibernate, et aussi tout simplement la complexité des options disponibles. Sachez que j'ai essayé de vous dissimuler certains aspects (les *proxy*, les *wrappers*) dont le rôle est peu clair et qui ne sont sans doute pas indispensables à la bonne gestion du problème. Voici un résumé de mes recommandations, pour une approche qui me semble solide et gérable sur le long terme. * Dans la configuration, déclarez toutes vos associations en ``Lazy``. * Configurez la session pour produire l'affichage des requêtes SQL. * Explorez votre application systématiquement pour analyser les requêtes SQL qui sont produites; dans une application MVC c'est assez simple: exécutez chaque action et regardez quelle(s) requête(s) SQL est/sont produite(s). * Si vous constatez trop de requêtes: remplacez les opérations de navigation par une requête HQL initiale qui charge les objets auxquels l'action va accéder; si possible réduisez à une seule requête SQL produite par action. * Si vous constatez qu'une ou plusieurs requêtes SQL paraîssent trop complexes, produisez son plan d'exécution, et modifiez éventuellement vos expressions HQL pour la décomposer. Le bon réglage des requêtes SQL est un art qui implique une très bonne compréhension de l'exécution des requêtes dans un système relationnel, l'utilisation des index, des algorithmes de jointure, de tri, des ressources mémoire, etc. Si vous n'êtes pas spécialiste, l'aide d'un DBA pour optimiser votre application peut s'avérer nécessaire.