Najlepszy projekt do odniesienia do wielu tabel z pojedynczej kolumny?

18

Proponowany schemat

Przede wszystkim oto przykład mojego proponowanego schematu, do którego można odwoływać się w całym poście:

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

Opis problemu

Mam stolik z ubraniami, który ma kolumny takie jak nazwa, kolor, cena, brandid i tak dalej, aby opisać atrybuty konkretnego elementu odzieży.

Oto mój problem: różne marki odzieży wymagają różnych informacji. Jaka jest najlepsza praktyka radzenia sobie z takim problemem?

Pamiętaj, że dla moich celów konieczne jest znalezienie informacji specyficznych dla marki, poczynając od pozycji ubrania . Wynika to z tego, że najpierw wyświetlam użytkownikowi informacje z pozycji dotyczącej ubrań , a następnie muszę użyć informacji specyficznych dla marki, aby kupić produkt. Podsumowując, musi istnieć kierunkowa zależność między ubraniami (od) a tabelami brand_x .

Proponowane / aktualne rozwiązanie

Aby sobie z tym poradzić, pomyślałem o następującym schemacie projektu:

Ubrania tabela mają zupełnie kolumnę, która może mieć wartości identyfikacyjnych od 1 do X, gdzie poszczególne odpowiada identyfikacyjnych tabeli danej marki. Na przykład wartość identyfikatora 1 będzie odpowiadać tabeli brand_1 (która może mieć kolumnę URL ), identyfikator 2 będzie odpowiadać brand_2 (która może mieć kolumnę dostawcy ) itp.

Tak więc, aby powiązać konkretny wpis dotyczący ubrania z informacjami dotyczącymi marki, wyobrażam sobie, że logika na poziomie aplikacji będzie wyglądać mniej więcej tak:

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.

Inne komentarze i przemyślenia

Próbuję znormalizować całą bazę danych w BCNF i chociaż to właśnie wymyśliłem, wynikowy kod aplikacji sprawia, że ​​czuję się bardzo niespokojny. Nie ma sposobu na wymuszenie relacji poza poziomem aplikacji, a zatem projekt jest bardzo zepsuty i, jak sądzę, bardzo podatny na błędy.

Badania

Przed napisaniem posta sprawdziłem poprzednie wpisy. Oto post z prawie identycznym problemem, który udało mi się znaleźć. I tak napisałem ten post, ponieważ wydaje się, że jedyna podana odpowiedź nie ma rozwiązania SQL ani rozwiązania projektowego (tj. Wspomina OOP, dziedziczenie i interfejsy).

Jestem także nowicjuszem, jeśli chodzi o projektowanie baz danych, dlatego doceniłbym wszelkie spostrzeżenia.


Wygląda na to, że na przepełnieniu stosu znajdują się bardziej pomocne odpowiedzi:

Odniosłem się do tamtejszych rozwiązań i sugeruję, aby inni również znaleźli moje pytanie.

Pomimo podanych powyżej linków wciąż szukam odpowiedzi tutaj i doceniłbym wszelkie dostarczone rozwiązania!

Używam PostgreSQL.

youngrrrr
źródło

Odpowiedzi:

7

Osobiście nie lubię używać do tego celu schematu wielostołowego.

  • Trudno jest zapewnić integralność.
  • Trudno to utrzymać.
  • Filtrowanie wyników jest trudne.

Ustawiłem próbkę dbfiddle .

Mój proponowany schemat tabeli:

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

Pozwól mi wstawić trochę danych:

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

Jeśli chcesz pobrać typowe atrybuty:

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

Lub możesz łatwo uzyskać ubrania według marki:

Daj mi wszystkie ubrania 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

Ale dla mnie jednym z najlepszych tego schematu jest to, że możesz filtrować według Attibutes:

Daj mi wszystkie ubrania, które mają atrybut: Rozmiar

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

Używanie schematu wielostołowego niezależnie od poprzednich zapytań wymaga obsługi nieograniczonej liczby tabel lub pól XML lub JSON.

Inną opcją dla tego schematu jest to, że możesz zdefiniować szablony, na przykład możesz dodać nową tabelę BrandAttrTemplates. Za każdym razem, gdy dodajesz nowy rekord, możesz użyć wyzwalacza lub SP, aby wygenerować zestaw predefiniowanych atrybutów dla tego Oddziału.

Przepraszam, chciałbym rozszerzyć moje wyjaśnienia o, myślę, że są one bardziej zrozumiałe niż mój angielski.

Aktualizacja

Moja obecna odpowiedź powinna działać bez względu na to, który RDBMS. Zgodnie z Twoimi komentarzami, jeśli chcesz filtrować wartości atrybutów, sugeruję niewielkie zmiany.

O ile MS-Sql nie pozwala na tablice, stworzyłem nową próbkę zachowującą ten sam schemat tabeli, ale zmieniając AttrValue na typ pola ARRAY.

