Проектиране на SQL схема за комбинация от връзка много към много (вариации на продукти)

Надявам се, че заглавието е донякъде полезно. Използвам MySQL като моя база данни

Създавам база данни с продукти и не съм сигурен как да се справя със съхраняването на цени/SKU на варианти на продукт. Един продукт може да има неограничени вариации и всяка комбинация от вариации има своя собствена цена/SKU/и т.н.

Ето как съм настроил таблицата с продукти/вариации в момента:

PRODUCTS
+--------------------------+
| id | name | description  |
+----+------+--------------+
| 1  | rug  | a cool rug   |
| 2  | cup  | a coffee cup |
+----+------+--------------+

PRODUCT_VARIANTS
+----+------------+----------+-----------+
| id | product_id | variant  | value     |
+----+------------+----------+-----------+
| 1  | 1          | color    | red       |
| 2  | 1          | color    | blue      |
| 3  | 1          | color    | green     |
| 4  | 1          | material | wool      |
| 5  | 1          | material | polyester |
| 6  | 2          | size     | small     |
| 7  | 2          | size     | medium    |
| 8  | 2          | size     | large     |
+----+------------+----------+-----------+

(`products.id` is a foreign key of `product_variants.product_id`)

Създадох SQLFiddle с тези примерни данни: http://sqlfiddle.com/#!2/2264d/1

Потребителят има право да въведе произволно име на вариант (product_variants.variant) и може да му присвои произволна стойност (product_variants.value). Не трябва да има ограничение за количеството варианти/стойности, които потребителят може да въведе.

Тук възниква проблемът ми: съхранявам цени/SKU за всеки вариант, без да добавям нова таблица/колона всеки път, когато някой добави продукт с вариант, който не е съществувал преди.

Всеки вариант може да има една и съща цена, но SKU е уникален за всеки продукт. Например продукт 1 има 6 различни комбинации (3 цвята * 2 материала), а продукт 2 има само 3 различни комбинации (3 размера * 1).

Мислех да съхраня комбинациите като текст, т.е.

+------------+-----------------+-------+------+
| product_id | combination     | price | SKU  |
+------------+-----------------+-------+------+
| 1          | red-wool        | 50.00 | A121 |
| 1          | red-polyester   | 50.00 | A122 |
| 1          | blue-wool       | 50.00 | A123 |
| 1          | blue-polyester  | 50.00 | A124 |
| 1          | green-wool      | 50.00 | A125 |
| 1          | green-polyester | 50.00 | A125 |
| 2          | small           | 4.00  | CD12 |
| 2          | medium          | 4.00  | CD13 |
| 2          | large           | 3.50  | CD14 |
+------------+-----------------+-------+------+

Но трябва да има по-добър, нормализиран начин за представяне на тези данни. Хипотетична ситуация: Искам да мога да търся син продукт, който е по-малко от $10. С горната структура на базата данни не е възможно да се направи без анализиране на текста и това е нещо, което искам да избегна.

Всяка помощ/предложения се оценяват =)


person Zaki Aziz    schedule 02.10.2013    source източник


Отговори (6)


Прилагайки нормализация към вашия проблем, решението е както е дадено. Стартирайте и го вижте на SQL Fiddle.

CREATE TABLE products (
    product_id  int AUTO_INCREMENT PRIMARY KEY,
    name        varchar(20),
    description varchar(30)
);

INSERT INTO products
    (name, description)
VALUES
    ('Rug', 'A cool rug' ),
    ('Cup', 'A coffee cup');

-- ========================================

CREATE TABLE variants (
    variant_id int AUTO_INCREMENT PRIMARY KEY,
    variant    varchar(50)
);

INSERT INTO variants
    (variant)
VALUES
    ('color'),
    ('material'),
    ('size');

-- ========================================

CREATE TABLE variant_value (
    value_id   int AUTO_INCREMENT PRIMARY KEY,
    variant_id int,
    value      varchar(50)
);

INSERT INTO variant_value
    (variant_id, value)
VALUES
    (1, 'red'),
    (1, 'blue'),
    (1, 'green'),
    (2, 'wool'),
    (2, 'polyester'),
    (3, 'small'),
    (3, 'medium'),
    (3, 'large');

-- ========================================

CREATE TABLE product_variants (
    product_variants_id int AUTO_INCREMENT PRIMARY KEY,
    product_id          int,
    productvariantname  varchar(50),
    sku                 varchar(50),
    price               float
);

INSERT INTO product_variants
    (product_id, productvariantname, sku, price)
VALUES
    (1, 'red-wool', 'a121', 50),
    (1, 'red-polyester', 'a122', 50);

-- ========================================

