具有可变属性的多个产品类型的数据库设计

时间:2013-06-15 10:31:26

标签: database

我有一个包含不同产品类型的数据库。每种类型都包含彼此差异很大的字段。第一类产品分为三类。第二类产品分为三类。但第三个和第四个,没有任何分类。

每种产品可以包含任意数量的不同属性。

我使用的数据库模型基本如下: (见链接) https://www.damirsystems.com/static/img/product_model_01.png

我有一个庞大的数据库,在产品表中包含大约500000个产品。

因此,当我要从数据库中获取具有所有属性的产品,或者按属性搜索产品过滤时,它会严重影响性能。

任何人都可以帮助我在sql中的表结构,或者为这个问题做一些索引或任何可行的解决方案。因为不同的电子商务网站正在使用这种数据库,并且可以使用大量不同类型的产品。


编辑:图像链接(在我的网站上)被阻止,所以这里是图像

enter image description here

1 个答案:

答案 0 :(得分:26)

您链接的模型看起来像部分entity–attribute–value(EAV)模型。 EAV非常灵活,但数据完整性差,而且繁琐且通常效率低下。它并非真正符合关系模型的精神。在一些大型电子商务网站上工作,我可以告诉你,这不是该领域的标准或良好的数据库设计实践。

如果您没有大量类型的产品(最多数十个,但不是数百个),那么您可以使用两种常用方法中的一种来处理此问题。

第一种方法是简单地为产品设置一个表,其中包含每种不同类型产品可能需要的所有属性的列。您可以使用适合每种产品的列,并将其余列留空。假设您销售书籍,音乐和视频:

create table Product (
    id integer primary key,
    name varchar(255) not null,
    type char(1) not null check (type in ('B', 'M', 'V')),
    number_of_pages integer, -- book only
    duration_in_seconds integer, -- music and video only
    classification varchar(2) check (classification in ('U', 'PG', '12', '15', '18')) -- video only
);

这具有简单且不需要连接的优点。但是,它并没有很好地执行您的数据的完整性(例如,您可能有一本没有多个页面的书),如果您有多种类型的产品,那么该表将会得到非常笨拙。

您可以使用表级检查约束来解决完整性问题,这些约束要求每种类型的产品都具有某些列的值,如下所示:

check ((case when type = 'B' then (number_of_pages is not null) else true end)))

(在那里向Joe Celko提示 - 我查找了如何在SQL中进行逻辑蕴涵,并找到了一个例子,他用这种结构来构建一个非常相似的检查约束!)

你甚至可以说:

check ((case when type = 'B' then (number_of_pages is not null) else (number_of_pages is null) end)))

确保没有行的列中的值不适合其类型。

第二种方法是使用多个表:一个基表包含所有产品通用的列,一个辅助表用于每种类型的产品,其中包含特定于该类型产品的列。所以:

create table Product (
    id integer primary key,
    type char(1) not null check (type in ('B', 'M', 'V')),
    name varchar(255) not null
);

create table Book (
    id integer primary key references Product,
    number_of_pages integer not null
);

create table Music (
    id integer primary key references Product,
    duration_in_seconds integer not null
);

create table Video (
    id integer primary key references Product,
    duration_in_seconds integer not null,
    classification varchar(2) not null check (classification in ('U', 'PG', '12', '15', '18'))
);

请注意,辅助表与主表具有相同的主键;它们的主键列也是主表的外键。

这种方法仍然相当简单,并且可以更好地执行完整性。但查询通常涉及连接:

select
  p.id,
  p.name
from
  Product p
  join Book b on p.id = b.id
where
  b.number_of_pages > 300;

完整性仍然不完美,因为可以在与主表中的错误类型的行对应的辅助表中创建行,或者在与单行对应的多个辅助表中创建行在主表中。您可以通过进一步细化模型来解决这个问题。如果您将主键设为包含类型列的复合键,则产品的类型将嵌入其主键中(书籍将具有主键,如(' B',1001))。您需要将类型列引入辅助表,以便它们可以使用指向主表的外键,并且您可以在每个辅助表中添加一个检查约束,该约束要求类型正确。像这样:

