Qu'est-ce qu'une Forme Normale et devez-vous les respecter ?
[English version available here]
En comparant les SGBD et les bases NoSQL, la partie "schéma" est souvent utilisée. Dans le monde NoSQL, vous voyez les "schema-less" ou "no schema" comme un avantage. Si cela peut être vrai parfois, vos applications s'appuieront sur un schéma de quelque manière que ce soit. Vous finissez toujours par présenter des données à l'utilisateur final, et vous comptez sur l'interface utilisateur pour corréler les données entre les écrans.
La définition du schéma est un point important lors de l'utilisation d'une base de données, car elle contribuera (ou non !) aux performances, à la cohérence, au stockage, etc. Lorsqu'il s'agit de concevoir un schéma, vous vous appuierez généralement sur une forme normale (NF) pour vous assurer qu'il est conçu correctement. Tout comme lorsque vous utilisez UML pour vos applications.
Qu'est-ce que la forme normale ?
Une NF est un ensemble de règles permettant de définir votre schéma, c'est-à-dire la manière dont vous stockez vos données, afin d'éviter tout problème de lecture de ce que vous avez stocké.
Je ne discuterai pas de toutes les NF existantes, mais seulement des 5 plus utilisés.
1NF
La première NF est la plus simple. Toutes vos tables doivent avoir une clé primaire, et vous ne pouvez pas mélanger les types de données ou utiliser des valeurs composées dans une colonne.
C'est tout !
2NF
Pour respecter la 2NF, tous les attributs non clés doivent dépendre de la PK entière.
Si vous avez une table où vous avez un produit, un entrepôt et la quantité :
Si vous avez la PK sur ProductID et Warehouse, tout va bien. Mais si vous décidez d'ajouter l'évaluation pour le produit, vous ne respecterez pas 2NF et vous vous exposez à un problème.
3NF
Pour la 3NF, chaque attribut doit dépendre de la clé, et uniquement de la PK !
Cela signifie que vous devrez introduire davantage de relations entre vos données pour en assurer la cohérence.
Imaginons que vous gérez un magasin de métadonnées pour un jeu. Pour ce jeu, vous décidez d'avoir une table pour stocker le niveau des joueurs comme ceci :
Pour qu'un joueur atteigne un certain niveau, il doit obtenir un certain classement. Le concepteur du jeu a défini que du niveau 1 à 4 il sera low, 5-8 sera medium et 9-10 sera advanced. Si vous ajoutez une colonne au tableau, vous ne respecterez pas la 3NF et risquez l'incohérence.
Vous pouvez l'éviter dans la logique de votre application, mais l'adoption de la 3NF empêchera ce genre de problème par conception.
4NF
4NF introduit une nouvelle règle sur la façon dont vous gérez le PK.
Imaginez que vous gérez un catalogue de voitures. Vous pouvez définir vos voitures avec le nom du modèle, les couleurs disponibles et un type. Toutes les couleurs ne sont pas disponibles pour tous les modèles. De même pour les types. Mais pour un modèle, toutes les couleurs seront les mêmes, quel que soit le type.
Ainsi, vous pouvez avoir le modèle Z en bleu et en vert et dans le style berline ou SUV par exemple. Une table ne respectant pas 4NF pourrait ressembler à ceci :
Mais que se passe-t-il lorsqu'une nouvelle couleur est disponible pour le modèle Z ? Pour respecter la 4NF, il faut diviser en 2 tables, une avec les couleurs disponibles et une pour les types.
5NF
Une fois encore, tout est question d'attributs et de gestion des PK. En 5NF, chaque dépendance de jointure doit reposer sur un PK clair.
L'idée est d'éviter les tuples redondants, et comme dans l'exemple en 4NF, vous finirez par diviser vos tables une fois de plus.
Schema-less ou Forme Normale
La définition de la forme normale est vraiment utile pour prévenir les incohérences ou les anomalies dans votre ensemble de données par conception. La redondance de vos données n'est plus le même problème aujourd'hui en raison de la diminution des coûts de stockage. Mais il est toujours préférable de prévenir les anomalies de votre service ou votre application afin de limiter l'impact sur les clients/utilisateurs .
Travailler avec une base de données de documents conduit généralement à se répéter dans plusieurs documents. Pour éviter les anomalies, il suffit de stocker toutes les données pertinentes et/ou liées dans un seul document. Ne vous inquiétez pas de la redondance, le stockage est bon marché. Mais le réseau est-il bon marché ? Si vous hébergez votre application dans le cloud, le coût du réseau sortant peut être élevé. Si vous ne disposez pas d'un moyen d'extraire moins d'attributs de vos documents, le coût du réseau augmentera... considérablement !
Lorsqu'il s'agit de SGBDR, le coût sera payé en calcul. Les jointures peuvent être facilement gérées entre deux tables, mais lorsque vous avez 10 tables, la latence de la requête augmentera.
Dénormalisation
Vous pouvez dénormaliser votre schéma afin d'améliorer les performances. Dans ce cas, il stockera simplement les données redondantes en fonction des besoins spécifiques des requêtes. Habituellement, cela n'est fait que pour :
- Performance: éviter les jointures complexes en interrogeant une seule table par la clé.
- Simplifier le schéma: limiter les schémas complexes avec des clés étrangères en cascade.
- Gestion de la charge en écriture : limiter le nombre d'écritures requises pour certaines insertions/mises à jour pourrait aider la base de données à gérer la charge.
Gardez à l'esprit que la dénormalisation augmentera le risque d'incohérence des données et rendra plus difficile la maintenance du schéma pour s'assurer qu'il soutient toujours sur les requêtes que vous utilisez.
Comment faire?
"What you must learn is that these rules are no different than the rules of a computer system. Some of them can be bent. Others can be broken." - Morpheus, 1999
Rien n'est gravé dans la pierre en matière de NF. Travailler sur une application ou une API implique de comprendre ce que l'on doit réaliser, où sont les chemins à risque, quelles sont les données les plus utilisées, etc.
En utilisant des vues et/ou des index couvrant peut aider à améliorer les performances en formatant ou en stockant les données différemment tout en les gardant "propres" lorsqu'il s'agit de les écrire.
L'application du pattern CQRS dans votre conception vous aidera certainement à séparer vos besoins d'accès aux données et à réfléchir à la conception de votre schéma. Et si vous souhaitez conserver une certaine marge de manœuvre pour l'évolution des données, vous pouvez tirer parti de l'utilisation du type JSONB.
En bref, vous devez faire ce qui est le mieux pour vos besoins spécifiques et ne pas suivre aveuglément un ensemble de règles. Revenez aux bases de la conception de systèmes et examinez les besoins de votre application. Définissez votre schéma avec tous les composants permettant de récupérer les données, comme les index et les vues dans le respect de vos besoins et contraintes de performances et de stockage. Ne suivez pas strictement les NF, appliquez-les simplement lorsque cela est utile et peut éviter tout problème, et contournez-les si nécessaire pour que votre système fonctionne correctement.
Nous avons vu de nombreux services fonctionnant parfaitement sur des bases de données Document, Graph, etc., alors n'ayez pas peur de ne pas respecter la NF lorsque vous utilisez un SGBDR. Pensez simplement à ce dont vous avez besoin et à la façon dont votre base de données spécifique fonctionne.
Le ratio écriture/lecture a un impact énorme sur votre décision.
Si votre application compte 90 % d'écritures, avez-vous vraiment besoin d'un temps de réponse inférieur à la milliseconde pour les lectures ? Devez-vous concevoir votre schéma en tenant compte d'une NF et augmenter la latence en écriture ? Dans ces cas d'utilisation, vous pouvez éviter d'avoir des écritures trop longues (et la contention subséquente sur les transactions) en n'utilisant simplement pas de schéma normalisé.
Lorsque les lectures atteignent 90 %, les index et les vues font généralement l'affaire. Vous rencontrerez des latences en écriture, mais les lectures ne seront pas un obstacle pour l'application, surtout si vous utilisez des index couvrant (attention à l'impact sur le stockage).
Enfin, sur un ratio 50-50, ayez une compréhension claire des besoins de l'application. Ne pas normaliser un schéma ne signifie pas qu'il ne peut pas contenir de données normalisées ! Vous pouvez normaliser là où c'est préférable ou nécessaire et dénormaliser là où c'est utile, et avoir le "meilleur des deux mondes" dans votre schéma.