Il miglior design per fare riferimento a più tabelle da una singola colonna?


18

Schema proposto

Innanzitutto, ecco un esempio del mio schema proposto da fare riferimento in tutto il mio 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

Dichiarazione problema

Ho una tavola dei vestiti che ha colonne come nome, colore, prezzo, brandid e così via per descrivere gli attributi di un particolare capo di abbigliamento.

Ecco il mio problema: diversi marchi di abbigliamento richiedono informazioni diverse. Qual è la migliore pratica per affrontare un problema come questo?

Si noti che per i miei scopi, è necessario trovare informazioni specifiche sul marchio a partire da una voce di abbigliamento . Questo perché visualizzo prima le informazioni da una voce di abbigliamento all'utente, dopodiché devo utilizzare le informazioni specifiche del marchio per acquistare l'articolo. In sintesi, deve esserci una relazione direzionale tra i vestiti (da) e le tabelle brand_x .

Soluzione proposta / attuale

Per far fronte a questo, ho pensato al seguente schema di progettazione:

La tabella dei vestiti avrà una colonna di marca che può avere valori di id che vanno da 1 a x, dove un particolare ID corrisponde a una tabella specifica del marchio. Ad esempio, il valore ID 1 corrisponderà alla tabella brand_1 (che potrebbe avere una colonna url ), l'id 2 corrisponderà a brand_2 (che potrebbe avere una colonna fornitore ), ecc.

Pertanto, per associare una particolare voce di abbigliamento alle informazioni specifiche del marchio, immagino che la logica a livello di applicazione sarà simile a questa:

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.

Altri commenti e pensieri

Sto tentando di normalizzare il mio intero database in BCNF e, sebbene sia quello che mi è venuto in mente, il codice dell'applicazione risultante mi fa sentire molto ansioso. Non c'è modo di imporre relazioni se non a livello di applicazione, e quindi il design sembra molto confuso e, presumo, molto soggetto a errori.

Ricerca

Mi sono assicurato di controllare le voci precedenti prima di scrivere un post. Ecco un post con un problema quasi identico che sono riuscito a trovare. Ho comunque pubblicato questo post perché sembra che l'unica risposta fornita non abbia una soluzione basata su design o SQL (ovvero menzioni OOP, ereditarietà e interfacce).

Sono anche un po 'alle prime armi quando si tratta di progettazione di database, e quindi apprezzerei qualsiasi intuizione.


Sembra che ci siano risposte più utili su Stack Overflow:

Ho fatto riferimento alle soluzioni e suggerisco ad altri di trovare la mia domanda.

Nonostante i link sopra indicati, sono ancora alla ricerca di risposte qui e apprezzerei tutte le soluzioni fornite!

Sto usando PostgreSQL.

Risposte:


7

Personalmente non mi piace usare uno schema multi-tabella per questo scopo.

  • È difficile garantire l'integrità.
  • È difficile da mantenere.
  • È difficile filtrare i risultati.

Ho impostato un campione dbfiddle .

Il mio schema di tabella proposto:

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)
);

Vorrei inserire alcuni dati:

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');

Se devi recuperare attributi comuni:

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

Oppure puoi facilmente ottenere vestiti per marchio:

Dammi tutti i vestiti di 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

Ma per me, uno dei migliori di questo schema è che puoi filtrare per Attibutes:

Dammi tutti i vestiti che ha l'attributo: Dimensione

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

Utilizzando uno schema a più tabelle, qualunque delle query precedenti richiederà di gestire un numero illimitato di tabelle o campi XML o JSON.

Un'altra opzione con questo schema è che è possibile definire modelli, ad esempio, è possibile aggiungere una nuova tabella BrandAttrTemplates. Ogni volta che aggiungi un nuovo record, puoi utilizzare un trigger o un SP per generare un set di attributi predefiniti per questo Branch.

Mi dispiace, vorrei estendere le mie spiegazioni, penso che sia più chiaro del mio inglese.

Aggiornare

La mia risposta attuale dovrebbe funzionare su qualsiasi RDBMS. Secondo i tuoi commenti, se hai bisogno di filtrare i valori degli attributi, suggerirei piccole modifiche.

