Кращий дизайн для посилання на кілька таблиць з одного стовпця?


18

Запропонована схема

Перш за все, ось приклад моєї запропонованої схеми для посилання на весь мій пост:

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

Постановка проблеми

У мене є таблиця одягу, яка містить стовпчики, такі як найменування, колір, ціна, бренд тощо, щоб описати атрибути певного предмета одягу.

Ось моя проблема: одяг для різних марок вимагає різної інформації. Яка найкраща практика для вирішення подібної проблеми?

Зауважте, що для моїх цілей необхідно знайти інформацію, що стосується бренду, починаючи з входу в одяг . Це тому, що я спочатку показую користувачеві інформацію про одяг , після чого я мушу використовувати її інформацію, що стосується бренду, для придбання товару. Підсумовуючи це, має бути прямий взаємозв'язок між одягом (від) та таблицями brand_x .

Пропоноване / поточне рішення

Щоб впоратися з цим, я продумав таку схему дизайну:

Таблиця одягу матиме стовпчик марки, який може мати значення id від 1 до x, де конкретний ідентифікатор відповідає таблиці, що відповідає марці. Наприклад, значення 1 ідентифікатора буде відповідати таблиці brand_1 (у якій може бути стовпець URL ), id 2 відповідатиме brand_2 (який може мати постачальника стовпець ) тощо.

Таким чином, щоб пов'язувати певний товар із одягом з його специфічною для бренду інформацією, я думаю, що логіка на рівні програми буде виглядати приблизно так:

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.

Інші коментарі та думки

Я намагаюся нормалізувати всю свою базу даних в BCNF, і хоча це саме те, що я придумав, отриманий код програми викликає відчуття тривоги. Немає способу налагодити відносини, окрім як на прикладному рівні, і, таким чином, дизайн виглядає дуже хитким і, я гадаю, дуже схильним до помилок.

Дослідження

Я обов'язково переглянув попередні записи, перш ніж робити публікацію. Ось публікація з майже ідентичною проблемою, яку мені вдалося знайти. Я все-таки зробив цю публікацію, тому що, здається, єдина відповідь, що надається, не має рішення на основі SQL або дизайну (тобто в ній згадується OOP, успадкування та інтерфейси).

Я також є новачком, коли справа стосується дизайну баз даних, і тому я буду вдячний за будь-яку інформацію.


Здається, є більше корисних відповідей на переповнення стека:

Я посилався на рішення, які пропонуються там, і пропоную іншим, хто вважає моє питання, так само.

Незважаючи на наведені вище посилання, я все ще шукаю відповідей тут і буду вдячний за будь-які надані рішення!

Я використовую PostgreSQL.

Відповіді:


7

Я особисто не люблю використовувати для цього схему з декількома таблицями.

  • Важко забезпечити цілісність.
  • Це важко підтримувати.
  • Фільтрувати результати важко.

Я встановив зразок dbfiddle .

Моя запропонована схема таблиці:

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

Дозвольте вставити деякі дані:

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

Якщо вам потрібно отримати спільні атрибути:

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

Або ви можете легко отримати одяг за маркою:

Дайте мені весь одяг 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

Але для мене однією з найкращих цієї схеми є те, що ви можете фільтрувати за Attibutes:

Дайте мені всю Одяг, що має атрибут: Розмір

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

Використовуючи схему з декількома таблицями, будь-який з попередніх запитів потребує обробки необмеженої кількості таблиць або полів XML або JSON.

Інший варіант цієї схеми полягає в тому, що ви можете визначати шаблони, наприклад, ви можете додати нову таблицю BrandAttrTemplates. Кожен раз, коли ви додаєте нову запис, ви можете використовувати тригер або SP для створення набору заздалегідь визначених атрибутів для цієї гілки.

Вибачте, я хотів би розширити свої пояснення, вважаючи, що це зрозуміліше, ніж моя англійська.

Оновлення

Моя нинішня відповідь повинна працювати незалежно від того, які RDBMS. Відповідно до ваших коментарів, якщо вам потрібно відфільтрувати значення атрибутів, я запропонував би невеликі зміни.

