Dans cet article, vous apprendrez à utiliser la sémantique derrière vos données lorsque vous partitionnez votre base de données. Cela peut considérablement améliorer les performances de votre application. Et, plus important encore, vous découvrirez que vous devez adapter vos critères de partitionnement à votre domaine d'application unique.
J'ai collaboré avec une startup pour développer une application web permettant aux experts sportifs de prendre des décisions et d'explorer des données. L'application prend en charge tous les sports, mais nous sommes basés en Europe - et les Européens adorent le football. Chacun des centaines de jeux joués chaque jour dans le monde est accompagné de milliers de lignes. En quelques mois seulement, le tableau des événements de notre application a atteint un demi-milliard de lignes !
En comprenant comment les experts du football interrogeaient nos données, nous pouvions partitionner la base de données intelligemment. L'amélioration du temps moyen sur cette nouvelle table était entre 20x et 40x plus rapide. L'amélioration moyenne du temps sur toutes les requêtes était de 5 à 10 X.
Examinons maintenant ce scénario et apprenons pourquoi vous ne pouvez pas ignorer le contexte de vos données lors du partitionnement d'une base de données.
Présenter le contexte
Notre application sportive propose à la fois des données brutes et agrégées, même si les professionnels qui l'ont adoptée préfèrent ces dernières. La base de données sous-jacente contient des téraoctets de données complexes, non structurées et hétérogènes provenant de plusieurs fournisseurs. Ainsi, le plus grand défi consistait à concevoir une base de données fiable, rapide et facile à explorer.
Domaine d'application
Dans cette industrie, de nombreux fournisseurs offrent à leurs clients l'accès aux événements des matchs de football les plus importants. Plus précisément, ils vous fournissent des données relatives à ce qui s'est passé pendant un match, telles que les buts, les passes décisives, les cartons jaunes, les passes et bien plus encore. Le tableau contenant ces données est de loin le plus volumineux avec lequel nous avons dû travailler.
Spécifications, technologies et architecture VPS
Mon équipe a développé l'application backend qui fournit les fonctionnalités d'exploration de données les plus cruciales. Nous avons adopté Kotlin v1.6 exécuté sur une JVM (Java Virtual Machine) comme langage de programmation, Spring Boot 2.5.3 comme framework et Hibernate 5.4.32.Final comme ORM (Object Relational Mapping). La principale raison pour laquelle nous avons opté pour cette pile technologique est que la vitesse est l'une des exigences commerciales les plus cruciales. Nous avions donc besoin d'une technologie capable de tirer parti d'un traitement multithread lourd, et Spring Boot s'est avéré être une solution fiable.
Nous avons déployé notre backend sur un VPS 16 Go 8CPU via un conteneur Docker géré par Dokku. Il peut utiliser au maximum 15 Go de RAM. En effet, un Go de RAM est dédié à un système de mise en cache basé sur Redis. Nous l'avons ajouté pour améliorer les performances et éviter de surcharger le backend avec des opérations répétées.
Structure de la base de données et des tables
En ce qui concerne la base de données, nous avons décidé d'opter pour MySQL 8. Un VPS de 8 Go et 2 CPU héberge actuellement le serveur de base de données, qui prend en charge jusqu'à 200 connexions simultanées. L'application dorsale et la base de données se trouvent dans la même batterie de serveurs pour éviter une surcharge de communication. Nous avons conçu la structure de la base de données pour éviter les doublons et dans un souci de performance. Nous avons décidé d'adopter une base de données relationnelle car nous voulions avoir une structure cohérente pour convertir les données reçues des fournisseurs. De cette façon, nous normalisons les données sportives, ce qui facilite leur exploration et leur présentation aux utilisateurs finaux.
La base de données contient des centaines de tables au moment de la rédaction, et je ne peux pas toutes les présenter à cause du NDA que j'ai signé. Heureusement, une table suffit pour analyser en profondeur pourquoi nous avons fini par adopter la partition basée sur le contexte des données que vous êtes sur le point de voir. Le véritable défi est venu lorsque nous avons commencé à effectuer des requêtes lourdes sur la table des événements. Mais avant de plonger là-dedans, voyons à quoi ressemble le tableau des événements :
Comme vous pouvez le voir, cela n'implique pas beaucoup de colonnes, mais gardez à l'esprit que j'ai dû en omettre certaines pour des raisons de confidentialité. Mais qu'est-ce vraiment les questions ici sont le parameterId
et gameId
Colonnes. Nous utilisons ces deux clés étrangères pour sélectionner un type de paramètre (par exemple, but, carton jaune, passe, pénalité) et les jeux dans lesquels cela s'est produit.
Problèmes de performances
La table des événements a atteint un demi-milliard de lignes en quelques mois seulement. Comme nous l'avons déjà expliqué en détail dans cet article de blog, le principal problème est que nous devons effectuer des opérations d'agrégation à l'aide de requêtes IN lentes. C'est parce que ce qui se passe pendant un match n'est pas si important. Au lieu de cela, les experts sportifs souhaitent analyser des données agrégées pour trouver des tendances et prendre des décisions en fonction de celles-ci.
De plus, bien qu'ils analysent généralement toute la saison ou les 5 ou 10 derniers matchs, les utilisateurs souhaitent souvent exclure certains matchs particuliers de leur analyse. C'est parce qu'ils ne veulent pas qu'un jeu particulièrement mal ou bien joué polarise leurs résultats. Nous ne pouvons pas pré-générer les données agrégées car nous aurions à le faire sur toutes les combinaisons possibles, ce qui n'est pas faisable. Nous devons donc stocker toutes les données et les agréger à la volée.
Comprendre le problème de performances
Passons maintenant à l'aspect central qui a conduit aux problèmes de performances auxquels nous avons dû faire face.
Les tables d'un million de lignes sont lentes
Si vous avez déjà traité des tables contenant des centaines de millions de lignes, vous savez qu'elles sont intrinsèquement lentes. Vous ne pouvez même pas penser à exécuter des JOIN sur des tables aussi volumineuses. Pourtant, vous pouvez effectuer des requêtes SELECT dans un délai raisonnable. Cela est particulièrement vrai lorsque ces requêtes impliquent des conditions WHERE simples. D'autre part, ils deviennent terriblement lents lors de l'utilisation de fonctions d'agrégation ou de clauses IN. Dans ces cas, ils peuvent facilement prendre jusqu'à 80 secondes, ce qui est tout simplement trop.
Les index ne suffisent pas
Pour améliorer les performances, nous avons décidé de définir des index. C'était notre première approche pour trouver une solution aux problèmes de performances. Mais, malheureusement, cela a conduit à un autre problème. Les index prennent du temps et de l'espace. Ceci est généralement insignifiant, mais pas lorsqu'il s'agit de tables aussi volumineuses. Il s'est avéré que la définition d'index complexes basés sur les requêtes les plus courantes prenait plusieurs heures et plusieurs Go d'espace. De plus, les index sont utiles mais ne sont pas magiques.
Le partitionnement de base de données basé sur le contexte des données comme solution
Comme nous ne pouvions pas résoudre le problème de performances avec des index personnalisés, nous avons décidé d'essayer une nouvelle approche. Nous avons discuté avec d'autres experts, cherché des solutions en ligne, lu des articles basés sur des scénarios similaires et avons finalement décidé que le partitionnement de la base de données était la bonne approche à suivre.
Pourquoi le partitionnement traditionnel n'est peut-être pas la bonne approche
Avant de partitionner toutes nos plus grandes tables, nous avons étudié le sujet à la fois dans la documentation officielle de MySQL et dans des articles intéressants. Bien que nous ayons tous convenu que c'était la voie à suivre, nous avons également réalisé qu'appliquer le partitionnement sans tenir compte de notre domaine d'application particulier serait une erreur. Plus précisément, nous avons compris à quel point il était crucial de trouver les bons critères lors du partitionnement d'une base de données. Certains experts en partitionnement nous ont appris que l'approche traditionnelle consiste à partitionner sur le nombre de lignes. Mais nous voulions trouver quelque chose de plus intelligent et de plus efficace que cela.
Fouiller dans le domaine applicatif pour trouver les critères de partitionnement
Nous avons appris une leçon essentielle en analysant le domaine d'application et en interrogeant nos utilisateurs. Les experts sportifs ont tendance à analyser les données agrégées des matchs d'une même compétition. Par exemple, une compétition de football peut être une ligue, un tournoi ou un match unique où vous pouvez gagner un trophée. Il existe des milliers de compétitions différentes. Les plus importantes en Europe sont la Ligue des champions, la Premier League, la Liga, la Serie A, la Bundesliga, l'Eredivisie, la Liga 1 et la Primeira Liga.
Cela signifie que nos utilisateurs prennent très rarement en compte les données provenant de différentes compétitions. De plus, ils préfèrent explorer les données saison par saison. En d'autres termes, ils sortent rarement du contexte représenté par une compétition sportive disputée au cours d'une saison particulière. Notre structure de base de données a exprimé ce concept avec une table appelée SeasonCompetition
, dont le but est d'associer une compétition à une saison précise. Nous avons donc réalisé qu'une bonne approche serait de partitionner nos plus grandes tables en sous-tables liées à un SeasonCompetition
particulier exemple.
Plus précisément, nous avons défini le format de nom suivant pour ces nouvelles tables :<tableName>_<seasonCompetitionId>
.
Par conséquent, si nous avions 100 lignes dans le SeasonCompetition
table, nous aurions à diviser le grand Events
tableau dans le plus petit Events_1
, Events_2
, …, Events_100
les tables. D'après notre analyse, cette approche conduirait à une amélioration considérable des performances dans le cas moyen, tout en introduisant des frais généraux dans les cas les plus rares.
Faire correspondre les critères avec les requêtes les plus courantes
Avant de coder et de lancer les scripts pour exécuter cette opération complexe et potentiellement sans retour, nous avons validé nos études en examinant les requêtes les plus courantes effectuées par notre application backend. Mais ce faisant, nous avons découvert que la grande majorité des requêtes ne concernaient que les jeux joués dans le cadre d'une SeasonCompetition. Cela nous a convaincus que nous avions raison. Nous avons donc partitionné toutes les grandes tables de la base de données avec l'approche que nous venons de définir.
SELECT AVG('value') as 'value', SUM('minutes') as 'minutes'
FROM 'Events'
WHERE 'parameterId' = 15 AND 'gameId' IN(223,241,245,212,201,299,187,304,187,205)
GROUP BY 'teamId'
Maintenant, étudions les avantages et les inconvénients de cette décision.
Avantages
- Exécuter des requêtes sur une table contenant au maximum un demi-million de lignes est beaucoup plus performant que de le faire sur une table d'un demi-milliard de lignes, en particulier lorsqu'il s'agit de requêtes agrégées.
- Les petits tableaux sont plus faciles à gérer et à mettre à jour. L'ajout d'une colonne ou d'un index n'est même pas comparable à avant en termes de temps et d'espace. De plus, chaque
SeasonCompetition
est différent et nécessite des analyses différentes. Par conséquent, cela peut nécessiter des colonnes et des index spéciaux, et le partitionnement susmentionné nous permet de gérer cela facilement. - Le fournisseur peut modifier certaines données. Cela nous oblige à effectuer des requêtes de suppression et de mise à jour, qui sont infiniment plus rapides sur de si petites tables. De plus, ils ne concernent toujours que certains jeux d'un
SeasonCompetition
particulier , nous n'avons donc plus besoin d'opérer que sur une seule table maintenant.
Inconvénients
- Avant de faire une requête sur ces sous-tables, nous devons connaître le
seasonCompetitionId
associés aux jeux qui vous intéressent. C'est parce que leseasonCompetitionId
La valeur est utilisée dans le nom de la table. Par conséquent, notre backend doit récupérer ces informations avant d'exécuter la requête en examinant les jeux en analyse, ce qui représente une petite surcharge. - Lorsqu'une requête implique un ensemble de jeux impliquant de nombreux
SeasonCompetitions
, l'application backend doit exécuter une requête sur chaque sous-table. Ainsi, dans ces cas, nous ne pouvons plus agréger les données au niveau de la base de données, et nous devons le faire au niveau de l'application. Cela introduit une certaine complexité dans la logique du backend. En même temps, nous pouvons exécuter ces requêtes en parallèle. De plus, nous pouvons agréger les données récupérées efficacement et en parallèle. - Gérer une base de données avec des milliers de tables n'est pas facile et peut être difficile à explorer dans un client. De même, l'ajout d'une nouvelle colonne ou la mise à jour d'une colonne existante dans chaque table est fastidieux et nécessite un script personnalisé.
Effets du partitionnement basé sur le contexte des données sur les performances
Examinons maintenant l'amélioration du temps obtenue lors de l'exécution d'une requête dans la nouvelle base de données partitionnée.
- Amélioration du temps dans le cas moyen (requête impliquant un seul
SeasonCompetition
):de 20x à 40x - Amélioration du temps dans le cas général (requête impliquant un ou plusieurs
SeasonCompetitions
):de 5x à 10x
Réflexions finales
Le partitionnement de votre base de données est sans aucun doute un excellent moyen d'améliorer les performances, en particulier sur les bases de données volumineuses. Cependant, le faire sans tenir compte de votre domaine d'application particulier peut être une erreur ou conduire à une solution inefficace. Au lieu de cela, prendre son temps pour étudier le domaine en interrogeant des experts et vos utilisateurs et en examinant les requêtes les plus exécutées est crucial pour concevoir des critères de partitionnement très efficaces. Cet article vous a montré comment procéder et a démontré les résultats d'une telle approche à travers une étude de cas réelle.