Comparaison des schémas normalisés et dénormalisés
Une technique courante, popularisée par les solutions NoSQL, consiste à dénormaliser les données en l’absence de prise en charge de
JOIN, en stockant effectivement toutes les statistiques ou les lignes associées sur une ligne parente sous forme de colonnes et d’objets imbriqués. Par exemple, dans un schéma d’exemple pour un blog, nous pouvons stocker tous les Comments sous forme d’Array d’objets dans leurs posts respectifs.
Quand utiliser la dénormalisation
- Dénormalisez les tables qui changent rarement, ou pour lesquelles un délai avant la mise à disposition des données pour les requêtes analytiques est acceptable, c.-à-d. lorsque les données peuvent être entièrement rechargées par lot.
- Évitez de dénormaliser les relations plusieurs-à-plusieurs. Cela peut obliger à mettre à jour de nombreuses lignes lorsqu’une seule ligne source change.
- Évitez de dénormaliser les relations à forte cardinalité. Si chaque ligne d’une table a des milliers d’entrées associées dans une autre table, celles-ci devront être représentées sous forme d’
Array, soit d’un type primitif, soit de tuples. En général, les tableaux contenant plus de 1 000 tuples ne sont pas recommandés. - Au lieu de dénormaliser toutes les colonnes sous forme d’objets imbriqués, envisagez de ne dénormaliser qu’une statistique à l’aide de vues matérialisées (voir ci-dessous).
Évitez la dénormalisation sur des données fréquemment mises à jour
- Déclencher les bonnes instructions JOIN lorsqu’une ligne d’une table change. Idéalement, cela ne devrait pas entraîner la mise à jour de tous les objets de la jointure, mais uniquement de ceux qui sont affectés. Adapter les jointures pour filtrer efficacement les bonnes lignes, tout en maintenant un débit élevé, nécessite des outils externes ou un travail d’ingénierie spécifique.
- Les mises à jour de lignes dans ClickHouse doivent être gérées avec soin, ce qui ajoute de la complexité.
Un processus de mise à jour par lot est donc plus courant : tous les objets dénormalisés sont alors rechargés périodiquement.
Cas pratiques de dénormalisation
Posts déjà dénormalisée avec des statistiques telles que AnswerCount et CommentCount : les données source sont fournies sous cette forme. En pratique, nous pourrions vouloir normaliser ces informations, car elles sont susceptibles de changer fréquemment. Bon nombre de ces colonnes sont également disponibles dans d’autres tables ; par exemple, les commentaires d’un post sont accessibles via la colonne PostId et la table Comments. Pour les besoins de cet exemple, nous supposons que les posts sont rechargés via un traitement par lots.
Nous nous limitons également à la dénormalisation d’autres tables dans Posts, car nous considérons qu’il s’agit de notre table principale pour l’analytics. Une dénormalisation dans l’autre sens pourrait aussi convenir à certaines requêtes, avec les mêmes considérations que ci-dessus.
Pour chacun des exemples suivants, supposez qu’il existe une requête nécessitant l’utilisation des deux tables dans une jointure.
Posts et Votes
Score actuelle représente une telle statistique, c’est-à-dire le total des votes positifs moins les votes négatifs. Dans l’idéal, nous pourrions simplement récupérer ces statistiques au moment de la requête au moyen d’une simple recherche (voir les dictionnaires).
Users et Badges
Users et Badges :
Nous commençons par insérer les données avec la commande suivante :
Il peut être utile de dénormaliser vers les utilisateurs certaines statistiques provenant des badges, par exemple le nombre de badges. Nous en donnons un exemple lors de l’utilisation de dictionnaires pour ce jeu de données au moment de l’insert.
Post et PostLinks
PostLinks relient des Post que les utilisateurs considèrent comme associés ou comme des doublons. La requête suivante présente le schéma et la commande de chargement :
Exemple simple de statistique
INSERT INTO SELECT qui joint notre statistique de doublons à nos posts.
Exploiter les types complexes pour les relations un-à-plusieurs
- Tuples nommés - Ils permettent de représenter une structure associée sous la forme d’un ensemble de colonnes.
- Array(Tuple) ou Nested - Un tableau de tuples nommés, également appelé Nested, où chaque entrée représente un objet. S’applique aux relations un-à-plusieurs.
PostLinks dans Posts.
Chaque post peut contenir plusieurs liens vers d’autres posts, comme indiqué plus haut dans le schéma PostLinks. En tant que type Nested, nous pourrions représenter ces posts liés et dupliqués comme suit :
Notez l’utilisation du paramètre flatten_nested=0. Nous recommandons de désactiver l’aplatissement des données Nested.
Cette dénormalisation peut être effectuée à l’aide d’une requête INSERT INTO SELECT avec un OUTER JOIN :
Notez le temps d’exécution ici. Nous avons réussi à dénormaliser 66m de lignes en environ 2mins. Comme nous le verrons plus tard, il s’agit d’une opération que nous pouvons planifier.Notez l’utilisation des fonctions
groupArray pour regrouper PostLinks dans un tableau pour chaque PostId, avant la jointure. Ce tableau est ensuite filtré en deux sous-listes : LinkedPosts et DuplicatePosts, qui excluent également tout résultat vide provenant de l’OUTER JOIN.
Nous pouvons sélectionner quelques lignes pour voir notre nouvelle structure dénormalisée :
Orchestrer et planifier la dénormalisation
Traitement par lots
INSERT INTO SELECT. Cela convient à des transformations périodiques par lots.
Les utilisateurs disposent de plusieurs options pour orchestrer cela dans ClickHouse, à supposer qu’un processus périodique de chargement par lots soit acceptable :
- Vues matérialisées actualisables - Les vues matérialisées actualisables peuvent être utilisées pour planifier périodiquement une requête, dont les résultats sont envoyés vers une table cible. Lors de l’exécution de la requête, la vue garantit que la table cible est mise à jour de manière atomique. ClickHouse offre ainsi un moyen natif de planifier ce travail.
- Outils externes - Utiliser des outils tels que dbt et Airflow pour planifier périodiquement la transformation. La ClickHouse integration for dbt garantit que cette opération est effectuée de manière atomique, en créant une nouvelle version de la table cible, puis en l’échangeant de manière atomique avec la version qui reçoit les requêtes (via la commande EXCHANGE).