L'utilisation de plusieurs clés étrangères séparées par des virgules est-elle incorrecte, et si oui, pourquoi?

31

Il y a deux tableaux: Dealet DealCategories. Une transaction peut avoir plusieurs catégories de transactions.

Donc, la bonne façon devrait être de créer un tableau appelé DealCategoriesavec la structure suivante:

DealCategoryId (PK)
DealId (FK)
DealCategoryId (FK)

Cependant, notre équipe d'externalisation a stocké les multiples catégories dans le Dealtableau de cette façon:

DealId (PK)
DealCategory -- In here they store multiple deal ids separated by commas like this: 18,25,32.

J'ai l'impression que ce qu'ils ont fait est mal, mais je ne sais pas comment expliquer clairement pourquoi ce n'est pas juste.

Comment dois-je leur expliquer que c'est faux? Ou peut-être que c'est moi qui me trompe et c'est acceptable?

Sarawut Positwinyu
la source
20
Vous avez raison. Le stockage d'une liste séparée par des virgules dans une colonne de base de données est-il vraiment si mauvais? . Réponse courte: Oui, c'est si mauvais.
ypercubeᵀᴹ
7
feu qui a externalisé l'équipe immédiatement avant de faire plus de mal ... (-_-)
Rafa

Réponses:

49

Oui, c'est une terrible idée.

Au lieu d'aller:

SELECT Deal.Name, DealCategory.Name
FROM Deal
  INNER JOIN
     DealCategories ON Deal.DealID = DealCategories.DealID
  INNER JOIN
     DealCategory ON DealCategories.DealCategoryID = DealCategory.DealCategoryID
WHERE Deal.DealID = 1234

Vous devez maintenant aller:

SELECT Deal.ID, Deal.Name, DealCategories
FROM Deal
WHERE Deal.DealID = 1234

Ensuite, vous devez faire des choses dans votre code d'application pour diviser cette liste de virgules en nombres individuels, puis interroger la base de données séparément:

SELECT DealCategory.Name
FROM DealCategory
WHERE DealCategory.DealCategoryID IN (<<that list from before>>)

Ce motif de conception découle soit d'une incompréhension complète de la modélisation relationnelle (vous n'avez pas à avoir peur des tableaux. Les tableaux sont vos amis. Utilisez-les), soit d'une croyance bizarrement erronée qu'il est plus rapide de prendre une liste séparée par des virgules et de la diviser dans le code d'application que pour ajouter une table de liens (ce n'est jamais le cas ). La troisième option est qu'ils ne sont pas suffisamment confiants / compétents avec SQL pour pouvoir configurer des clés étrangères, mais si c'est le cas, ils ne devraient rien avoir à voir avec la conception d'un modèle relationnel.