Що стосується того, що MS-Sql не дозволяє мати масиви, я створив нову вибірку з такою ж схемою таблиці, але змінив AttrValue на тип поля ARRAY.

Фактично, використовуючи POSTGRES, ви можете скористатися перевагами цього масиву, використовуючи індекс GIN.

(Дозвольте сказати, що @EvanCarrol має хороші знання про Postgres, безумовно, краще за мене. Але дозвольте додати свій шматочок.)

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

Тепер ви можете додатково запитувати, використовуючи окремі значення атрибутів, як-от:

Надайте мені список усіх штанів Розмір: 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;

Це результат:

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

Мені дуже подобається це пояснення, але, схоже, ми просто торгуємо схемою з декількома таблицями за те, що ці кілька CSV-файлів є в одному стовпчику - якщо це має сенс. З іншого боку, я відчуваю, що мені подобається цей підхід краще, тому що він не потребує змін у схемі, але знову ж таки відчуває, що ми підштовхуємо проблему в іншому місці (а саме, маючи стовпчики змінної довжини). Це може бути проблемою; що робити, якщо я хочу запитати штани розміром 3 у БД? Можливо, немає такого приємного, чистого вирішення подібної проблеми. Чи є назва цього поняття, щоб я міг детальніше розглянути його?
youngrrrr

Насправді ... щоб відповісти на поставлену мною проблему, можливо, відповідь можна запозичити з рішення @ EvanCarroll: а саме, використовуючи типи jsonb, а не просто TEXT / STRINGS у форматі CSV. Але знову ж таки - якщо для цієї концепції є назва, будь ласка, дайте мені знати!
youngrrrr

1
Це рішення значення значення атрибутів суб'єкта господарювання. Це не поганий компроміс між продуктивністю та хорошим дизайном. Це, проте, компроміс. Ви торгуєте деякими показниками для більш чіткого дизайну, не заваленого нескінченними таблицями "Brand_X". Штраф за ефективність, що йде від вашого найпоширенішого напряму, повинен бути мінімальним. Йти іншим шляхом буде більш болісно, ​​але це компроміс. en.wikipedia.org/wiki/…
Джонатан Фейт

4

Що ви описуєте - це, принаймні частково, каталог товарів. У вас є кілька атрибутів, спільних для всіх продуктів. Вони належать до добре нормованої таблиці.

Крім того, у вас є низка атрибутів, які є специфічними для бренду (і, я думаю, може бути специфічним для продукту). Що вашій системі потрібно робити з цими конкретними атрибутами? Чи є у вас бізнес-логіка, яка залежить від схеми цих атрибутів, або ви просто перераховуєте їх у серії пар "label": "value"?

Інші відповіді пропонують використовувати те, що є по суті CSV-підходом (чи це, JSONчи ARRAYіншим чином). Ці підходи відмовляються від регулярної обробки реляційних схем шляхом переміщення схеми з метаданих та в самі дані.

Для цього існує модель портативного дизайну, яка дуже добре вписується в реляційні бази даних. Це EAV (сутність-атрибут-значення). Я впевнений, що ви читали в багатьох, багатьох місцях, що "EAV - це зло" (і це). Однак є одна особлива програма, де проблеми з EAV не важливі, і це каталоги атрибутів продуктів.

Усі звичні аргументи проти EAV не застосовуються до каталогу особливостей продукту, оскільки значення особливостей продукту, як правило, лише повторюються у список або в гіршому випадку - у таблицю порівняння.

Використовуючи JSONтип стовпця, ви зможете застосувати будь-які обмеження даних із бази даних і змусити їх у логіці програми. Також використання однієї таблиці атрибутів для кожної марки має такі недоліки:

  • Це не добре, якщо у вас є сотні марок (або більше).
  • Якщо ви змінюєте допустимі атрибути на бренд, вам слід змінити визначення таблиці, а не просто додавати або видаляти рядки в таблиці керування полем марки.
  • Ви все ще можете розібратися з малонаселеними таблицями, якщо бренд має багато потенційних особливостей, відомі лише невеликі підмножини.

