Bestes Design, um mehrere Tabellen aus einer Spalte zu referenzieren?


18

Vorgeschlagenes Schema

Zuallererst ist hier ein Beispiel für mein vorgeschlagenes Schema, auf das ich in meinem Beitrag verweisen möchte:

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

Problemstellung

Ich habe eine Kleidung Tabelle , die Spalten wie hat Namen, Farbe, Preis, brandid und so weiter zu beschreiben , die Attribute für ein bestimmtes Kleidungsstück.

Hier ist mein Problem: verschiedene Marken s Kleidungs erfordert unterschiedliche Informationen. Was ist die beste Vorgehensweise, um mit einem solchen Problem umzugehen?

Beachten Sie, dass für meine Zwecke markenspezifische Informationen ab einem Kleidungseintrag gesucht werden müssen. Dies liegt daran, dass ich dem Benutzer zuerst die Informationen aus einem Kleidungseintrag anzeige. Danach muss ich die markenspezifischen Informationen verwenden, um den Artikel zu kaufen. Zusammenfassend muss zwischen clothes (from) und den brand_x- Tabellen eine Richtungsbeziehung bestehen .

Vorgeschlagene / aktuelle Lösung

Um damit umzugehen, habe ich mir folgendes Designschema überlegt:

Die Kleidertabelle enthält eine Markenspalte mit ID-Werten von 1 bis x, wobei eine bestimmte ID einer markenspezifischen Tabelle entspricht. Zum Beispiel entspricht der ID-Wert 1 der Tabelle brand_1 (die möglicherweise eine URL- Spalte enthält), ID 2 entspricht brand_2 (die möglicherweise eine Lieferantenspalte enthält ) usw.

Um einen bestimmten Kleidungseintrag mit seinen markenspezifischen Informationen zu verknüpfen, stelle ich mir vor, dass die Logik auf Anwendungsebene ungefähr so ​​aussieht:

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.

Andere Kommentare und Gedanken

Ich versuche, meine gesamte Datenbank in BCNF zu normalisieren, und obwohl mir dies eingefallen ist, macht mir der resultierende Anwendungscode große Sorgen. Es gibt keine Möglichkeit, Beziehungen zu erzwingen, außer auf Anwendungsebene, und daher fühlt sich das Design sehr kitschig und, wie ich vermute, sehr fehleranfällig an.

Forschung

Ich habe mir die vorherigen Einträge angesehen, bevor ich einen Beitrag verfasst habe. Hier ist ein Beitrag mit einem nahezu identischen Problem, das ich gefunden habe. Ich habe diesen Beitrag trotzdem verfasst, weil es den Anschein hat, als ob die einzige Antwort, die bereitgestellt wird, keine SQL- oder designbasierte Lösung enthält (dh, sie erwähnt OOP, Vererbung und Schnittstellen).

Ich bin auch ein Anfänger, wenn es um Datenbankdesign geht, und würde mich über Erkenntnisse freuen.


Offenbar gibt es weitere hilfreiche Antworten zum Stapelüberlauf:

Ich habe dort auf die Lösungen verwiesen und anderen vorgeschlagen, die meine Frage finden, dies auch zu tun.

Trotz der oben angegebenen Links bin ich immer noch auf der Suche nach Antworten und würde mich über mögliche Lösungen freuen!

Ich benutze PostgreSQL.

Antworten:


7

Ich persönlich verwende für diesen Zweck kein Mehrtabellenschema.

  • Es ist schwer, Integrität zu gewährleisten.
  • Es ist schwer zu pflegen.
  • Es ist schwierig, Ergebnisse zu filtern.

Ich habe ein DBFiddle- Sample gesetzt .

Mein vorgeschlagenes Tabellenschema:

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

Lassen Sie mich einige Daten einfügen:

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

Wenn Sie allgemeine Attribute abrufen müssen:

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

Oder Sie können ganz einfach Kleidung nach Marke bekommen:

Gib mir alle Klamotten von 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

Aber für mich ist eines der besten dieses Schemas, dass Sie nach Attributen filtern können:

Gib mir alle Klamotten mit dem Attribut: Größe

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

Bei Verwendung eines Schemas mit mehreren Tabellen müssen bei allen vorherigen Abfragen eine unbegrenzte Anzahl von Tabellen oder XML- oder JSON-Felder verarbeitet werden.

Eine weitere Option in diesem Schema ist, dass Sie Vorlagen definieren können. Sie können beispielsweise eine neue Tabelle BrandAttrTemplates hinzufügen. Jedes Mal, wenn Sie einen neuen Datensatz hinzufügen, können Sie einen Trigger oder einen SP verwenden, um einen Satz vordefinierter Attribute für diesen Zweig zu generieren.

Es tut mir leid, ich möchte meine Erklärungen gerne erweitern, da ich denke, dass es klarer ist als mein Englisch.

Aktualisieren

Meine aktuelle Antwort sollte auf jedem RDBMS funktionieren. Laut Ihren Kommentaren würde ich kleine Änderungen vorschlagen, wenn Sie Attributwerte filtern müssen.

