Partager via


Modélisation pour les performances

Dans de nombreux cas, la façon dont vous effectuez votre modélisation peut avoir un impact considérable sur les performances de votre application ; alors qu’un modèle correctement normalisé et « correct » est généralement un bon point de départ, dans les applications réelles, certains compromis pragmatiques peuvent contribuer grandement à l'obtention de bonnes performances. Étant donné qu’il est assez difficile de modifier votre modèle une fois qu’une application est en cours d’exécution en production, il est important de garder à l’esprit les performances lors de la création du modèle initial.

Dénormalisation et mise en cache

La dénormalisation est la pratique qui consiste à ajouter des données redondantes à votre schéma, généralement afin d’éliminer les jointures lors de l’interrogation. Par exemple, pour un modèle avec blogs et billets, où chaque billet a une évaluation, vous pouvez être amené à afficher fréquemment l’évaluation moyenne du blog. L’approche simple consiste à regrouper les billets par leur blog et à calculer la moyenne dans le cadre de la requête, mais cela nécessite une jointure coûteuse entre les deux tables. La dénormalisation ajouterait la moyenne calculée de tous les billets à une nouvelle colonne sur blog, afin qu’elle soit immédiatement accessible, sans jointure ni calcul.

Les éléments ci-dessus peuvent être affichés sous la forme d’une mise en cache : les informations agrégées des billets sont mises en cache sur leur blog et comme pour toute mise en cache, le problème est de conserver la valeur mise en cache à jour avec les données mises en cache. Dans de nombreux cas, il est acceptable que les données mises en cache restent en suspens pendant un certain temps. Par exemple, dans l’exemple ci-dessus, il est généralement raisonnable que l’évaluation moyenne du blog ne soit pas totalement à jour à un moment donné. Si c'est le cas, vous pouvez la faire recalculer de temps en temps. Sinon, un système plus élaboré doit être configuré pour maintenir les valeurs mises en cache à jour.

Les détails suivants décrivent certaines techniques de dénormalisation et de mise en cache dans EF Core, et pointent vers les sections pertinentes de la documentation.

Colonnes calculées stockées

Si les données à mettre en cache sont un produit d'autres colonnes de la même table, alors une colonne calculée stockée peut être une solution parfaite. Par exemple, une Customer peut avoir des colonnes FirstName et LastName , mais nous devrons peut-être effectuer une recherche par le nom complet du client. Une colonne calculée stockée est automatiquement conservée par la base de données, qui la recalcule chaque fois que la ligne est modifiée, et vous pouvez même définir un index dessus pour accélérer les requêtes.

Mise à jour des colonnes mises en cache lorsque les entrées changent

Si votre colonne mise en cache doit référencer des entrées en dehors de la ligne de la table, vous ne pouvez pas utiliser de colonnes calculées. Toutefois, il est toujours possible de recalculer la colonne chaque fois que son entrée change. Par exemple, vous pouvez recalculer l’évaluation moyenne du blog chaque fois qu’un billet est modifié, ajouté ou supprimé. Veillez à identifier les conditions exactes lorsque le recalcul est nécessaire car sinon, votre valeur mise en cache sera désynchronisée.

Pour ce faire, vous devez effectuer la mise à jour vous-même via l’API EF Core standard. Les événements ou les intercepteurs SaveChanges peuvent être utilisés pour vérifier automatiquement si des publications sont mises à jour et pour effectuer le recalcul de cette façon. Notez que cela implique généralement des allers-retours de base de données supplémentaires, car des commandes supplémentaires doivent être envoyées.

Pour les applications plus sensibles aux performances, les déclencheurs de base de données peuvent être définis pour effectuer automatiquement le recalcul dans la base de données. Cela enregistre les allers-retours de base de données supplémentaires, se produit automatiquement dans la même transaction que la mise à jour principale et peut être plus simple à configurer. EF ne fournit aucune API spécifique pour la création ou la maintenance de déclencheurs, mais il est parfaitement correct de créer une migration vide et d’ajouter la définition de déclencheur via des requêtes SQL brutes.

Vues matérialisées/indexées

Les vues matérialisées (ou indexées) sont similaires aux vues régulières, sauf que leurs données sont stockées sur le disque (« matérialisées »), plutôt que calculées chaque fois que la vue est interrogée. Ces vues sont conceptuellement similaires aux colonnes calculées stockées, car elles mettent en cache les résultats des calculs potentiellement coûteux. Toutefois, elles cachent l’ensemble de résultats d’une requête entière au lieu d’une seule colonne. Les vues matérialisées peuvent être interrogées comme n’importe quelle table régulière et, étant donné qu’elles sont mises en cache sur le disque, ces requêtes s’exécutent très rapidement et de manière économique sans devoir effectuer constamment des calculs coûteux de la requête qui définit la vue.

