一张桌子还是多张?

时间:2008-10-14 08:39:06

标签: database database-design

我正在尝试设计一个应用程序来保存学术参考信息。问题是每种不同类型的参考(例如期刊文章,书籍,报纸文章等)都需要不同的信息。例如,期刊参考既需要期刊标题,也需要文章标题,还需要页码,而一本书需要出版商和期刊文章不需要的出版日期。

因此,我是否应该将所有引用存储在我的数据库中的一个表中,并且当它们不适用时将字段留空,或者我应该有各种表,例如BookReferences,JournalReferences,NewspaperReferences,并在每个表中放入适当的引用一。那么问题是它会使搜索所有参考文件变得更加困难,而且编辑也可能需要更加单独地完成。

(顺便说一下,我打算在这个项目中使用Ruby on Rails,但我怀疑这对这个设计问题有什么不同)

更新

还有更多观点吗?我希望得到一个简单的答案,说一个特定的方法肯定被认为是“最好的” - 但通常事情并不像这样简单。单表继承选项看起来很有趣,但是我找不到很多关于它的信息 - 我可以在这个网站上发布另一个关于它的问题。

我在Olvak's answerCorey's answer之间分开。科里的答案给出了奥尔瓦克不是最好的理由,但奥尔瓦克的答案给出了科里为什么不是最好的理由!我从未意识到这可能会如此困难......

任何进一步的建议都非常感谢!

15 个答案:

答案 0 :(得分:35)

我会为所有引用设置一个表,但是其他表(如BookReferences等)的元数据不适用于所有引用类型。

搜索和查询并不会更困难 - 毕竟你可以创建一个聚合所有信息的视图,就像在单表解决方案中一样,然后进一步查询该视图。

将所有内容放在一个包含大量空值的表中似乎是更简单的解决方案,但实际上它会导致很多麻烦。例如:使用单独的表,您可以为每个BookReference定义哪些字段 required ,但如果所有字段都在一个表中,则每个字段都必须是可空的,因此是可选的。插入无效数据也会更容易,例如书籍参考也错误地包含非空的期刊名称。

编辑:有些人似乎害怕加入。 不要害怕加入!如果你在几个确实繁琐的查询中使用完全相同的连接,但在这种情况下,连接应该在视图中定义,并且您的查询应该查询该视图。视图实际上是关系数据库中的基本抽象,您应该使用它们的原因与在代码中使用函数的原因相同:避免重复,并封装和创建抽象。

编辑:有一些关于表现的评论。事先很难猜测数据库模式的性能,因为它通常是非直观的。例如,几个表之间的连接可以比单个表的完整表扫描更快 - 这完全取决于查询的类型,数据的性质,可用的索引等。此外,在许多数据库系统中,您可以使用物化视图等功能来优化不同查询的性能,而不会影响逻辑模型。除非你是谷歌或Flickr,否则“非正规化的表现”现在主要是货币崇拜,除非你是Google或Flickr。

答案 1 :(得分:10)

“一张大桌子让生活变得更轻松”:我已经看到了这种自然的结果,是一张100多列的桌子,我可以告诉你,我发现这并不是一件好事。

主要问题是这些表的设计者倾向于省略确保数据完整性所需的约束。例如,OP说:

  

期刊参考既需要期刊标题,也需要文章标题,还需要一个页码,而一本书需要出版商和期刊文章不需要的出版日期

...这意味着以下约束:

CONSTRAINT a_journal_must_have_a_journal_title
   CHECK ( type <> 'journal' OR journal_title IS NOT NULL );

CONSTRAINT a_journal_must_have_an_article_title 
   CHECK ( type <> 'journal' OR article_title IS NOT NULL );

CONSTRAINT a_journal_must_have_a_page_number 
   CHECK ( type <> 'journal' OR page_number IS NOT NULL );

CONSTRAINT a_journal_cannot_have_a_publisher 
   CHECK ( type <> 'journal' OR publisher IS NULL );

CONSTRAINT a_journal_cannot_have_a_publication_date 
   CHECK ( type <> 'journal' OR publication_date IS NULL );

CONSTRAINT a_book_cannot_have_a_journal_title 
   CHECK ( type <> 'book' OR journal_title IS NULL );

CONSTRAINT a_book_cannot_have_a_article_title 
   CHECK ( type <> 'book' OR article_title IS NULL );

CONSTRAINT a_book_cannot_have_a_page_number 
   CHECK ( type <> 'book' OR page_number IS NULL );

CONSTRAINT a_book_must_have_a_publisher 
   CHECK ( type <> 'book' OR publisher IS NOT NULL );

CONSTRAINT a_jbook_must_have_a_publication_date 
   CHECK ( type <> 'book' OR publication_date IS NOT NULL );

......我怀疑这只是冰山一角!

我希望在写完几百个这样的约束之后,设计师可能会对所有那些可以为空的列进行再次思考:)

