La meilleure conception pour référencer plusieurs tables à partir d'une seule colonne?

18

Schéma proposé

Tout d'abord, voici un exemple de mon schéma proposé à référencer tout au long de mon post:

Clothes
---------- 
ClothesID (PK) INT NOT NULL
Name VARCHAR(50) NOT NULL
Color VARCHAR(50) NOT NULL
Price DECIMAL(5,2) NOT NULL
BrandID INT NOT NULL
...

Brand_1
--------
ClothesID (FK/PK) int NOT NULL
ViewingUrl VARCHAR(50) NOT NULL
SomeOtherBrand1SpecificAttr VARCHAR(50) NOT NULL

Brand_2
--------
ClothesID (FK/PK) int NOT NULL
PhotoUrl VARCHAR(50) NOT NULL
SomeOtherBrand2SpecificAttr VARCHAR(50) NOT NULL

Brand_X
--------
ClothesID (FK/PK) int NOT NULL
SomeOtherBrandXSpecificAttr VARCHAR(50) NOT NULL

Énoncé du problème

J'ai une table de vêtements qui a des colonnes comme le nom, la couleur, le prix, le brandid et ainsi de suite pour décrire les attributs d'un vêtement particulier.

Voici mon problème: différentes marques de vêtements nécessitent des informations différentes. Quelle est la meilleure pratique pour traiter un problème comme celui-ci?

Notez que pour mes besoins, il est nécessaire de trouver des informations spécifiques à la marque à partir d'une entrée de vêtements . C'est parce que j'affiche d'abord les informations d'une entrée de vêtements à l'utilisateur, après quoi je dois utiliser ses informations spécifiques à la marque pour acheter l'article. En résumé, il doit y avoir une relation directionnelle entre les vêtements (de) et la marque_x tables .

Solution proposée / actuelle

Pour y faire face, j'ai pensé au schéma de conception suivant:

La table de vêtements aura une colonne de marque qui peut avoir des valeurs d'ID allant de 1 à x, où un ID particulier correspond à une table spécifique à la marque. Par exemple, l'id valeur 1 correspondra à la table brand_1 (qui pourrait avoir une colonne url ), l'id 2 correspondra à brand_2 (qui pourrait avoir une colonne fournisseur ), etc.

Ainsi, pour associer une entrée de vêtements particulière à ses informations spécifiques à la marque, j'imagine que la logique au niveau de l'application ressemblera à ceci:

clothesId = <some value>
brand = query("SELECT brand FROM clothes WHERE id = clothesId")

if (brand == 1) {
    // get brand_1 attributes for given clothesId
} else if (brand == 2) {
    // get brand_2 attributes for given clothesId
} ... etc.

Autres commentaires et réflexions

J'essaie de normaliser l'ensemble de ma base de données dans BCNF, et bien que ce soit ce que j'ai trouvé, le code d'application qui en résulte me rend très anxieux. Il n'y a aucun moyen d'imposer des relations, sauf au niveau de l'application, et donc la conception semble très hacky et, je pense, très sujette aux erreurs.

Recherche