La prise en charge spécifique des vues matérialisées varie selon les bases de données. Dans certaines bases de données (par exemple, PostgreSQL), les vues matérialisées doivent être actualisées manuellement pour que leurs valeurs soient synchronisées avec leurs tables sous-jacentes. Cela est généralement effectué via un minuteur (dans les cas où certains décalages de données sont acceptables) ou via un appel de déclencheur ou de procédure stockée dans des conditions spécifiques. Les vues indexées SQL Server, en revanche, sont automatiquement mises à jour à mesure que leurs tables sous-jacentes sont modifiées. Cela garantit que la vue affiche toujours les données les plus récentes, au coût de mises à jour plus lentes. En outre, les vues d’index SQL Server ont diverses restrictions sur ce qu’elles prennent en charge. Consultez la documentation pour plus d’informations.

EF ne fournit actuellement aucune API spécifique pour la création ou la maintenance de vues, matérialisées/indexées ou autres, mais il convient parfaitement de créer une migration vide et d’ajouter la définition de vue via des requêtes SQL brutes.

Mappage d’héritage

Il est recommandé de lire la page dédiée à l’héritage avant de poursuivre la lecture de cette section.

EF Core prend actuellement en charge trois techniques pour mapper un modèle d’héritage à une base de données relationnelle :

  • La technique de table par hiérarchie (TPH), suivant laquelle une hiérarchie .NET entière de classes est mappée à une seule table de base de données.
  • La technique de table par type (TPT), suivant laquelle chaque type de la hiérarchie .NET est mappé à une autre table de la base de données.
  • La technique de table par type concret (TPC), suivant laquelle chaque type concret de la hiérarchie .NET est mappé à une table différente de la base de données, où chaque table contient des colonnes pour toutes les propriétés du type correspondant.

Le choix de la technique de mappage d’héritage peut avoir un impact considérable sur les performances des applications : il est recommandé de mesurer soigneusement les performances avant de faire un choix.

Intuitivement, TPT peut sembler comme la technique « propre » : une table distincte pour chaque type .NET rend le schéma de base de données similaire à la hiérarchie de type .NET. De plus, étant donné que TPH doit représenter l’intégralité de la hiérarchie dans une seule table, les lignes ont toutes les colonnes, quel que soit le type réellement conservé dans la ligne, et les colonnes non liées restent vides et inutilisées. Outre l’apparence d’une technique de mappage « non propre », beaucoup croient que ces colonnes vides occupent beaucoup d’espace dans la base de données et peuvent également nuire aux performances.

Conseil

Si votre système de base de données le prend en charge (par exemple, SQL Server), envisagez d'utiliser des « colonnes éparses » pour les colonnes TPH qui seront rarement remplies.

Toutefois, les mesures montrent que la technique TPT est, dans la plupart des cas, la technique de mappage inférieure du point de vue des performances : en effet, comme toutes les données dans TPH proviennent d’une table unique, les requêtes TPT doivent joindre plusieurs tables et les jointures sont l’une des principales sources de problèmes de performances dans les bases de données relationnelles. Les bases de données ont généralement tendance à bien traiter les colonnes vides et les fonctionnalités telles que les colonnes éparses SQL Server peuvent réduire encore davantage cette surcharge.

La technique TPC a des caractéristiques de performances similaires à TPH, mais est légèrement plus lente lors de la sélection d’entités de tous les types, car cela implique plusieurs tables. Toutefois, TPC excelle vraiment lors de l’interrogation d’entités d’un type feuille unique : la requête utilise uniquement une seule table et n’a besoin d’aucun filtrage.

Pour obtenir un exemple concret, consultez ce test qui configure un modèle simple avec une hiérarchie de 7 types. 5000 lignes sont introduites pour chaque type (soit 35 000 lignes au total) et le test charge simplement toutes les lignes de la base de données :

Méthode Moyenne Erreur StdDev Gen 0 Gen1 Affecté
TPH 149 ms 3,38 ms 9,80 ms 4000.0000 1000.0000 40 Mo
TPT 312,9 ms 6,17 ms 10,81 ms 9000.0000 3000.0000 75 Mo
TPC 158,2 ms 3,24 ms 8,88 ms 5000.0000 2000.0000 46 Mo

Comme on peut le voir, TPH et TPC sont considérablement plus efficaces que TPT pour ce scénario. Notez que les résultats réels dépendent toujours de la requête spécifique exécutée et du nombre de tables de la hiérarchie. D’autres requêtes peuvent donc afficher un écart de performances différent. Il est recommandé d’utiliser ce code de test comme modèle pour tester d’autres requêtes.