Отримати дані про товар із особливостями бренда не особливо складно. Можливо, простіше створити динамічний SQL за допомогою моделі EAV, ніж це було б за допомогою моделі таблиці за категорією. У таблиці за категорією вам потрібне відображення (або ваше JSON), щоб дізнатися, що таке назви стовпців функції. Тоді ви можете скласти список елементів для пункту де. У моделі EAV це WHERE X AND Y AND Zстає INNER JOIN X INNER JOIN Y INNER JOIN Zтак, що запит трохи складніший, але логіка побудови запиту все ще повністю керована таблицею, і вона буде більш ніж масштабованою, якщо у вас створені відповідні індекси.

Є багато причин не використовувати EAV як загальний підхід. Ці причини не поширюються на каталог особливостей продукту, тому в цьому конкретному додатку немає нічого поганого з EAV.

Безумовно, це коротка відповідь на складну і суперечливу тему. Я раніше відповідав на подібні запитання і детальніше розповів про загальну неприязнь до EAV. Наприклад:

Я б сказав, що EAV використовується останнім часом рідше, ніж раніше, з переважно поважних причин. Однак, я думаю, це також недостатньо зрозуміло.


3

Ось моя проблема: різні марки одягу вимагають різної інформації. Яка найкраща практика для вирішення подібної проблеми?

Використання JSON та PostgreSQL

Я думаю, що ти робиш це складніше, ніж це потрібно, і згодом тебе покусають. Вам не потрібна модель Entity-attribute-value, якщо вам фактично не потрібна 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
);

У цій схемі немає абсолютно нічого поганого.

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

Тепер ви можете запросити його за допомогою простого з'єднання

SELECT *
FROM brands
JOIN clothes
  USING (brand_id);

І будь-який з операторів JSON працює в пункті де.

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

В якості бічної примітки, не ставте URL-адреси в базу даних. Вони змінюються з часом. Просто створіть функцію, яка їх бере.

generate_url_brand( brand_id );
generate_url_clothes( clothes_id );

чи що завгодно. Якщо ви використовуєте PostgreSQL, ви навіть можете використовувати гашиди .

Крім того, особлива примітка, jsonbзберігається як двійковий (таким чином, -'b '), і він також може індексувати, або SARGable або що-небудь ще круті діти називають це сьогодні:CREATE INDEX ON brands USING gin ( attributes );

Різниця тут полягає в простоті запиту ..

Дайте мені весь одяг Brand2

SELECT * FROM clothes WHERE brand_id = 2;

Дайте мені всю Одяг, що має атрибут: Розмір

SELECT * FROM clothes WHERE attributes ? 'size';

Як щодо іншого ..

Дайте мені весь одяг та атрибути для будь-якого одягу, доступного великим розміром.

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

Отже, якщо я правильно розумію, суть того, що ви сказали, полягає у наявності взаємозв'язків між брендами та атрибутами (тобто чи дійсні вони чи ні), тоді рішення Макнетса було б кращим (але запити були б дорожчими / повільнішими). З іншого боку, якщо ці відносини не є важливими / більш "тимчасовими", то ви можете віддати перевагу вашому рішенню. Чи можете ви пояснити трохи більше, що ви мали на увазі, коли ви сказали: "Я ніколи не використовував би це з PostgreSQL?" Здавалося, що цього коментаря не було. Вибачте за всі питання !! Я дуже ціную ваші відповіді до цих пір :)
youngrrrr

1
Очевидно, що стосунки існують, питання лише в тому, скільки потрібно, щоб ним керувати. Якщо я використовую розпливчастий термін, як властивості , атрибути чи подібне, я, як правило, маю на увазі сказати, що це майже ad hoc або дуже неструктуровано. Для цього JSONB просто кращий, тому що він простіший. Ви можете знайти цю публікацію інформативно coussej.github.io/2016/01/14/…
Еван Керролл

-1

Одне просте рішення - включити всі можливі атрибути як стовпчики на основний стіл одягу та зробити всі стовпці конкретних брендів нульовими. Це рішення порушує нормалізацію бази даних, але дуже легко здійснити.


Я думаю .. Я маю уявлення про те, що ви говорите, але може бути корисним включити більше деталей і, можливо, приклад.
youngrrrr
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.