J'ai vérifié les entrées précédentes avant de publier un article. Voici un article avec un problème presque identique que j'ai réussi à trouver. J'ai quand même fait ce post car il semble que la seule réponse fournie ne dispose pas d'une solution SQL ou basée sur la conception (c'est-à-dire qu'elle mentionne la POO, l'héritage et les interfaces).

Je suis également un peu novice en ce qui concerne la conception de bases de données, et j'apprécierais donc toutes les informations.


Il semble qu'il y ait des réponses plus utiles sur Stack Overflow:

J'y ai fait référence aux solutions et je suggère à d'autres personnes qui trouvent ma question de le faire également.

Malgré les liens ci-dessus, je suis toujours à la recherche de réponses ici et j'apprécierais toute solution fournie!

J'utilise PostgreSQL.

youngrrrr
la source

Réponses:

7

Personnellement, je n'aime pas utiliser un schéma multi-tables à cet effet.

  • Il est difficile d'assurer l'intégrité.
  • C'est difficile à maintenir.
  • Il est difficile de filtrer les résultats.

J'ai défini un échantillon dbfiddle .

Mon schéma de table proposé:

CREATE TABLE #Brands
(
BrandId int NOT NULL PRIMARY KEY,
BrandName nvarchar(100) NOT NULL 
);

CREATE TABLE #Clothes
(
ClothesId int NOT NULL PRIMARY KEY,
ClothesName nvarchar(100) NOT NULL 
);

-- Lookup table for known attributes
--
CREATE TABLE #Attributes
(
AttrId int NOT NULL PRIMARY KEY,
AttrName nvarchar(100) NOT NULL 
);

-- holds common propeties, url, price, etc.
--
CREATE TABLE #BrandsClothes
(
BrandId int NOT NULL REFERENCES #Brands(BrandId),
ClothesId int NOT NULL REFERENCES #Clothes(ClothesId),
VievingUrl nvarchar(300) NOT NULL,
Price money NOT NULL,
PRIMARY KEY CLUSTERED (BrandId, ClothesId),
INDEX IX_BrandsClothes NONCLUSTERED (ClothesId, BrandId)
);

-- holds specific and unlimited attributes 
--
CREATE TABLE #BCAttributes
(
BrandId int NOT NULL REFERENCES #Brands(BrandId),
ClothesId int NOT NULL REFERENCES #Clothes(ClothesId),
AttrId int NOT NULL REFERENCES #Attributes(AttrId),
AttrValue nvarchar(300) NOT NULL,
PRIMARY KEY CLUSTERED (BrandId, ClothesId, AttrId),
INDEX IX_BCAttributes NONCLUSTERED (ClothesId, BrandId, AttrId)
);

Permettez-moi d'insérer quelques données:

INSERT INTO #Brands VALUES 
(1, 'Brand1'), (2, 'Brand2');

INSERT INTO #Clothes VALUES 
(1, 'Pants'), (2, 'T-Shirt');

INSERT INTO #Attributes VALUES
(1, 'Color'), (2, 'Size'), (3, 'Shape'), (4, 'Provider'), (0, 'Custom');

INSERT INTO #BrandsClothes VALUES
(1, 1, 'http://mysite.com?B=1&C=1', 123.99),
(1, 2, 'http://mysite.com?B=1&C=2', 110.99),
(2, 1, 'http://mysite.com?B=2&C=1', 75.99),
(2, 2, 'http://mysite.com?B=2&C=2', 85.99);

INSERT INTO #BCAttributes VALUES
(1, 1, 1, 'Blue, Red, White'),
(1, 1, 2, '32, 33, 34'),
(1, 2, 1, 'Pearl, Black widow'),
(1, 2, 2, 'M, L, XL'),
(2, 1, 4, 'Levis, G-Star, Armani'),
(2, 1, 3, 'Slim fit, Regular fit, Custom fit'),
(2, 2, 4, 'G-Star, Armani'),
(2, 2, 3, 'Slim fit, Regular fit'),
(2, 2, 0, '15% Discount');

Si vous devez récupérer des attributs communs:

SELECT     b.BrandName, c.ClothesName, bc.VievingUrl, bc.Price
FROM       #BrandsClothes bc
INNER JOIN #Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN #Clothes c
ON         c.ClothesId = bc.ClothesId
ORDER BY   bc.BrandId, bc.ClothesId;

BrandName   ClothesName   VievingUrl                  Price
---------   -----------   -------------------------   ------
Brand1      Pants         http://mysite.com?B=1&C=1   123.99
Brand1      T-Shirt       http://mysite.com?B=1&C=2   110.99
Brand2      Pants         http://mysite.com?B=2&C=1    75.99
Brand2      T-Shirt       http://mysite.com?B=2&C=2    85.99

Ou vous pouvez facilement obtenir des vêtements par marque:

Donnez-moi tous les vêtements de Brand2

SELECT     c.ClothesName, b.BrandName, a.AttrName, bca.AttrValue
FROM       #BCAttributes bca
INNER JOIN #BrandsClothes bc
ON         bc.BrandId = bca.BrandId
AND        bc.ClothesId = bca.ClothesId
INNER JOIN #Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN #Clothes c
ON         c.ClothesId = bc.ClothesId
INNER JOIN #Attributes a
ON         a.AttrId = bca.AttrId
WHERE      bca.ClothesId = 2
ORDER BY   bca.ClothesId, bca.BrandId, bca.AttrId;

ClothesName   BrandName   AttrName   AttrValue
-----------   ---------   --------   ---------------------
T-Shirt       Brand1      Color      Pearl, Black widow
T-Shirt       Brand1      Size       M, L, XL
T-Shirt       Brand2      Custom     15% Discount
T-Shirt       Brand2      Shape      Slim fit, Regular fit
T-Shirt       Brand2      Provider   G-Star, Armani

Mais pour moi, l'un des meilleurs de ce schéma est que vous pouvez filtrer par Attibutes:

Donnez-moi tous les vêtements qui ont l'attribut: Taille

SELECT     c.ClothesName, b.BrandName, a.AttrName, bca.AttrValue
FROM       #BCAttributes bca
INNER JOIN #BrandsClothes bc
ON         bc.BrandId = bca.BrandId
AND        bc.ClothesId = bca.ClothesId
INNER JOIN #Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN #Clothes c
ON         c.ClothesId = bc.ClothesId
INNER JOIN #Attributes a
ON         a.AttrId = bca.AttrId
WHERE      bca.AttrId = 2
ORDER BY   bca.ClothesId, bca.BrandId, bca.AttrId;

ClothesName   BrandName   AttrName   AttrValue
-----------   ---------   --------   ----------
Pants         Brand1      Size       32, 33, 34
T-Shirt       Brand1      Size       M, L, XL

L'utilisation d'un schéma multi-tables, quelles que soient les requêtes précédentes, nécessitera de traiter un nombre illimité de tables ou des champs XML ou JSON.

Une autre option avec ce schéma est que vous pouvez définir des modèles, par exemple, vous pouvez ajouter une nouvelle table BrandAttrTemplates. Chaque fois que vous ajoutez un nouvel enregistrement, vous pouvez utiliser un déclencheur ou un SP pour générer un ensemble d'attributs prédéfinis pour cette branche.

Je suis désolé, je voudrais étendre mes explications par je pense que c'est plus clair que mon anglais.

Mise à jour

Ma réponse actuelle devrait fonctionner quel que soit le SGBDR. Selon vos commentaires, si vous avez besoin de filtrer les valeurs des attributs, je suggérerais de petites modifications.

Dans la mesure où MS-Sql n'autorise pas les tableaux, j'ai configuré un nouvel exemple contenant le même schéma de table, mais en changeant AttrValue en type de champ ARRAY.

En fait, en utilisant POSTGRES, vous pouvez profiter de ce tableau en utilisant un index GIN.

(Permettez-moi de dire que @EvanCarrol a une bonne connaissance de Postgres, certainement mieux que moi. Mais permettez-moi d'ajouter ma part.)

CREATE TABLE BCAttributes
(
BrandId int NOT NULL REFERENCES Brands(BrandId),
ClothesId int NOT NULL REFERENCES Clothes(ClothesId),
AttrId int NOT NULL REFERENCES Attrib(AttrId),
AttrValue text[],
PRIMARY KEY (BrandId, ClothesId, AttrId)
);

CREATE INDEX ix_attributes on BCAttributes(ClothesId, BrandId, AttrId);
CREATE INDEX ix_gin_attributes on BCAttributes using GIN (AttrValue);


INSERT INTO BCAttributes VALUES
(1, 1, 1, '{Blue, Red, White}'),
(1, 1, 2, '{32, 33, 34}'),
(1, 2, 1, '{Pearl, Black widow}'),
(1, 2, 2, '{M, L, XL}'),
(2, 1, 4, '{Levis, G-Star, Armani}'),
(2, 1, 3, '{Slim fit, Regular fit, Custom fit}'),
(2, 2, 4, '{G-Star, Armani}'),
(2, 2, 3, '{Slim fit, Regular fit}'),
(2, 2, 0, '{15% Discount}');

Maintenant, vous pouvez également interroger en utilisant des valeurs d'attributs individuels comme:

Donnez-moi une liste de tous les pantalons Taille: 33

AttribId = 2 AND ARRAY['33'] && bca.AttrValue

SELECT     c.ClothesName, b.BrandName, a.AttrName, array_to_string(bca.AttrValue, ', ')
FROM       BCAttributes bca
INNER JOIN BrandsClothes bc
ON         bc.BrandId = bca.BrandId
AND        bc.ClothesId = bca.ClothesId
INNER JOIN Brands b
ON         b.BrandId = bc.BrandId
INNER JOIN Clothes c
ON         c.ClothesId = bc.ClothesId
INNER JOIN Attrib a
ON         a.AttrId = bca.AttrId
WHERE      bca.AttrId = 2
AND        ARRAY['33'] && bca.AttrValue
ORDER BY   bca.ClothesId, bca.BrandId, bca.AttrId;

Voici le résultat:

clothes name | brand name | attribute | values 
------------- ------------ ----------  ---------------- 
Pants          Brand1       Size        32, 33, 34
McNets
la source
J'aime vraiment cette explication, mais il semble que nous échangeons simplement un schéma multi-tables pour avoir ces multiples CSV dans une seule colonne - si cela a du sens. D'un autre côté, j'ai l'impression que j'aime mieux cette approche car elle ne nécessite aucune modification du schéma, mais encore une fois, on a l'impression de pousser le problème ailleurs (à savoir en ayant des colonnes de longueur variable). Cela peut être un problème; Et si je voulais interroger un pantalon de taille 3 dans la DB? Il n'y a peut-être pas de solution propre et agréable à ce genre de problème. Y a-t-il un nom pour ce concept afin que je puisse peut-être y réfléchir davantage?
youngrrrr
En fait ... pour répondre au problème que j'ai posé, peut-être que la réponse peut être empruntée à la solution de @ EvanCarroll: à savoir, en utilisant des types jsonb au lieu de simplement TEXT / STRINGS au format CSV. Mais encore une fois - s'il y a un nom pour ce concept, faites-le moi savoir!
youngrrrr
1
Il s'agit d'un type de solution Entity Attribute Value. Ce n'est pas un mauvais compromis entre performances et bon design. C'est un compromis, cependant. Vous échangez des performances pour une conception plus propre, pas jonchée de tables "Brand_X" sans fin. La pénalité de performance, allant de votre direction la plus commune indiquée devrait être minime. Aller dans l'autre sens sera plus douloureux, mais c'est le compromis. en.wikipedia.org/wiki/…
Jonathan Fite
4

Ce que vous décrivez est, au moins en partie, un catalogue de produits. Vous disposez de plusieurs attributs communs à tous les produits. Ceux-ci appartiennent à un tableau bien normalisé.

Au-delà de cela, vous avez une série d'attributs spécifiques à la marque (et je pense que cela pourrait être spécifique au produit). Que doit faire votre système avec ces attributs spécifiques? Avez-vous une logique métier qui dépend du schéma de ces attributs ou les listez-vous simplement dans une série de paires "label": "value"?

D'autres réponses suggèrent d'utiliser ce qui est essentiellement une approche CSV (que ce JSONsoit ARRAYou non) - Ces approches renoncent à la gestion régulière du schéma relationnel en déplaçant le schéma hors des métadonnées et dans les données elles-mêmes.

Il existe un modèle de conception portable qui convient très bien aux bases de données relationnelles. C'est EAV (entité-attribut-valeur). Je suis sûr que vous avez lu dans de très nombreux endroits que "l'EAV est mauvais" (et il l'est). Cependant, il existe une application particulière où les problèmes liés à l'EAV ne sont pas importants, à savoir les catalogues d'attributs de produit.

Tous les arguments habituels contre EAV ne s'appliquent pas à un catalogue de fonctionnalités de produit, car les valeurs des fonctionnalités de produit ne sont généralement régurgitées que dans une liste ou, dans le pire des cas, dans un tableau de comparaison.

L'utilisation d'un JSONtype de colonne vous permet d'appliquer toutes les contraintes de données de la base de données et les force dans votre logique d'application. De plus, l'utilisation d'une table d'attributs pour chaque marque présente les inconvénients suivants:

  • Il n'évolue pas bien si vous avez finalement des centaines de marques (ou plus).
  • Si vous modifiez les attributs autorisés sur une marque, vous devez modifier une définition de table au lieu d'ajouter ou de supprimer simplement des lignes dans une table de contrôle de champ de marque.
  • Vous pouvez toujours vous retrouver avec des tables peu peuplées si la marque possède de nombreuses fonctionnalités potentielles, dont seul un petit sous-ensemble est connu.

Il n'est pas particulièrement difficile de récupérer des données sur un produit avec des fonctionnalités spécifiques à la marque. Il est sans doute plus facile de créer un SQL dynamique en utilisant le modèle EAV que ce ne serait en utilisant le modèle de table par catégorie. Dans le tableau par catégorie, vous avez besoin d'une réflexion (ou de votre JSON) pour savoir quels sont les noms des colonnes d'entités. Ensuite, vous pouvez créer une liste d'éléments pour une clause where. Dans le modèle EAV, le WHERE X AND Y AND Zdevient INNER JOIN X INNER JOIN Y INNER JOIN Z, donc la requête est un peu plus compliquée, mais la logique de construction de la requête est toujours totalement pilotée par une table et elle sera plus que suffisamment évolutive si vous avez construit les index appropriés.

Il y a beaucoup de raisons de ne pas utiliser l'EAV comme approche générale. Ces raisons ne s'appliquent pas à un catalogue de fonctionnalités de produit, il n'y a donc rien de mal avec EAV dans cette application spécifique.

Certes, il s'agit d'une réponse courte à un sujet complexe et controversé. J'ai déjà répondu à des questions similaires et je suis entré dans les détails de l'aversion générale à l'EAV. Par exemple:

Je dirais que l'EAV est utilisé moins souvent ces derniers temps qu'auparavant, pour la plupart du temps pour de bonnes raisons. Cependant, je pense que ce n'est pas non plus bien compris.

Joel Brown
la source
3

Voici mon problème: différentes marques de vêtements nécessitent des informations différentes. Quelle est la meilleure pratique pour traiter un problème comme celui-ci?

Utilisation de JSON et PostgreSQL

Je pense que vous rendez cela plus difficile que nécessaire et vous serez mordu plus tard. Vous n'avez pas besoin d'un modèle entité-attribut-valeur, sauf si vous avez réellement besoin d'EAV.

CREATE TABLE brands (
  brand_id     serial PRIMARY KEY,
  brand_name   text,
  attributes   jsonb
);
CREATE TABLE clothes (
  clothes_id   serial        PRIMARY KEY,
  brand_id     int           NOT NULL REFERENCES brands,
  clothes_name text          NOT NULL,
  color        text,
  price        numeric(5,2)  NOT NULL
);

Il n'y a absolument rien de mal à ce schéma.

INSERT INTO brands (brand_name, attributes)
VALUES
  ( 'Gucci', $${"luxury": true, "products": ["purses", "tawdry bougie thing"]}$$ ),
  ( 'Hugo Boss', $${"origin": "Germany", "known_for": "Designing uniforms"}$$ ),
  ( 'Louis Vuitton', $${"origin": "France", "known_for": "Designer Purses"}$$ ),
  ( 'Coco Chanel', $${"known_for": "Spying", "smells_like": "Banana", "luxury": true}$$ )
;

INSERT INTO clothes (brand_id, clothes_name, color, price) VALUES
  ( 1, 'Purse', 'orange', 100 ),
  ( 2, 'Underwear', 'Gray', 10 ),
  ( 2, 'Boxers', 'Gray', 10 ),
  ( 3, 'Purse with Roman Numbers', 'Brown', 10 ),
  ( 4, 'Spray', 'Clear', 100 )
;

Vous pouvez maintenant l'interroger à l'aide d'une simple jointure

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

Et l'un des opérateurs JSON fonctionne dans une clause where.

SELECT *
FROM brands
JOIN clothes
  USING (brand_id)
WHERE attributes->>'known_for' ILIKE '%Design%';

En remarque, ne mettez pas les URL dans la base de données. Ils changent avec le temps. Créez simplement une fonction qui les prend.

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

ou peu importe. Si vous utilisez PostgreSQL, vous pouvez même utiliser des hachages .

A noter également, jsonbest stocké sous forme binaire (donc le «-b») et il est également indexable, ou SARGable ou tout ce que les enfants cool l'appellent ces jours-ci:CREATE INDEX ON brands USING gin ( attributes );

La différence ici réside dans la simplicité de la requête.

Donnez-moi tous les vêtements de Brand2

SELECT * FROM clothes WHERE brand_id = 2;

Donnez-moi tous les vêtements qui ont l'attribut: Taille

SELECT * FROM clothes WHERE attributes ? 'size';

Que diriez-vous d'un autre ..

Donnez-moi tous les vêtements et attributs pour tous les vêtements disponibles en grand.

SELECT * FROM clothes WHERE attributes->>'size' = 'large';
Evan Carroll
la source
Donc, si je comprends bien, l'essentiel de ce que vous avez dit est qu'il existe une relation entre les marques et les attributs (c'est-à-dire si elle est valide ou non), alors la solution de McNets serait préférée (mais les requêtes seraient plus coûteuses / plus lentes). D'un autre côté, si cette relation n'est pas importante / plus "ad-hoc", alors on pourrait préférer votre solution. Pouvez-vous expliquer un peu plus ce que vous vouliez dire quand vous avez dit "je ne l'utiliserais jamais avec PostgreSQL?" Il ne semblait pas y avoir d'explication à ce commentaire. Désolé pour toutes ces questions!! J'apprécie vraiment vos réponses jusqu'à présent :)
youngrrrr
1
Il y a clairement une relation, la seule question est de savoir combien vous avez besoin pour la gérer. Si j'utilise un terme vague comme propriétés , attributs ou similaires, je veux généralement dire que c'est à peu près ad-hoc ou très non structuré. Pour cela, JSONB est juste meilleur car c'est plus simple. vous pouvez trouver ce post informatif coussej.github.io/2016/01/14/…
Evan Carroll
-1

Une solution simple consiste à inclure tous les attributs possibles en tant que colonnes sur la table principale des vêtements et à rendre toutes les colonnes spécifiques à la marque annulables. Cette solution rompt la normalisation de la base de données, mais est très facile à mettre en œuvre.

Matthew Sontum
la source
Je pense .. J'ai une idée de ce que vous dites, mais il peut être utile d'inclure plus de détails et peut-être aussi un exemple.
youngrrrr