答案 2 :(得分:7)

我的建议是从正确设计数据库开始,即使用规范化来确保表只包含有关一件事(书籍,期刊等)的数据,并且这些属性存储在右表中。

如果将来它会产生性能问题,您可以将其去规范化为更少的表,但除非您拥有庞大的数据库,否则这不太可能成为问题。

创建一个表,其中包含所有引用的公共属性。

创建单独的表以保存特定于每种引用类型的属性。

另一个问题是你是否会对一件作品有很多引用,例如。数百个对特定期刊的引用。然后规范化会建议您有一个表格,其中包含期刊(标题,作者,期刊),一个表格,其中包含特定于期刊(文章,页面)的参考信息,另一个表格保存所有参考文献共有的数据(参考日期,参考类型)。

答案 3 :(得分:4)

在添加需要额外字段的新引用类型时,使用具有“type”字段的单表将会出现问题。扩展类型字段值没有问题,但您必须向表中添加列,填充所有当前行的默认值等。

拥有单独的表格可以轻松添加新的引用类型(并自动生成表单!),搜索也不会更难。

答案 4 :(得分:3)

Rails支持单表继承和多态ActiveRecord类型。我建议调查这些 - ActiveRecord对如何构建数据库有一些看法。

答案 5 :(得分:3)

我认为你必须展望每个解决方案的SQL会是什么样子。如果你通过这个练习,那么你会发现将所有内容放在一个表中是最容易编码的,并且可能会带来最佳性能。从一个表中分离出你想要的东西比从多个表中分离出来更容易。

让我说一个大桌子看起来像这样:

1 id
2型
3字段 - 通用到书籍和期刊
4字段特定的书籍 5字段特定到期刊

如果我只对书籍感兴趣,我可以创建一个视图,或者只是简单的sql,如下所示:

create view book as  
select id, field_common-to-book-and-journal, field-specific-to-book
from my-one-big-table
where type = 'book'

因此,在我想要的时候,很容易模拟数据在单独的表中。

但是,如果我把数据放在单独的表中,那么我最终会像这样编写SQL:

select id, field-common-to-book-and-journal from books
union
select id, field-common-to-book-and-journal from journal-articles
union
.... etc, for each type

我不了解其他数据库,但在SQL Server中进行联合可能成本很高,并且在使用ntext等数据类型时存在限制。

如果您遵循olavk的建议,那么您在一个查询中组合类型的SQL将最终看起来像这样:

select 
    common.id, 
    common.field-common-to-book-and-journal, 
    book.field-specific-to-book 
    journal.field-specific-to-journal
from common-table common
left outer join book-specific-table book on 
left outer join journal-specific-table journal on
... etc, for each type

我使用过所有这三种方式的系统,到目前为止,用一张大桌子可以轻松实现生活。

答案 6 :(得分:2)