Per quanto MS-Sql non consenta le matrici, ho impostato un nuovo esempio mantenendo lo stesso schema di tabella, ma cambiando AttrValue in un tipo di campo ARRAY.

In effetti, usando POSTGRES, puoi approfittare di questo array usando un indice GIN.

(Lasciami dire che @EvanCarrol ha una buona conoscenza di Postgres, sicuramente meglio di me. Ma lasciami aggiungere un po '.)

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}');

Ora puoi anche eseguire una query utilizzando i singoli valori degli attributi come:

Dammi un elenco di tutti i pantaloni Taglia: 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;

Questo è il risultato:

clothes name | brand name | attribute | values 
------------- ------------ ----------  ---------------- 
Pants          Brand1       Size        32, 33, 34

Mi piace molto questa spiegazione, ma sembra che stiamo solo scambiando uno schema multi-tabella per avere quei CSV multipli in una singola colonna - se questo ha senso. D'altra parte, mi sento come se mi piacesse questo approccio meglio perché non richiede modifiche allo schema, ma di nuovo sembra che stiamo spingendo il problema altrove (vale a dire con colonne di lunghezza variabile). Questo può essere un problema; e se volessi interrogare pantaloni di taglia 3 nel DB? Forse non esiste una soluzione bella e pulita a questo tipo di problema. C'è un nome per questo concetto in modo che potrei forse approfondire di più?
youngrrrr

In realtà ... per rispondere al problema che ho posto, forse la risposta può essere presa in prestito dalla soluzione di @ EvanCarroll: vale a dire, usando i tipi jsonb anziché semplicemente TEXT / STRINGS in formato CSV. Ma ancora una volta - se c'è un nome per questo concetto, per favore fatemi sapere!
youngrrrr

1
È un tipo di soluzione Valore attributo entità. Non è un cattivo compromesso tra prestazioni e buon design. È un compromesso, però. Scambia alcune prestazioni per un design più pulito, non disseminato di infinite tabelle "Brand_X". La penalità prestazionale, che va dalla direzione più indicata, dovrebbe essere minima. Andare dall'altra parte sarà più doloroso, ma questo è il compromesso. en.wikipedia.org/wiki/…
Jonathan Fite

4

Quello che stai descrivendo è, almeno in parte, un catalogo prodotti. Hai diversi attributi comuni a tutti i prodotti. Questi appartengono a una tabella ben normalizzata.

Oltre a ciò, hai una serie di attributi che sono specifici del marchio (e mi aspetto che possano essere specifici del prodotto). Cosa deve fare il tuo sistema con questi attributi specifici? Hai una logica aziendale che dipende dallo schema di questi attributi o li stai semplicemente elencando in una serie di coppie "etichetta": "valore"?

Altre risposte suggeriscono di usare un approccio essenzialmente CSV (che sia JSONo ARRAYmeno) - Questi approcci rinunciano alla normale gestione dello schema relazionale spostando lo schema dai metadati e nei dati stessi.

Esiste un modello di progettazione portatile per questo che si adatta molto bene ai database relazionali. È EAV (entità-attributo-valore). Sono sicuro che hai letto in molti, molti posti che "EAV è il male" (ed è). Tuttavia, esiste una particolare applicazione in cui i problemi con EAV non sono importanti, ovvero i cataloghi di attributi di prodotto.

Tutti i soliti argomenti contro EAV non si applicano a un catalogo delle funzionalità del prodotto, poiché i valori delle funzionalità del prodotto vengono generalmente rigurgitati in un elenco o nel caso peggiore in una tabella di confronto.

L'utilizzo di un JSONtipo di colonna consente di imporre eventuali vincoli di dati al di fuori del database e li impone nella logica dell'applicazione. Inoltre, l'utilizzo di una tabella di attributi per ogni marchio presenta i seguenti svantaggi:

  • Non si adatta bene se alla fine hai centinaia di marchi (o più).
  • Se si modificano gli attributi consentiti su un marchio, è necessario modificare la definizione di una tabella anziché aggiungere o rimuovere righe in una tabella di controllo del campo del marchio.
  • Potresti comunque finire con tabelle scarsamente popolate se il marchio ha molte funzionalità potenziali, solo un piccolo sottoinsieme delle quali è noto.