W rzeczywistości, używając POSTGRES, możesz skorzystać z tej tablicy za pomocą indeksu GIN.

(Powiem, że @EvanCarrol ma dobrą wiedzę o Postgresie, na pewno lepszą ode mnie. Ale pozwólcie, że dodam trochę.)

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

Teraz możesz dodatkowo wykonać zapytanie za pomocą wartości poszczególnych atrybutów, takich jak:

Daj mi listę wszystkich spodni Rozmiar: 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;

Oto wynik:

clothes name | brand name | attribute | values 
------------- ------------ ----------  ---------------- 
Pants          Brand1       Size        32, 33, 34
McNets
źródło
Naprawdę podoba mi się to wyjaśnienie, ale wygląda na to, że po prostu handlujemy schematem wielostołowym, aby umieścić te wiele plików CSV w jednej kolumnie - jeśli ma to sens. Z drugiej strony wydaje mi się, że bardziej podoba mi się to podejście, ponieważ nie wymaga żadnych zmian w schemacie, ale znowu wydaje się, że popychamy problem gdzie indziej (mianowicie poprzez kolumny o zmiennej długości). To może być problem; co jeśli chciałbym zapytać spodnie o rozmiarze 3 w DB? Może nie ma dobrego, czystego rozwiązania tego rodzaju problemu. Czy istnieje nazwa tego pomysłu, abym mógł bardziej się nim zająć?
youngrrrr
Właściwie ... aby odpowiedzieć na postawiony przeze mnie problem, być może odpowiedź można wypożyczyć z rozwiązania @ EvanCarroll: mianowicie, używając typów jsonb zamiast po prostu TEKSTU / ŁAŃCUCHÓW w formacie CSV. Ale znowu - jeśli istnieje nazwa tej koncepcji, daj mi znać!
youngrrrr
1
Jest to rozwiązanie typu Entity Attribute Value. To niezły kompromis między wydajnością a dobrym designem. Jest to jednak kompromis. Wymieniasz trochę wydajności na czystsze wzornictwo, nie zaśmiecone niekończącymi się tabelami „Brand_X”. Kara za wydajność, idąc od podanego najczęściej kierunku powinna być minimalna. Idąc w drugą stronę będzie bardziej bolesne, ale taki jest kompromis. en.wikipedia.org/wiki/…
Jonathan Fite
4

Opisujesz, przynajmniej częściowo, katalog produktów. Masz kilka atrybutów, które są wspólne dla wszystkich produktów. Należą one do dobrze znormalizowanej tabeli.

Poza tym masz szereg atrybutów, które są specyficzne dla marki (i spodziewam się, że mogą być specyficzne dla produktu). Co twój system musi zrobić z tymi konkretnymi atrybutami? Czy masz logikę biznesową, która zależy od schematu tych atrybutów, czy po prostu umieszczasz je w szeregu par „etykieta”: „wartość”?

Inne odpowiedzi sugerują użycie tak naprawdę podejścia CSV (czy to jest, JSONczy ARRAYnie) - Podejścia te rezygnują z regularnej obsługi schematu relacyjnego poprzez przeniesienie schematu z metadanych do samych danych.

Istnieje przenośny wzorzec projektowy, który bardzo dobrze pasuje do relacyjnych baz danych. Jest to EAV (encja-atrybut-wartość). Jestem pewien, że czytałeś w wielu, wielu miejscach, że „EAV is Evil” (i tak jest). Istnieje jednak jedna konkretna aplikacja, w której problemy z EAV nie są ważne, a mianowicie katalogi atrybutów produktu.

Wszystkie zwykłe argumenty przeciwko EAV nie dotyczą katalogu funkcji produktu, ponieważ wartości funkcji produktu są zazwyczaj zwracane tylko do listy, a najgorsze do tabeli porównawczej.

Użycie JSONtypu kolumny powoduje wymuszenie wszelkich ograniczeń danych z bazy danych i zmusza je do logiki aplikacji. Ponadto użycie jednej tabeli atrybutów dla każdej marki ma następujące wady:

  • Nie skaluje się dobrze, jeśli ostatecznie masz setki marek (lub więcej).
  • Jeśli zmienisz dopuszczalne atrybuty marki, musisz zmienić definicję tabeli zamiast dodawać lub usuwać wiersze w tabeli kontrolnej pola marki.
  • Nadal możesz skończyć z rzadko zapełnianymi tabelami, jeśli marka ma wiele potencjalnych cech, z których tylko niewielki podzbiór jest znany.

