【问题标题】:Database Design For Multiple Product Types with variable attributes具有可变属性的多种产品类型的数据库设计
【发布时间】:2013-06-15 10:31:26
【问题描述】:

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

每个产品可以有任意数量的不同属性。

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

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

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

谁能帮助我在 sql 中的表结构是什么,或者为这个问题做更多的索引或任何可行的解决方案。因为不同的电子商务网站都在使用这种数据库,并且可以很好地处理大量不同类型的产品。


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

【问题讨论】:

标签: database


【解决方案1】:

您链接到的模型看起来像部分 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
);

这也使得只给定主键就可以更轻松地查询正确的表 - 您可以立即知道它所指的产品类型,而无需先查询主表。

仍然存在列重复的潜在问题 - 如上面的架构,其中持续时间列在两个表中重复。您可以通过为共享列引入中间辅助表来解决此问题:

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;

至关重要的是,要强制执行外键关系,它的所有组成列都必须是非空的。因此,对于任何给定的行,因为只有一个类型标志是非空的,所以只有一个关系将被强制执行。因为这些约束是可延迟的,所以可以在辅助表中所需的行存在之前将一行插入主表。只要在事务提交之前插入,一切都是光明正大的。

【讨论】:

  • 哦,好吧,真可惜。祝你好运找到解决方案!
  • @mils:“提供较差的数据完整性,而且很麻烦而且通常效率低下”就是这样。我的全部答案是关于如何应用数据库约束的全部力量来保持数据健康;在 EAV 中,您基本上无法做到这一点,因为每个产品都只是一袋属性,因此您无法编写有意义的约束。 EAV 基本上是和 MongoDB 一样的数据模型!最重要的是,如果要进行查询,则必须进行大量连接,这编写起来很痛苦,运行起来也很慢。
  • 这是一个很好的答案,但我很困扰“但是,仍然可以在主表中创建行而在任何辅助表中没有相应的行。我不知道如何解决那。”部分——从那以后的 5 年里,你有没有找到解决这个问题的方法?我真的很想在数据库级别强制执行此操作,但我找不到任何可接受的解决方案(我拥有的解决方案太长,不适合此评论,这可能解释了为什么我不急于将其落实到位...)。 TIA!
  • @VZ 我还没有在野外看到任何解决方案,但我想到了一个,并添加了它。
  • @TomAnderson 我试图实现您出色解释的第二种方法,但无法将我引用的表的主键更改为复合键,因为它在具有太多其他依赖项的生产环境中。我解决了以下问题(翻译为您的示例):我在 Product (type,id) 上创建了一个唯一键,然后在 Media 表中将其作为普通外键引用以强制执行正确的类型。因此 Product 的 PK 可以仅保留为 id 列。 PS:很长一段时间以来我见过的最佳 SQL 设计答案。 :)
猜你喜欢
  • 1970-01-01
  • 2017-03-29
  • 1970-01-01
  • 2017-03-14
  • 1970-01-01
  • 2013-02-15
  • 2011-02-26
  • 2017-08-09
  • 2023-04-06
相关资源
最近更新 更多