Non è particolarmente difficile recuperare dati su un prodotto con caratteristiche specifiche del marchio. È senza dubbio più facile creare un SQL dinamico utilizzando il modello EAV che utilizzare il modello tabella per categoria. Nella tabella per categoria, devi riflettere (o il tuo JSON) per scoprire quali sono i nomi delle colonne delle caratteristiche. Quindi è possibile creare un elenco di elementi per una clausola where. Nel modello EAV, WHERE X AND Y AND Zdiventa INNER JOIN X INNER JOIN Y INNER JOIN Z, quindi la query è un po 'più complicata, ma la logica per creare la query è ancora totalmente guidata dalla tabella e sarà più che abbastanza scalabile se si hanno gli indici corretti.

Ci sono molte ragioni per non usare l'EAV come approccio generale. Questi motivi non si applicano al catalogo delle funzionalità di un prodotto, quindi non c'è nulla di sbagliato in EAV in questa specifica applicazione.

A dire il vero, questa è una risposta breve per un argomento complesso e controverso. Ho già risposto a domande simili prima e ho approfondito l'avversione generale nei confronti di EAV. Per esempio:

Direi che EAV è usato meno spesso ultimamente di quanto non fosse prima, per ragioni principalmente buone. Tuttavia, penso anche che non sia ben compreso.


3

Ecco il mio problema: diversi marchi di abbigliamento richiedono informazioni diverse. Qual è la migliore pratica per affrontare un problema come questo?

Utilizzando JSON e PostgreSQL

Penso che stai rendendo questo più difficile di quanto deve essere e ti verrà morso con esso in seguito. Non è necessario il modello di entità-attributo-valore a meno che non sia effettivamente necessario 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
);

Non c'è assolutamente nulla di sbagliato in questo schema.

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 )
;

Ora puoi interrogarlo usando un semplice join

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

E tutti gli operatori JSON lavorano in una clausola where.

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

Come nota a margine, non inserire gli URL nel database. Cambiano nel tempo. Basta creare una funzione che li prende.

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

o qualunque altra cosa. Se stai usando PostgreSQL puoi anche usare gli hashids .

Anche di nota speciale, jsonbè memorizzato come binario (quindi -b) ed è anche indicizzabile, o SARGable o qualunque altra cosa i ragazzi carini lo chiamino in questi giorni:CREATE INDEX ON brands USING gin ( attributes );

La differenza qui sta nella semplicità della query ..

Dammi tutti i vestiti di Brand2

SELECT * FROM clothes WHERE brand_id = 2;

Dammi tutti i vestiti che ha l'attributo: Dimensione

SELECT * FROM clothes WHERE attributes ? 'size';

Che ne dici di uno diverso ...

Dammi tutti i vestiti e gli attributi per tutti gli abiti disponibili in grande.

SELECT * FROM clothes WHERE attributes->>'size' = 'large';

Quindi, se capisco correttamente, l'essenza di ciò che hai detto è se esiste una relazione tra marchi e attributi (ovvero se sia valida o meno), la soluzione di McNets sarebbe preferita (ma le domande sarebbero più costose / più lente). D'altra parte, se questa relazione non è importante / più "ad-hoc", allora si potrebbe preferire la soluzione. Puoi spiegarci un po 'di più con cosa intendevi quando hai detto "Non lo userei mai con PostgreSQL?" Non sembrava esserci una spiegazione a quel commento. Ci scusiamo per tutte le domande !! Apprezzo molto le tue risposte finora :)
youngrrrr

1
Esiste chiaramente una relazione, l'unica domanda è quanto è necessario gestirla. Se sto usando un termine vago come proprietà , attributi o simili, di solito intendo dire che è praticamente ad-hoc o altamente non strutturato. Per questo, JSONB è semplicemente migliore perché è più semplice. puoi trovare questo post informativo coussej.github.io/2016/01/14/…
Evan Carroll

-1

Una soluzione semplice è quella di includere tutti i possibili attributi come colonne sul tavolo principale e rendere nulle tutte le colonne specifiche del marchio. Questa soluzione interrompe la normalizzazione del database, ma è molto facile da implementare.


Penso ... Ho un'idea di quello che stai dicendo, ma può essere utile includere più dettagli e forse anche un esempio.
youngrrrr
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.