Soweit MS-Sql keine Arrays zulässt, habe ich ein neues Beispiel erstellt, das dasselbe Tabellenschema enthält, aber AttrValue in einen ARRAY-Feldtyp ändert.

Tatsächlich können Sie mit POSTGRES dieses Array mithilfe eines GIN-Index nutzen.

(Lassen Sie mich sagen, dass @EvanCarrol ein gutes Wissen über Postgres hat, sicherlich besser als ich. Aber lassen Sie mich mein bisschen hinzufügen.)

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

Jetzt können Sie zusätzlich mit einzelnen Attributen Werte abfragen wie:

Gib mir eine Liste aller Hosen Größe: 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;

Das ist das Ergebnis:

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

Ich mag diese Erklärung wirklich, aber es scheint, als würden wir nur ein Schema mit mehreren Tabellen gegeneinander austauschen, um diese mehreren CSVs in einer einzigen Spalte zu haben - wenn das Sinn macht. Auf der anderen Seite gefällt mir dieser Ansatz besser, da keine Änderungen am Schema erforderlich sind, aber es fühlt sich einfach so an, als würden wir das Problem an eine andere Stelle verschieben (nämlich durch Spalten mit variabler Länge). Dies kann ein Problem sein. was ist, wenn ich Hosen der Größe 3 in der DB abfragen wollte? Vielleicht gibt es keine schöne, saubere Lösung für diese Art von Problem. Gibt es einen Namen für dieses Konzept, damit ich es mir vielleicht genauer ansehen kann?
Youngrrrr

Eigentlich ... um das Problem zu beantworten, das ich gestellt habe, kann die Antwort vielleicht aus der @ EvanCarroll-Lösung entlehnt werden: indem man jsonb-Typen anstelle von TEXT / STRINGS im CSV-Format verwendet. Aber auch hier - wenn es einen Namen für dieses Konzept gibt, lassen Sie es mich bitte wissen!
Youngrrrr

1
Es handelt sich um einen Entitätsattributwert-Lösungstyp. Es ist kein schlechter Kompromiss zwischen Leistung und gutem Design. Es ist jedoch ein Kompromiss. Sie tauschen etwas Leistung gegen ein sauberes Design, das nicht mit endlosen "Brand_X" -Tabellen übersät ist. Die Performance-Beeinträchtigung, die von Ihrer angegebenen häufigsten Richtung ausgeht, sollte minimal sein. In die andere Richtung zu gehen wird schmerzhafter sein, aber das ist der Kompromiss. en.wikipedia.org/wiki/…
Jonathan Fite

4

Was Sie beschreiben, ist zumindest teilweise ein Produktkatalog. Sie haben mehrere Attribute, die allen Produkten gemeinsam sind. Diese gehören in eine gut normalisierte Tabelle.

Darüber hinaus haben Sie eine Reihe von Attributen, die markenspezifisch sind (und ich erwarte, dass sie produktspezifisch sein könnten). Was muss Ihr System mit diesen spezifischen Attributen tun? Haben Sie eine Geschäftslogik, die vom Schema dieser Attribute abhängt, oder listen Sie sie nur in einer Reihe von "label": "value" -Paaren auf?

Andere Antworten schlagen vor, einen CSV-Ansatz zu verwenden (ob dies nun der Fall ist JSONoder ARRAYnicht). Diese Ansätze verzichten auf die reguläre Behandlung relationaler Schemata, indem das Schema aus den Metadaten in die Daten selbst verschoben wird.

Hierfür gibt es ein portables Entwurfsmuster, das sehr gut zu relationalen Datenbanken passt. Es ist EAV (Entity-Attribut-Wert). Ich bin sicher, Sie haben an vielen, vielen Orten gelesen, dass "EAV ist böse" (und es ist). Es gibt jedoch eine bestimmte Anwendung, bei der die Probleme mit EAV nicht wichtig sind, nämlich Produktattributkataloge.

Alle üblichen Argumente gegen EAV gelten nicht für einen Produktmerkmalskatalog, da Produktmerkmalswerte in der Regel nur in eine Liste oder im schlimmsten Fall in eine Vergleichstabelle übernommen werden.

Durch die Verwendung eines JSONSpaltentyps können Sie alle Dateneinschränkungen aus der Datenbank erzwingen und in Ihre Anwendungslogik übernehmen. Die Verwendung einer Attributtabelle für jede Marke hat außerdem die folgenden Nachteile:

  • Es ist nicht gut skalierbar, wenn Sie irgendwann Hunderte von Marken (oder mehr) haben.
  • Wenn Sie die zulässigen Attribute für eine Marke ändern, müssen Sie eine Tabellendefinition ändern, anstatt nur Zeilen in einer Markenfeld-Steuertabelle hinzuzufügen oder zu entfernen.
  • Möglicherweise haben Sie immer noch dünn besetzte Tabellen, wenn die Marke viele potenzielle Merkmale aufweist, von denen nur eine kleine Teilmenge bekannt ist.