CREATE TABLE product_details (
    product_detail_id   int AUTO_INCREMENT PRIMARY KEY,
    product_variants_id int,
    value_id            int
);

INSERT INTO product_details
    (product_variants_id, value_id)
VALUES
    (1, 1),
    (1, 4),
    (2, 1),
    (2, 5);
person sahalMoidu    schedule 05.10.2013
comment
Освен това можете да използвате nullval (обединяване), за да предоставите цена по подразбиране от основния продукт, когато не е дадена цена на sku вариант. - person ChuckCottrill; 08.10.2013
comment
@ChuckCottrill: Не бих препоръчал това, тъй като бихте смесили вашата бизнес логика в слоя си с данни - person Mads Nielsen; 23.07.2015
comment
Можете ли да обясните използването на таблицата product_details? - person Carlo; 15.05.2016
comment
Вместо две таблици products и product_variants, ще бъде ли по-добре, ако се използва една таблица product_variants с product_id в таблицата product_variants, препращаща обратно към основния идентификатор на продукта. - person Laxman; 04.07.2016
comment
Можете ли да обясните използването на таблицата product_details? - person Green; 07.11.2016
comment
@Green Може би вече разбрахте - таблицата product_details съдържа външните ключове за product_variants и variants_value. Таблицата product_details трябва да позволява обединяването на всички други таблици. - person Benny Thadikaran; 16.08.2018
comment
Къде може да се съхранява запасът от продукт? А наличността на конкретен вариант? - person CodeTrooper; 11.09.2018
comment
В крайна сметка ще разберете, че сте свръхнормализирали, като сте отделили variant_value. Моделът на EAV схема е достатъчно лош; това само го влошава. - person Rick James; 03.01.2019
comment
стойността на атрибут е varchar? какъв би бил най-добрият начин за запитване към продукти, където bestBeforeDate е между определен диапазон? Имам атрибут BestBeforeDate (вариант). - person DmitriBodiu; 10.11.2020

Част от вашите проблеми произтичат от объркване между продукт и SKU.

Когато продавате "Пуловер XYZ, размер M, син модел", последният отговаря на SKU. Предлага се на пазара като пуловер XYZ (продуктът), който има набор от атрибути (размер и цветове), всеки със собствен набор от потенциални стойности. И не всички възможни комбинации от последното могат да доведат до валидни резултати: няма да намерите абсурдно тънки и дълги дънки. SKU, продукти, атрибути, стойности на атрибути.

И когато потребител иска син пуловер за $10, той всъщност търси SKU в продуктова категория.

Надявам се, че горното изяснява объркването ви и откъде произтичат вашият проблем и въпрос.

По отношение на схемата, вие искате нещо подобно:


продукти

  • #идентификация на продукта
  • име
  • описание

По желание добавете също:

  • цена
  • в наличност

Това е таблица, свързана с маркетинг. Нищо друго. Ако нещо извън маркетинга използва продукт във вашето приложение, ще попаднете в свят на болка надолу по пътя.

Цената, ако присъства, е главна цена, използвана за попълване на полето, когато е нула в SKU. Това прави въвеждането на цена по-лесно за потребителя.

in_stock е, надяваме се, разбиращ се флаг, в идеалния случай поддържан от тригер. Трябва да е вярно, ако някоя SKU, свързана с този продукт, е на склад.


product_attributes

  • идентификация на продукта
  • #attribute_id
  • име

стойности на_атрибут_на_продукт

  • атрибут_id
  • #value_id
  • стойност

Това просто съдържа неща като цвят, размер и т.н., заедно с техните стойности като синьо, червено, S, M, L.

Обърнете внимание на полето product_id: създайте нов набор от атрибути и стойности за всеки продукт. Размерите се променят в зависимост от продукта. Понякога е S, M, L и т.н.; друг път ще бъде 38, 40, 42 и какво ли още не. Понякога размерът е достатъчен; друг път се нуждаете от ширина и дължина. Синьото може да е валиден цвят за този продукт; друг може да предлага Navy, Royal Blue, Teal и какво ли още не. НЕ приемайте, че има някаква връзка между атрибутите на един продукт и тези на друг; приликите, когато ги има, са изцяло козметични и случайни.


SKU

  • идентификация на продукта
  • #sku_id
  • цена

По желание добавете:

  • име
  • баркод
  • наличност

Това съответства на доставките, които се изпращат.

Това всъщност е най-важната маса отдолу. Това, а не product_id, почти сигурно е това, което трябва да се споменава в клиентските поръчки. Това е и това, към което трябва да се препраща за съхраняване на запаси и така нататък. (Единственото изключение, което някога съм виждал от последните две точки, е когато продавате нещо наистина родово. Но дори и тогава по-добрият начин да се справите с това според моя опит е да хвърлите n-m връзка между взаимозаменяеми SKU.)

