具有许多产品类别的网站的数据库设计

时间:2019-02-20 11:19:43

标签: sql postgresql database-design

我是SQL的新手。尝试学习尽可能多的东西,因此将小型网上商店作为我的培训目标。 我正在努力应对数据库结构。 我想实现的是:

  • 带有ID和名称的类别(例如电视,洗衣机)
  • 字段(每个类别的字段都限于该类别(例如电视的-分辨率,HDR等,洗衣机-容量,洗涤周期)
  • 产品(每个产品应具有常规字段(名称,品牌等),另外还有类别字段,这些字段在每个类别中都不同。

因此,系统方面的主要思想是创建一个类别,向其添加字段,然后使用常规+类别字段将一些产品插入该类别。

我该如何实现?我试图将所有这些与一对多关系联系起来,但这似乎并没有按我预期的那样工作

3 个答案:

答案 0 :(得分:2)

这是一个已知的(反)模式,称为“实体属性值”(如果要了解更多信息,可以在Internet上搜索该名称)。

现在(尤其是对于Postgres),我会去一个JSONB列,该列存储每个产品的类别特定属性,而不是附加的fields表。

您甚至可以根据product表中的元信息来验证category表中的动态属性。

是这样的:

create table category
(
   id integer primary key, 
   name varchar(50) not null,
   allowed_attributes jsonb not null
);

create table product
(
   id integer primary key, 
   name varchar(100) not null, 
   brand varchar(100) not null, -- that should probably be a foreign key
   ... other common columns ...
);

create table product_category
(
   product_id integer not null references product,
   category_id integer not null references category, 
   attributes jsonb not null, -- category specific attributes
   primary key (product_id, category_id)
);

现在,有了类别表中的“允许的属性”列表,我们可以编写一个触发器来验证它们。

首先,我创建一个辅助函数,以确保一个JSON值中的所有键都存在于另一个中:

create function validate_attributes(p_allowed jsonb, p_to_check jsonb)
  returns boolean
as
$$
   select p_allowed ?& (select array_agg(k) from jsonb_object_keys(p_to_check) as t(k));
$$
language sql;

此函数随后在类别表的触发器中使用:

create function validate_category_trg()
  returns trigger
as
$$
declare
   l_allowed jsonb;
   l_valid   boolean;
begin

   select allowed_attributes 
      into l_allowed
   from category
   where id = new.category_id;

   l_valid := validate_attributes(l_allowed, new.attributes);
   if l_valid = false then 
     raise 'some attributes are not allowed for that category';
   end if;
   return new;
end;
$$
language plpgsql;

现在让我们插入一些示例数据:

insert into category (id, name, allowed_attributes)
values
(1, 'TV Set', '{"display_size": "number", "color": "string"}'::jsonb), 
(2, 'Laptop', '{"ram_gb": "number", "display_size": "number"}');

insert into product (id, name)
values
(1, 'Big TV'),
(2, 'Small  TV'),
(3, 'High-End Laptop');

现在我们插入类别信息:

insert into product_category (product_id, category_id, attributes)
values
(1, 1, '{"display_size": 60}'),  -- Big TV 
(2, 1, '{"display_size": 32}'),  -- Small TV
(3, 2, '{"ram_gb": 128}'); -- Laptop

这是有效的,因为所有属性都在类别中定义。如果我们尝试插入以下内容:

insert into product_category (product_id, category_id, attributes)
values
(3, 2, '{"usb_ports": 5}');

然后,触发器将引发异常,从而阻止用户插入行。

可以扩展为实际使用存储在allowed_attributes中的数据类型信息。

要根据属性查找产品,我们可以使用Postgres提供的JSON functions,例如所有具有display_size的产品:

select p.*
from product p
where exists (select *
              from product_category pc
              where pc.product_id = p.id 
                and pc.attributes ? 'display_size');

查找包含多个属性的产品同样简单(而使用“传统” EAV模型则要复杂得多)。

以下查询仅查找具有属性display_size ram_gb

的产品
select p.*
from product p
where exists (select *
              from product_category pc
              where pc.product_id = p.id 
                and pc.attributes ?& '{display_size, ram_gb}');

此索引可以非常有效地索引,以加快搜索速度。


我不确定您是否确实希望将属性存储在product_category表中。也许应该将它们直接存储在product表中-但这取决于您的要求和管理方式。

使用上述方法,您可以具有“计算机硬件”类别,该类别将存储诸如CPU数量,RAM和时钟速度之类的信息。可以使用该类别(及其属性),例如同时使用智能手机和笔记本电脑。

但是,如果要这样做,您将需要在product_category中多行描述一个产品。

最常见的方法可能是将属性直接存储在产品上,并跳过所有动态JSONB验证。

是这样的:

create table category
(
   id integer primary key, 
   name varchar(50) not null
);

create table product
(
   id integer primary key, 
   name varchar(100) not null, 
   brand varchar(100) not null, -- that should probably be a foreign key
   attributes jsonb not null, 
   ... other common columns ...
);

create table product_category
(
   product_id integer not null references product,
   category_id integer not null references category, 
   primary key (product_id, category_id)
);

或者,如果您需要类别特定的动态属性和产品特定属性(无论类别如何),甚至可以将两者组合使用。

答案 1 :(得分:0)

您可以创建联结表和外键来表示表之间的关系。

类别表

id |名称

字段表

id |名称

类别字段表

id | category_id | field_id

品牌 id |名称

产品表

id | category_id | brand_id |名称

产品功能

id | product_id | field_id |值

对于产品表格,您可能需要考虑为品牌使用单独的表格,并在brand_id表格中包含products列而不是名称,以避免重复。

category_fields表将存储类别的id和相关字段的id,表中的每一行代表该类别的不同字段。

然后,表product_features将存储特征,这些特征取决于分配给产品类别的字段。

答案 2 :(得分:0)

使用基于Dataphor的伪代码,内联引用(外键),数据类型和无关详细信息:

create table Category {
  CategoryId,
  CategoryName,
  key { CategoryId },
  key { CategoryName } /* Don't want categories that differ only by surrogate id */
};

/* Allowed fields */
create table CategoryField {
  CategoryId,
  FieldName,
  key { CategoryId, FieldName },
  reference CategoryField_Category
    { CategoryId } references Category { CategoryId }
};

create table Product {
  ProductId,
  ProductName,
  ProductBrand,
  CategoryId,
  key { ProductId }, /* Probably other attributes, keys and references as well */
  reference Product_Category
    { CategoryId } references Category { CategoryId }
};

create table ProductFieldValue {
  ProductId,
  CategoryId, /* Violates BCNF, but is controlled by foreign superkey */
  FieldName,
  FieldValue,
  key { ProductId, FieldName },
  reference PFV_Product
    { ProductId, CategoryId } references Product { ProductId, CategoryId },
  reference PFV_CategoryField
    { CategoryId, FieldName } references CategoryField { CategoryId, FieldName }
};

重叠的外键(我更喜欢术语“引用”,特别是因为其中之一实际上是一个适当的外键),请确保每个产品只能具有根据CategoryField表中的行的字段值。

此模型中有一些冗余-ProductFieldValue违反了Boyce-Codd正常形式(也为2NF,但没关系)-因此,您必须自己决定简单完整性控制的好处是否胜过该缺点。但是请注意,冗余是受控制的。不会有任何不一致的地方。

此模型假定所有字段值都具有相同的数据类型,例如一个字符串。如果您还希望对此进行限制(例如,某些字段只能包含数字值;某些字段会被枚举等等),事情会变得更加复杂。