Es ist nicht besonders schwierig, Daten zu einem Produkt mit markenspezifischen Merkmalen abzurufen. Es ist wohl einfacher, ein dynamisches SQL mithilfe des EAV-Modells zu erstellen, als mithilfe des Table-per-Category-Modells. In Tabelle pro Kategorie benötigen Sie Reflektion (oder Ihre JSON), um herauszufinden, wie die Namen der Feature-Spalten lauten. Anschließend können Sie eine Liste von Elementen für eine where-Klausel erstellen. Im EAV-Modell wird das WHERE X AND Y AND Zzu INNER JOIN X INNER JOIN Y INNER JOIN Z, daher ist die Abfrage etwas komplizierter, aber die Logik zum Erstellen der Abfrage ist immer noch vollständig tabellenbasiert und mehr als skalierbar, wenn Sie die richtigen Indizes erstellen.

Es gibt viele Gründe, EAV nicht als allgemeinen Ansatz zu verwenden. Diese Gründe gelten nicht für einen Produktfeaturekatalog, sodass in dieser speziellen Anwendung nichts an EAV falsch ist.

Dies ist sicherlich eine kurze Antwort auf ein komplexes und kontroverses Thema. Ich habe ähnliche Fragen schon einmal beantwortet und auf die allgemeine Abneigung gegen EAV eingegangen. Beispielsweise:

Ich würde sagen, EAV wird in letzter Zeit aus meist guten Gründen seltener verwendet als früher. Ich denke jedoch, dass es auch nicht gut verstanden wird.


3

Hier ist mein Problem: Unterschiedliche Bekleidungsmarken erfordern unterschiedliche Informationen. Was ist die beste Vorgehensweise, um mit einem solchen Problem umzugehen?

Verwendung von JSON und PostgreSQL

Ich denke du machst es schwieriger als es sein muss und du wirst später damit gebissen. Sie benötigen kein Entity-Attribut-Wert-Modell, es sei denn, Sie benötigen tatsächlich 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
);

An diesem Schema ist absolut nichts auszusetzen.

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

Jetzt können Sie es mit einem einfachen Join abfragen

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

Und jeder der JSON-Operatoren arbeitet in einer where-Klausel.

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

Fügen Sie die URLs nicht in die Datenbank ein. Sie ändern sich im Laufe der Zeit. Erstellen Sie einfach eine Funktion, die sie übernimmt.

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

oder Wasauchimmer. Wenn Sie PostgreSQL verwenden, können Sie sogar Hash-IDs verwenden .

Ebenfalls von besonderer Bedeutung, jsonbwird als Binärdatei gespeichert (daher das -'b ') und ist auch indexierbar, oder SARGable oder was auch immer die coolen Kids heutzutage nennen:CREATE INDEX ON brands USING gin ( attributes );

Der Unterschied liegt hier in der Einfachheit der Abfrage.

Gib mir alle Klamotten von Brand2

SELECT * FROM clothes WHERE brand_id = 2;

Gib mir alle Klamotten mit dem Attribut: Größe

SELECT * FROM clothes WHERE attributes ? 'size';

Wie wäre es mit einem anderen ..

Gib mir alle Kleider und Attribute für alle Kleider, die in großen Mengen erhältlich sind.

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

Wenn ich das richtig verstehe, ist das Wesentliche, was Sie gesagt haben, wenn es eine Beziehung zwischen Marken und Attributen gibt (dh ob es gültig ist oder nicht), die Lösung von McNets wäre vorzuziehen (aber die Abfragen wären teurer / langsamer). Auf der anderen Seite, wenn diese Beziehung nicht wichtig ist / mehr "ad-hoc", dann könnte man Ihre Lösung bevorzugen. Können Sie etwas genauer erklären, was Sie damit gemeint haben, als Sie sagten "Ich würde es aber niemals mit PostgreSQL verwenden"? Zu diesem Kommentar schien es keine Erklärung zu geben. Entschuldigung für alle Fragen !! Ich
freue

1
Es gibt eindeutig eine Beziehung, die einzige Frage ist, wie viel Sie benötigen, um damit umzugehen. Wenn ich einen vagen Begriff wie Eigenschaften , Attribute oder ähnliches verwende, dann meine ich normalerweise, dass er ziemlich spontan oder sehr unstrukturiert ist. Dafür ist JSONB einfach besser, weil es einfacher ist. Sie finden diesen Beitrag informativ coussej.github.io/2016/01/14/…
Evan Carroll

-1

Eine einfache Lösung besteht darin, alle möglichen Attribute als Spalten in die Hauptkleidungstabelle aufzunehmen und alle markenspezifischen Spalten auf null zu setzen. Diese Lösung unterbricht die Datenbanknormalisierung, ist jedoch sehr einfach zu implementieren.


Ich denke, ich habe eine Idee von dem, was Sie sagen, aber es kann hilfreich sein, mehr Details und vielleicht auch ein Beispiel hinzuzufügen.
Youngrrrr
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.