Полето за име, ако го добавите, е предимно за удобство. Ако остане нула, използвайте код от страна на приложението, за да съответства на името на генеричния продукт, разширено, ако е необходимо, със съответните имена и стойности на атрибути. Попълването му позволява да се перифразира последното родово име („Levis' 501, W: 32, L: 32, Цвят: тъмно синьо“) с нещо по-естествено („Levis' 501, 32x32, тъмно синьо“).

В случай, че има значение, складът се поддържа по-добре с помощта на тригер в дългосрочен план, със схема за двойно счетоводство на заден план. Това позволява да се разграничи между наличност и наличност за доставка днес (което е цифрата, която всъщност искате тук) срещу наличност, но вече продадена, сред множеството сценарии от реалния свят, които ще срещнете. О, и... понякога е число, а не цяло число, ако някога трябва да продадете нещо, измерено в килограми или литри. Ако е така, не забравяйте да добавите допълнителен флаг is_int, за да избегнете клиентите да ви изпращат поръчки за .1 лаптопи.


продуктови_варианти

  • идентификация на продукта
  • #sku_id
  • #attribute_id
  • value_id

Това свързва идентификатора на доставката със съответните атрибути и стойности с цел генериране на имена по подразбиране.

Първичният ключ е включен (sku_id, attribute_id).

Може да намерите отклонение в полето product_id. Така е, освен ако не добавите външни ключове, препращащи към:

  • Складови единици (product_id, sku_id)
  • product_attributes (product_id, attribute_id)
  • product_attribute_values ​​(attribute_id, value_id)

(Не забравяйте допълнителните уникални индекси на съответните кортежи, ако решите да добавите тези външни ключове.)


Три допълнителни забележки в заключение.

Първо, бих искал да подчертая още веднъж, че по отношение на потока не всички комбинации от атрибути и стойности дават валиден резултат. Ширината може да е 28-42, а дължината може да е 28-42, но вероятно няма да видите сериозно тесни дънки 28x42. Най-добре е да НЕ попълвате автоматично всяка възможна вариация на всеки продукт по подразбиране: добавете потребителски интерфейс, за да ги активирате/деактивирате според нуждите, направете отметка по подразбиране, заедно с полетата за име, баркод и цена. (Името и цената обикновено се оставят празни; но един ден ще трябва да организирате разпродажба само на сини пуловери на основание, че цветът е прекратен, докато продължавате да продавате другите опции.)

Второ, имайте предвид, ако някога ви се наложи да управлявате допълнително продуктови опции, че много от тях всъщност са прикрити продуктови атрибути и че тези, които не са, дават нови SKU, които също трябва да бъдат взети под внимание, когато става въпрос за съхраняване на склад. По-голяма HD опция за лаптоп, например, всъщност е вариант на същия продукт (нормален срещу голям HD размер), който се маскира като опция поради (много валидни) съображения за потребителския интерфейс. За разлика от това, опаковането на лаптопа като коледен подарък е истинска опция, която има препратки към напълно отделна SKU от счетоводна гледна точка (напр. 0,8 метра подаръчна опаковка) – и ако някога трябва да измислите средни пределни разходи, част от времето на персонала.

И накрая, ще трябва да измислите метод за подреждане на вашите атрибути, техните стойности и последващите варианти. За това най-лесно е да хвърлите допълнително поле за позиция в таблиците с атрибути и стойности.