Odzyskiwanie danych o produkcie z funkcjami specyficznymi dla marki nie jest szczególnie trudne. Prawdopodobnie łatwiej jest stworzyć dynamiczny SQL przy użyciu modelu EAV, niż przy użyciu modelu tabeli dla kategorii. W tabeli dla kategorii potrzebujesz refleksji (lub swojej JSON), aby dowiedzieć się, jakie są nazwy kolumn funkcji. Następnie możesz zbudować listę elementów dla klauzuli where. W modelu EAV WHERE X AND Y AND Zstaje się INNER JOIN X INNER JOIN Y INNER JOIN Z, więc zapytanie jest nieco bardziej skomplikowane, ale logika budowania zapytania jest nadal całkowicie zależna od tabeli i będzie bardziej niż wystarczająco skalowalna, jeśli masz zbudowane odpowiednie indeksy.

Istnieje wiele powodów, dla których nie należy stosować EAV jako ogólnego podejścia. Te powody nie dotyczą katalogu funkcji produktu, więc nie ma nic złego w EAV w tej konkretnej aplikacji.

Z pewnością jest to krótka odpowiedź na złożony i kontrowersyjny temat. Odpowiedziałem wcześniej na podobne pytania i podałem więcej szczegółów na temat ogólnej niechęci do EAV. Na przykład:

Powiedziałbym, że EAV jest ostatnio używany rzadziej niż kiedyś, głównie z dobrych powodów. Myślę jednak, że nie jest to również dobrze zrozumiane.

Joel Brown
źródło
3

Oto mój problem: różne marki odzieży wymagają różnych informacji. Jaka jest najlepsza praktyka radzenia sobie z takim problemem?

Korzystanie z JSON i PostgreSQL

Myślę, że czynisz to trudniejszym, niż to musi być, i później zostaniesz ugryziony. Nie potrzebujesz modelu Entity – atrybut – wartość, chyba że faktycznie potrzebujesz 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
);

Nie ma absolutnie nic złego w tym schemacie.

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

Teraz możesz wykonać zapytanie za pomocą prostego łączenia

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

I każdy z operatorów JSON działa w klauzuli where.

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

Na marginesie, nie umieszczaj adresów URL w bazie danych. Zmieniają się z czasem. Po prostu stwórz funkcję, która je przyjmuje.

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

lub cokolwiek. Jeśli korzystasz z PostgreSQL, możesz nawet używać skrótów .

Na szczególną uwagę zasługuje również zapis jsonbbinarny (a więc -'b) i indeksowanie, SARGable lub cokolwiek innego, jak nazywają to teraz fajne dzieci:CREATE INDEX ON brands USING gin ( attributes );

Różnica polega na prostocie zapytania.

Daj mi wszystkie ubrania Brand2

SELECT * FROM clothes WHERE brand_id = 2;

Daj mi wszystkie ubrania, które mają atrybut: Rozmiar

SELECT * FROM clothes WHERE attributes ? 'size';

Co powiesz na inny ...

Daj mi wszystkie ubrania i atrybuty dla wszystkich ubrań dostępnych w dużych rozmiarach.

SELECT * FROM clothes WHERE attributes->>'size' = 'large';
Evan Carroll
źródło
Tak więc, jeśli dobrze rozumiem, istotą tego, co powiedziałeś, jest to, czy istnieje związek między markami i atrybutami (tj. Czy jest poprawny), wtedy rozwiązanie McNets byłoby preferowane (ale zapytania byłyby bardziej kosztowne / wolniejsze). Z drugiej strony, jeśli ta relacja nie jest ważna / bardziej „ad-hoc”, wówczas można preferować twoje rozwiązanie. Czy możesz wyjaśnić nieco więcej, co miałeś na myśli, mówiąc „nigdy nie użyłbym tego z PostgreSQL?” Wydawało się, że nie ma wyjaśnienia dla tego komentarza. Przepraszam za wszystkie pytania !! Naprawdę doceniam twoje odpowiedzi do tej pory :)
youngrrrr
1
Istnieje wyraźny związek, jedynym pytaniem jest, ile trzeba nim zarządzać. Jeśli używam niejasnych terminów, takich jak właściwości , atrybuty lub tym podobne, zwykle chcę powiedzieć, że jest to dość ad-hoc lub wysoce nieuporządkowane. Do tego JSONB jest po prostu lepszy, ponieważ jest prostszy. ten post może znaleźć informacyjny coussej.github.io/2016/01/14/…
Evan Carroll
-1

Jednym łatwym rozwiązaniem jest uwzględnienie wszystkich możliwych atrybutów jako kolumn na głównym stole z ubraniami i nadanie wszystkim kolumnom specyficznym dla marki wartości dopuszczalnej. To rozwiązanie łamie normalizację bazy danych, ale jest bardzo łatwe do wdrożenia.

Matthew Sontum
źródło
Myślę, że ... Mam pojęcie o tym, co mówisz, ale może być pomocne podanie bardziej szczegółowych informacji i być może również przykładu.
youngrrrr