Лучший дизайн для ссылки на несколько таблиц из одного столбца?

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

Постановка задачи

У меня есть таблица одежды, в которой есть столбцы, такие как имя, цвет, цена, бренд и т. Д., Чтобы описать атрибуты для конкретного предмета одежды.

Вот моя проблема: одежда разных марок требует разной информации. Как лучше всего справляться с такой проблемой?

Обратите внимание, что для моих целей необходимо найти информацию о бренде, начиная с записи одежды . Это потому, что я сначала отображаю информацию из записи одежды для пользователя, после чего я должен использовать информацию, относящуюся к бренду, для покупки предмета. Таким образом, между одеждой (от) и брендом должна быть направленная связь таблицами .

Предлагаемое / текущее решение

Чтобы справиться с этим, я подумал о следующей схеме проектирования:

Таблица одежды будет иметь столбец бренда, который может иметь значения идентификатора в диапазоне от 1 до x, где конкретный идентификатор соответствует таблице для конкретного бренда. Например, значение id 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-решения или решения, основанного на дизайне (то есть он упоминает ООП, наследование и интерфейсы).

Я также новичок, когда дело доходит до проектирования баз данных, и поэтому я буду признателен за любые идеи.


Похоже, есть более полезные ответы на переполнение стека:

Я сослался на решения там и предлагаю другим, которые найдут мой вопрос, сделать то же самое.

Несмотря на приведенные выше ссылки, я все еще ищу ответы здесь и буду признателен за любые предоставленные решения!

Я использую PostgreSQL.

youngrrrr
источник

Ответы:

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, чтобы сгенерировать набор предопределенных атрибутов для этой ветви.

Извините, я хотел бы расширить мои объяснения, я думаю, что это более понятно, чем мой английский.

Обновить

Мой текущий ответ должен работать независимо от того, какая СУБД. Согласно вашим комментариям, если вам нужно отфильтровать значения атрибутов, я бы предложил небольшие изменения.

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

Фактически, используя POSTGRES, вы можете использовать Advantatge для этого массива, используя индекс 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
McNets
источник
Мне действительно нравится это объяснение, но кажется, что мы просто торгуем по схеме из нескольких таблиц, чтобы эти несколько CSV были в одном столбце - если это имеет смысл. С другой стороны, я чувствую, что мне нравится этот подход лучше, потому что он не требует никаких изменений в схеме, но опять-таки кажется, что мы выдвигаем проблему в другом месте (а именно, имея столбцы переменной длины). Это может быть проблемой; Что делать, если я хочу запросить штаны размера 3 в БД? Может быть, нет хорошего, чистого решения этой проблемы. Есть ли название для этой концепции, чтобы я мог рассмотреть ее подробнее?
youngrrrr
На самом деле ... чтобы ответить на поставленную мной проблему, возможно, ответ можно позаимствовать из решения @ EvanCarroll, а именно: использовать типы jsonb вместо просто TEXT / STRINGS в формате CSV. Но опять же - если есть название для этой концепции, пожалуйста, дайте мне знать!
youngrrrr
1
Это решение типа Entity Attribute Value. Это не плохой компромисс между производительностью и хорошим дизайном. Это компромисс, хотя. Вы торгуете некоторой производительностью для более чистого дизайна, не заваленного бесконечными таблицами «Brand_X». Нарушение производительности, исходя из заявленного вами наиболее распространенного направления, должно быть минимальным. Идти другим путем будет более болезненным, но это компромисс. en.wikipedia.org/wiki/…
Джонатан
4

То, что вы описываете, является, по крайней мере частично, каталогом продукции. У вас есть несколько атрибутов, которые являются общими для всех продуктов. Они относятся к хорошо нормализованной таблице.

Кроме того, у вас есть ряд атрибутов, которые являются специфическими для бренда (и я ожидаю, что они могут быть специфическими для продукта). Что ваша система должна делать с этими конкретными атрибутами? У вас есть бизнес-логика, которая зависит от схемы этих атрибутов, или вы просто перечисляете их в серии пар "метка": "значение"?

Другие ответы предлагают использовать то, что по сути является CSV-подходом (будь JSONто ARRAYили нет). Эти подходы требуют регулярной обработки реляционной схемы, перемещая схему из метаданных в сами данные.

Для этого существует портативный шаблон проектирования, который очень хорошо подходит для реляционных баз данных. Это EAV (сущность-атрибут-значение). Я уверен, что вы читали во многих, многих местах, что «EAV is Evil» (и это так). Однако есть одно конкретное приложение, в котором проблемы с EAV не важны, это каталоги атрибутов продукта.

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

Использование JSONтипа столбца лишает вас возможности навязывать любые ограничения данных из базы данных и вводить их в логику вашего приложения. Кроме того, использование одной таблицы атрибутов для каждого бренда имеет следующие недостатки:

  • Это не очень хорошо масштабируется, если у вас есть сотни брендов (или больше).
  • Если вы изменяете допустимые атрибуты бренда, вам нужно изменить определение таблицы, а не просто добавлять или удалять строки в контрольной таблице поля бренда.
  • Вы все равно можете получить малонаселенные таблицы, если у бренда много потенциальных возможностей, из которых известно лишь небольшое подмножество.

Не особенно сложно получить данные о продукте с особенностями бренда. Возможно, проще создать динамический SQL с использованием модели EAV, чем с использованием модели таблицы на категорию. В таблице на категорию вам нужно размышление (или ваше JSON), чтобы узнать, как называются имена столбцов объектов. Затем вы можете создать список элементов для предложения where. В модели 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 работает в предложении where.

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';
Эван Кэрролл
источник
Итак, если я правильно понимаю, суть того, что вы сказали, заключается в том, что если есть связь между брендами и атрибутами (то есть, является ли она действительной), то решение McNets будет предпочтительным (но запросы будут более дорогостоящими / медленными). С другой стороны, если эти отношения не важны / более «случайны», то можно предпочесть ваше решение. Можете ли вы объяснить немного больше, что вы имели в виду, когда говорили «я бы никогда не использовал это с PostgreSQL?» Похоже, этому комментарию нет объяснения. Извините за все вопросы!! Я действительно ценю ваши ответы до сих пор :)
youngrrrr
1
Там явно есть отношения, единственный вопрос в том, сколько вам нужно, чтобы справиться с этим. Если я использую расплывчатый термин, такой как свойства , атрибуты или тому подобное, я обычно хочу сказать, что это в значительной степени случайный или крайне неструктурированный. Для этого JSONB просто лучше, потому что он проще. Вы можете найти этот пост информативным coussej.github.io/2016/01/14/…
Эван Кэрролл
-1

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

Мэтью Сонтум
источник
Я думаю ... У меня есть представление о том, что вы говорите, но может быть полезно включить больше деталей и, возможно, пример.
youngrrrr