person Denis de Bernardy    schedule 10.10.2013
comment
Съжалявам, че отговарям на по-стар отговор @Denis, но това е ТАКА. Имам въпрос относно това как SKU проследява своята колекция от стойности на атрибути. Така че, ако направя цвят и материал два атрибута на продукт за стол, тогава задайте 2 възможни стойности за всеки атрибут. Когато отида да направя първата си SKU, не трябва ли таблицата SKU да съдържа препратки към различните идентификатори на стойност на атрибут? Благодаря ви за чудесния коментар, просто съм объркан относно вашата финална маса (product_variants) - person Trey Stout; 24.11.2014
comment
@TreyStout: По мое мнение това е основно проблем с потребителския интерфейс. Дори в приложение, ориентирано към тригер, не бих попълнил автоматично таблицата с варианти, когато се създават атрибути и техните стойности, защото това става объркано и в крайна сметка прави много изчисления за нищо. Вместо това бих накарал интерфейсът за създаване на продукт да попълни пълен списък с потенциални SKU в движение, въз основа на атрибутите и техните стойности. Тези, на които човешки оператор присвоява SKU код (т.е. те са действителни продукти, а не потенциално съществуващи), тогава са единствените, които след това бих съхранил в базата данни. - person Denis de Bernardy; 27.11.2014
comment
@DenisdeBernardy Зает съм с прилагането на вашето решение по мой проект и се чудех дали бихте могли да отидете и да погледнете моя въпрос, който публикувах на: stackoverflow.com/questions/30995983/. Надявам се да чуя от теб :) - person Brendan Vogt; 23.06.2015
comment
Как ще се управляват акциите тук? Предполагам, че тъй като имаме варианти за product_id, ще има наличност за всеки вариант? Но как тогава продуктите, които нямат вариации, биха се справили със складовите наличности? - person CodeTrooper; 11.09.2018
comment
Какво ще стане, ако product_attributes не бяха ограничени само до няколко стойности (съдържащи се в product_attribute_values таблица) и можеха да бъдат въведени от потребителя, за да персонализират напълно своите продукти? Помислете за продукт, чиято ширина може да бъде персонализирана чрез поле за въвеждане на число. Някакви идеи? - person andcl; 02.01.2019
comment
Кажете, че искате да получите всички продукти със син цвят, как бихте се справили с това с този дизайн? - person bfl; 29.01.2019
comment
@CodeTrooper И аз бих искал да знам отговора - person vikrant; 31.10.2020
comment
стойността на productAttribute е varchar? какъв би бил най-добрият начин за запитване към продукти, където bestBeforeDate е между определен диапазон? Имам атрибут BestBeforeDate. - person DmitriBodiu; 10.11.2020

Бих използвал 4 таблици:

generic_product: product_id, name, description 

напр. 1, 'черг', 'черг за кафе' / 2, 'чаша', 'чаша за кафе'

generic_product_property: product_id, property_id, property_name 

напр. 1, 10, 'цвят' / 1, 11, 'материал'

sellable_product: sku, product_id, price 

e.g. 'A121', 1, 50.00 / 'A122', 1, 45.00

sellable_product_property: sku, property_id, property_value 

напр. "A121", 10, "червено" / "A121", 11, "вълна" / "A122", 10, "зелено" / "A122", 11, "вълна"

Това ще позволи на вашия потребител да дефинира всяко свойство за вашите продаваеми продукти, което желае.

Вашето приложение ще трябва да гарантира със своята бизнес логика, че sellable_products са описани напълно (проверете дали за всяко приложимо общо свойство на продукта е дефинирано свойството на продаваем продукт).

person xwoker    schedule 05.10.2013
comment
стойността на property_value е varchar? какъв би бил най-добрият начин за запитване към продукти, където bestBeforeDate е между определен диапазон? Имам атрибут BestBeforeDate (вариант). - person DmitriBodiu; 10.11.2020
comment
Това зависи от представянето на bestBeforeDate. ако currentTimeMili е тривиално, в противен случай зависи от възможностите на вашата база данни. Можете също така да имате няколко полета тип property_value (напр. едно от тип datetime). Тогава това ще бъде компромис между общото решение и най-доброто за вашите конкретни UC - person xwoker; 11.11.2020

Това е подобно на друг въпрос, който видях преди време в SO

Проектиране на база данни: Кой е по-добрият подход?

Ако погледнете там, ще видите, че по същество задавате един и същ въпрос за тясна (базирана на атрибути) спрямо широка таблица. Използвал съм и двете в зависимост от сценария, но бих бил много внимателен с начина, по който го прилагате в момента. И фактът, че наистина няма добър начин да съпоставите тези варианти със SKU (поне не, за който се сещам), може да ви принуди да промените таблиците си.

Ако имате толкова много различни варианти, може също да искате да разгледате база данни с ключ-стойност или друго NoSQL решение.

person Brad    schedule 02.10.2013

Най-общо казано, вие търсите това, което се нарича grouper или junk измерение. По принцип това е само ред за всяка комбинация. Схемата на @sahalMoidu изглежда така, сякаш трябва да ви даде това, което искате.

Но преди да се захванете твърде много с нормализацията, трябва да знаете дали db е там за съхраняване на данни (транзакционни и т.н.) или за извличане на данни (размерни, отчитане и т.н.). Дори и да е транзакционна база данни, трябва да се запитате какво се опитвате да постигнете чрез нормализиране.

person Andrew    schedule 09.10.2013

Sku е вашият основен ключ. Можете да настроите връзки на външен ключ към таблицата с варианти с sku. Забравете напълно за productid.

Създаване на таблица x (sku, цена, описание) първичен ключ sku

person danny117    schedule 11.10.2013
comment
стойността на атрибут е varchar? какъв би бил най-добрият начин за запитване към продукти, където bestBeforeDate е между определен диапазон? Имам атрибут BestBeforeDate (вариант). - person DmitriBodiu; 10.11.2020