SQL Antipatterns (Karwin, 2010) consacre un chapitre entier à cet antipattern (qu'il appelle «Jaywalking»), pages 15-23. En outre, l'auteur a posté une question similaire sur SO . Les points clés qu'il note (appliqués à cet exemple) sont:

  • La recherche de toutes les offres dans une catégorie spécifique est plutôt compliquée (le moyen le plus simple de résoudre ce problème est une expression régulière, mais une expression régulière est un problème en soi).
  • Vous ne pouvez pas appliquer l'intégrité référentielle sans relations de clés étrangères. Si vous supprimez DealCategory nr. # 26, vous devez ensuite, dans votre code d'application, parcourir chaque transaction à la recherche de références à la catégorie # 26 et les supprimer. C'est quelque chose qui devrait être géré au niveau de la couche de données, et devoir le gérer dans votre application est une très mauvaise chose .
  • Les requêtes agrégées ( COUNT, SUMetc.), là encore, varient de «compliquées» à «presque impossibles». Demandez à vos développeurs comment ils vous obtiendraient une liste de toutes les catégories avec un décompte du nombre d'offres dans cette catégorie. Avec une conception appropriée, cela représente quatre lignes de SQL.
  • Les mises à jour deviennent beaucoup plus difficiles (c'est-à-dire que vous avez un accord dans cinq catégories, mais que vous souhaitez en supprimer deux et en ajouter trois autres). C'est trois lignes de SQL avec une conception appropriée.
  • Finalement, vous rencontrerez des VARCHARlimitations de longueur de liste. Bien que si vous avez une liste séparée par des virgules de plus de 4000 caractères, il est probable que le monstre sera lent comme l'enfer de toute façon.
  • Extraire une liste de la base de données, la diviser, puis revenir à la base de données pour une autre requête est intrinsèquement plus lent qu'une requête.

TLDR: C'est une conception fondamentalement imparfaite, elle n'évolue pas bien, elle introduit une complexité supplémentaire, même pour les requêtes les plus simples, et dès le départ, elle ralentit votre application.

Simon Righarts
la source
1
Simon, quelqu'un a posé la même question ( dba.stackexchange.com/questions/17824/… ), mais je ne sais pas pourquoi les mêmes FK et PK sont dans le même tableau, qui freinent le 3FN.
jcho360
2
Je ne savais pas vraiment s'ils voulaient avoir une relation plusieurs-à-plusieurs entre les offres et les catégories, ou une sorte de hiérarchie des catégories. De toute façon, c'était une ligne de touche au point principal, qu'être des champs délimités par des virgules au lieu d'une table de liens est une mauvaise idée.
Simon Righarts
4

Cependant, notre équipe d'externalisation a stocké les multiples catégories dans le tableau des accords de cette façon:

DealId (PK) DealCategory - Ici, ils stockent plusieurs identifiants de transaction séparés par des virgules comme ceci: 18,25,32.

C'est en fait une bonne conception si vous avez seulement besoin de rechercher les catégories pour une offre donnée.

Mais c'est terrible si vous voulez connaître toutes les offres dans une catégorie donnée.

Et cela rend également très difficile et susceptible d'erreurs de faire autre chose - comme les mises à jour, les comptages, les jointures, etc.

La dénormalisation a sa place, mais vous devez garder à l'esprit qu'elle optimise pour un type de requête au détriment de toutes les autres que vous pourriez faire contre les mêmes données. Si vous savez que vous interrogerez toujours dans un même modèle, cela pourrait vous donner un avantage d'utiliser la conception dénormalisée. Mais s'il y a une chance que vous ayez besoin de plus de flexibilité dans les types de requêtes, respectez une conception normalisée.

Comme toute autre forme d'optimisation, vous devez savoir quelles requêtes vous allez exécuter avant de pouvoir décider si la dénormalisation est justifiée.

Bill Karwin
la source
1
Pensez-vous vraiment qu'une chaîne avec des ID enfants séparés par des virgules est utile? Je veux dire, l'application devait d'abord lire, puis analyser les identifiants et interroger tous les enfants, comme select * from DealCategories where DealId in (1,2,3,4,...). Vous avez plus d'expérience que moi en matière de conception de bases de données, alors peut-être avez-vous de bonnes raisons dans certains cas pour un tel "réglage extrême" dans des cas très spécifiques. Ma seule idée pour justifier cela est une selectcharge très élevée sur Deal / DealCategory. Cela me ressemble beaucoup à une équipe externalisée sans aucune connaissance en conception de base de données, au-delà de la création de tables, elle l'a créée.
Erik Hart
1
@ErikHart, il s'agit de dénormalisation, et cela peut être utile, mais mon point est qu'il dépend entièrement des requêtes que vous devez exécuter. Vous avez raison: la dénormalisation rend toutes les requêtes moins performantes, à l'exception de celle pour laquelle elle est optimisée. Si vous avez seulement besoin d'exécuter cette seule requête et que vous ne vous souciez pas des autres requêtes, c'est une victoire. Mais ce sont des cas rares, car généralement nous voulons de la flexibilité pour interroger les données de différentes manières.
Bill Karwin
1
@ErikHart, si cette équipe de sous-traitance avait reçu des spécifications de projet qui ne comprenaient qu'une seule requête par rapport à ces données, elle aurait pu concevoir une optimisation pour cette requête spécifique uniquement. En d'autres termes, "vous l'avez demandé, vous l'avez obtenu". Mais le fournisseur d'externalisation n'a aucune raison de planifier les utilisations futures des données - il implémente l'application à la lettre de ce qui est écrit dans la spécification.
Bill Karwin
1

Plusieurs valeurs dans une colonne sont contre la 1ère forme normale.

Ce n'est également absolument aucun gain de vitesse, car les tables doivent être liées dans la base de données. Vous devez d'abord lire et analyser une chaîne, puis sélectionner toutes les catégories pour le "Deal".

L'implémentation correcte serait une table de jonction comme "DealDealCategories", avec DealId et DealCategoryId.

Mauvaise implémentation de la hiérarchie?

En outre, un FK dans DealCategories vers un autre DealCategory ressemble à une mauvaise implémentation d'une hiérarchie / arborescence de DealCategories. Travailler avec des arbres via une relation Parent ID (appelée liste de contiguïté) est pénible!

Vérifiez les ensembles imbriqués (bons à lire, mais difficiles à modifier) ​​et les tables de fermeture (meilleures performances globales, mais peut-être une utilisation élevée de la mémoire - probablement pas trop pour vos DealCategories) lors de la mise en œuvre des hiérarchies!

Erik Hart
la source