create table Product (
    type char(1) not null check (type in ('B', 'M', 'V')),
    id integer not null,
    name varchar(255) not null,
    primary key (type, id)
);

create table Book (
    type char(1) not null check (type = 'B'),
    id integer not null,
    number_of_pages integer not null,
    primary key (type, id),
    foreign key (type, id) references Product
);

这也使得在只给出主键的情况下查询正确的表更容易 - 您可以立即告诉它所引用的产品类型,而无需先查询主表。

仍然存在重复列的潜在问题 - 如上图所示,其中duration列在两个表中重复。您可以通过为共享列引入中间辅助表来解决此问题:

create table Media (
    type char(1) not null check (type in ('M', 'V')),
    id integer not null,
    duration_in_seconds integer not null,
    primary key (type, id),
    foreign key (type, id) references Product
);

create table Music (
    type char(1) not null check (type = 'M'),
    id integer not null,
    primary key (type, id),
    foreign key (type, id) references Product
);

create table Video (
    type char(1) not null check (type = 'V'),
    id integer not null,
    classification varchar(2) not null check (classification in ('U', 'PG', '12', '15', '18')),
    primary key (type, id),
    foreign key (type, id) references Product
);

你可能认为不值得付出额外的努力。但是,你可能会考虑做的是混合两种方法(单表和辅助表)来处理这样的情况,并为一些类似的产品提供共享表:

create table Media (
    type char(1) not null check (type in ('M', 'V')),
    id integer not null,
    duration_in_seconds integer not null,
    classification varchar(2) check (classification in ('U', 'PG', '12', '15', '18')),
    primary key (type, id),
    foreign key (type, id) references Product,
    check ((case when type = 'V' then (classification is not null) else (classification is null) end)))
);

如果在应用程序中将类似的产品集中在一起,那将特别有用。在这个例子中,如果你的店面一起呈现音频和视频,但是与书籍分开,那么这种结构可以支持比为每种媒体分别使用单独的辅助表更有效的检索。

所有这些方法共享一个漏洞:它仍然可以在主表中创建行,而在任何辅助表中都没有相应的行。要解决此问题,您需要第二组外键约束,这次是从主表到辅助表。由于以下几个原因,这一点特别有趣:您希望一次强制执行一个可能的外键关系,并且该关系在两个表中的行之间创建循环依赖关系。您可以使用检查约束中的某些条件实现前者,使用deferrable constraints使用后者。辅助表可以与上面相同,但是主表需要增长我将暂时调用的类型标记'列:

create table Product (
    type char(1) not null check (type in ('B', 'M', 'V')),
    id integer not null,

    is_book char(1) null check (is_book is not distinct from (case type when 'B' then type else null end)),
    is_music char(1) null check (is_music is not distinct from (case type when 'M' then type else null end)),
    is_video char(1) null check (is_video is not distinct from (case type when 'V' then type else null end)),

    name varchar(255) not null,
    primary key (type, id)
);

类型标志列基本上是type列的重复,每个潜在类型一个,当且仅当产品属于该类型时才设置(由这些检查约束强制执行)。这些是真实的列,因此在插入行时必须为它们提供值,即使这些值是完全可预测的;这有点难看,但希望不是一个表明。

有了这些,然后在创建所有表之后,您可以使用类型标志而不是类型来形成外键,指向特定的辅助表:

alter table Product add foreign key (is_book, id) references Book deferrable initially deferred;
alter table Product add foreign key (is_music, id) references Music deferrable initially deferred;
alter table Product add foreign key (is_video, id) references Video deferrable initially deferred;

至关重要的是,要强制执行外键关系,其所有组成列必须为非null。因此,对于任何给定的行,因为只有一个类型标志是非空的,所以只会强制执行一个关系。由于这些约束是可延迟的,因此可以在辅助表中的所需行存在之前将行插入主表。只要在交易提交之前插入它,它就全都在板上。