其中最好的取决于有多少不同的字段和字段大小,你对总行大小有限制(在某种程度上可以忽略这一点,因为知道所有字段永远都不会被填写,但是一旦你到达页面太宽的地方,数据库中的实际存储最终会分割信息,使得检索需要更长时间。因此,如果信息很小并且(这很重要)不太可能发生太大变化(这将是一个罕见的事件,需要添加尚未考虑的新类型的信息),那么单个表是更好的路径。如果表太宽或者需要存储的数据类型会有很多可能的变化,那么特殊表格是一种更好的方法,虽然它总是更难以正确查询。如果你经常想要同时查询多种类型的引用,那么大表是一种更有效的方法。如果你通常只需要抓一个在一次,你失去了很多在加入效率方面。

如果您选择使用一个表路由,请确保在表上放置触发器,以强制执行每种类型数据的数据完整性规则。你需要这个,因为你不能依赖于制作所需的字段。

拥有单独表的一个问题是,在运行时您不需要知道需要加入哪些表。这使您处于动态SQl领域,我不喜欢它(出于安全性,效率和维护的原因),或者让您对可能需要或可能不需要的表格进行左连接,效率低下。

另一种可能性是将整个refence字符串存储在一个较大的字段中,并使用用户界面检查以确保在记录并将信息发送到数据库之前所有必需的部分都在那里。对于大多数想要获取所有信息的查询来说,这是最快的查询,但如果你只需要提取一些数据就会很痛苦。它还依赖于通过用户界面插入的所有数据,这可能是您的情况,也可能不是。说实话,我看不出你需要将这些信息单独分开,所以这就是我可能采取的方法。但是我不知道你的商业规则,所以不要理睬。

答案 7 :(得分:1)

还有另一种选择:不是我完全支持的选项,但它仍然是另一种选择:

使用三个表:

refs (id, title, refType)
-- title of the reference, and what type of reference it is

fieldDef (id, fieldName, refType, dataType)
-- name of the field, which reference types it applies to, and
-- what type of data is stored in these fields (ISDN number, date, etc)

fields (refId, fieldId, value)
-- where you actually add data to the references.

refType可以是引用的类型,如果你使它成为一个整数,其值增加2的幂(1,2,4,8 ......),那么它们可以加在一起制作一个fieldDef表中的位掩码。

优点:非常简单且可扩展。如果您想出另一种类型的引用,或现有引用类型的新字段类型,则可以非常快速地添加它。可以为每种引用类型自动生成表单。所有数据都存储在一个地方,这意味着您不需要跟踪CRUD operations的多个模式(schemata?)

缺点:这就是每日WTF制作的东西。选择语句会变得非常混乱和复杂。数据库无法执行类型检查(例如:对于日期等),并且通用“值”字段将不会针对存储在其中的数据进行优化。

答案 8 :(得分:1)

我认为没有必要加入表格特别繁琐;我会采取更加规范化的方法。

答案 9 :(得分:0)

一个表和一个“类型”字段将是我的建议

答案 10 :(得分:0)

您问的是数据库规范化。杰夫阿特伍德在他的帖子Maybe Normalizing Isn't Normal中写到了这一点。这是一个很好的阅读。

答案 11 :(得分:0)

我过去最终做的是使用子类别:在其中包含一个包含所有公共字段的表,然后是几个可以具有零或一个表的表与“核心”表的关系。

以下示例类似于我们“在野外”使用的内容;它基本上构建了一个分层数据结构,其中每个节点可以是一个文件夹或文档:

CREATE TABLE Node (
  Id int identity primary key,
  ParentId int null references Node.ParentId,
  Name varchar(50) not null,
  Description varchar(max) null
)

CREATE TABLE Doc (
  Id int primary key references Node.Id,
  FileExtension char(3) not null,
  MimeType varchar(50) not null,
  ContentLength bigint not null,
  FilePathOnDisk varchar(255)
)

CREATE TABLE Folder (
  Id int primary key references Node.Id,
  ReadOnly bit not null
)

所以你的GetFolder sproc会这样做:

SELECT n.Id, n.ParentId, n.Name, n.Description, f.ReadOnly
FROM Node n 
JOIN Folder f ON n.Id = f.Id
WHERE f.Id = @Id

这很好地转换为基于类的继承:

public class Folder : Node
{
  public bool IsReadOnly { get; set; }
  ...etc
}

答案 12 :(得分:0)

Olavk提出了很好的观点,Corey给出了非常详细的解释。但是,阅读科里的信息,可以得出奥拉夫克答案的结论。请记住,根据您对信息的处理方式,您最终可能会暂停查询。找到该项目,然后针对每个参考,直接选择感兴趣的内容。

还要考虑将所有内容存储在多个表中并从单个表中读取它的想法。我为大型数据库执行此操作,其中大多数查询需要某些常见信息,但仍需要完整的多表布局。插件因它们启动的触发器而减慢一点(在我的情况下,每个文件一个,其中每个文件负责插入多达一百万行),但我后来的选择查询可以从几分钟到几位数秒。

数据仓库:)

答案 13 :(得分:0)

前段时间我和我的上级讨论过这些问题。当然,我无法证明“分层多表方法”(见olavk's answer)更好,但我感觉到了!我会一直选择这种方法。一个根表包含实体共有的所有字段,1-1个子表包含它们没有共同的字段。如果需要这种方法可以扩展到更多子表,只要业务逻辑和其他实体将有一些东西。也就是说,我认为不需要过分支持这一点。

我也反对在没有根表的情况下创建单独的“子”表,其中每个表都有相同字段的副本。我认为Corey's answer建议将这种方法作为一个糟糕的多表模型的例子,他也批评它。我想补充说,必须编写连接不是主要问题。这根本不是问题,因为大多数数据库查询都有很多连接,这是正常的事情。很难与其他表创建关系 - 您总是需要Id和TypeId来知道哪个表链接到它。对于根表,您只需要Id。

答案 14 :(得分:-1)

两者怎么样? 你的蛋糕也吃了!

在“一个大桌子”和“完全规范化”的DB之间还有另一个选项,它真正结合了两个世界的优点:你可以使用名为materialized views的东西,就像它们只是视图一样尽可能灵活,并根据需要查询多个表,设置所有连接等,但它们也像表一样,结果实际存储在表中。

关于这一点的好处是,一旦你设置了它并决定何时刷新它(每当其中一个基础表sis发生变化,或者每晚只有一次)你就不再担心它了。您可以查询物化视图,就好像它是一个大表(因为它是),并且性能将快(比使用它后面的select语句更快)。最重要的是,您没有维护数据完整性的麻烦。这就是DB要处理的内容。

如果您没有支持此功能的数据库,您仍然可以通过在每晚的批处理作业中根据视图结果构建一个表来使用这个想法。