您链接到的模型看起来像部分实体-属性-值(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)))
);
如果在应用程序中将类似类型的产品集中在一起,这将特别有用。在此示例中,如果您的店面同时呈现音频和视频,但与书籍分开呈现,那么这种结构可以支持更有效的检索,而不是为每种媒体提供单独的辅助表。
所有这些方法都有一个漏洞:仍然可以在主表中创建行,而在任何辅助表中没有相应的行。要解决这个问题,您需要第二组外键约束,这次是从主表到辅助表。由于以下几个原因,这特别有趣:您希望一次强制执行可能的外键关系之一,并且该关系在两个表中的行之间创建了循环依赖关系。您可以使用检查约束中的一些条件来实现前者,而后者使用可延迟约束来实现。辅助表可以和上面一样,但是主表需要增加我暂时称之为“类型标志”的列:
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;
至关重要的是,要强制执行外键关系,它的所有组成列都必须是非空的。因此,对于任何给定的行,因为只有一个类型标志是非空的,所以只有一个关系将被强制执行。因为这些约束是可延迟的,所以可以在辅助表中所需的行存在之前将一行插入主表。只要在事务提交之前插入,一切都是光明正大的。