我一直在尝试设计一个与项目概念相适应的数据库,并遇到了一个似乎引起激烈争论的问题。我已经阅读了几篇文章和一些Stack Overflow的答案,指出永远不会(或几乎永远不会)在字段中存储ID或类似内容的列表-所有数据都应该是相关的,等等。

不过,我遇到的问题是,我试图做一个任务分配器。人们将创建任务,将其分配给多个人,并将其保存到数据库中。

当然,如果我将这些任务单独保存在“人”中,则必须有数十个虚拟对象“ TaskID”列并对其进行微管理,因为例如可以将0到100个任务分配给一个人。

然后,如果我将任务保存在“任务”表中,我将必须拥有几十个虚拟的“ PersonID”列并对其进行微管理-与以前一样的问题。

对于这样的问题,可以以一种形式或另一种形式保存ID列表吗?还是我只是在想另一种不违反原则就可以实现的方法?

评论

我意识到这被标记为“关系数据库”,所以我只将其作为注释而不是答案,但在其他类型的数据库中,确实有存储列表的意义。卡桑德拉(Cassandra)因为没有连接而浮现在脑海。

做得好,然后在这里提问!的确,从不违反第一范式的“建议”对您而言确实很不错,因为您真的应该想出另一种关系方法,即“多对多”关系,为此存在一种标准模式。应该使用的关系数据库。

“还好吗”是的……无论如何,答案是肯定的。只要您有正当的理由。总是有一个用例会迫使您违反最佳实践,因为这样做很有意义。 (不过,就您而言,您绝对不应该)

我目前正在使用数组(而不是定界字符串-VARCHAR ARRAY)来存储标签列表。这可能不是最终将它们存储在行尾的方式,但是列表在原型设计阶段非常有用,当您没有其他要指向的内容并且不想在无法构建整个数据库架构之前做其他事情。

@Ben“(尽管它们将无法建立索引)”-在Postgres中,针对JSON列的多个查询(可能还有XML,尽管我没有检查过)都是可索引的。

#1 楼

您需要研究的关键词和关键概念是数据库规范化。

您要做的是,与其添加有关人员表或任务表的分配信息,不如添加具有该表的新表。分配信息以及相关关系。

示例,您具有以下表格:

人员:

+−−−−+−−−−−−−−−−−+
| ID |    Name   |
+====+===========+
|  1 |  Alfred   |
|  2 |  Jebediah |
|  3 |  Jacob    |
|  4 |  Ezekiel  |
+−−−−+−−−−−−−−−−−+


任务:

+−−−−+−−−−−−−−−−−−−−−−−−−−+
| ID |        Name        |
+====+====================+
|  1 |  Feed the Chickens |
|  2 |  Plow              |
|  3 |  Milking Cows      |
|  4 |  Raise a barn      |
+−−−−+−−−−−−−−−−−−−−−−−−−−+


然后您将创建带有Assignments的第三个表。该表将模型化人员与任务之间的关系:

+−−−−+−−−−−−−−−−−+−−−−−−−−−+
| ID |  PersonId |  TaskId |
+====+===========+=========+
|  1 |         1 |       3 |
|  2 |         3 |       2 |
|  3 |         2 |       1 |
|  4 |         1 |       4 |
+−−−−+−−−−−−−−−−−+−−−−−−−−−+


然后我们将具有外键约束,以便数据库将强制执行PersonId和TaskIds必须是这些外国物品的有效ID。对于第一行,我们可以看到PersonId is 1,因此将Alfred分配给了TaskId 3挤奶牛。

您应该在这里看到的是,每个任务可以分配少或多的分配或您想要的每个人。在此示例中,未为以西结(Ezekiel)分配任何任务,而为Alfred分配2。如果您有一个100人的任务,那么执行SELECT PersonId from Assignments WHERE TaskId=<whatever>;将产生100行,并分配了各种不同的人员。您可以在PersonId上进行WHERE查找分配给该人的所有任务。

如果要返回将ID替换为Names和任务的查询,那么您将学习如何联接表。

评论


您想要搜索以了解更多信息的关键字是“多对多关系”

– BlueRaja-Danny Pflughoeft
18-11-14在9:38



为了详细说明Thierrys的评论,您可能会认为您不需要标准化,因为我只需要X,并且存储ID列表非常简单,但是对于以后可能扩展的任何系统,您都会后悔没有对其进行标准化较早。始终规范化;唯一的问题是什么正常形式

– Jan Doggen
18-11-14在10:18



同意@Jan-根据我的更好判断,我允许我的团队在不久前采取设计捷径,存储JSON代替“不需要扩展”的内容。持续了六个月的FML。然后,我们的升级程序费劲地将JSON迁移到我们应该开始的方案中。我真的应该更了解。

–轨道轻赛
18-11-14在11:57



@Deduplicator:它只是一个花园式,自动递增的整数主键列的表示。很典型的东西。

–whatsisname
18年11月14日在20:21



@whatsisname在“人员或任务”表上,我同意您的看法。在桥表上,其唯一目的是代表两个已经具有代理键的表之间的多对多关系?没有充分的理由,我不会添加一个。这只是开销,因为它永远不会在查询或关系中使用。

– jpmc26
18-11-14在22:00



#2 楼

您在这里问两个问题。

首先,您询问是否可以将序列化的列表存储在列中。是的,很好。如果您的项目需要它。例如,目录页面的产品成分可能不希望您单独跟踪每种成分。

不幸的是,您的第二个问题描述了一种场景,在这种情况下,您应该选择一种更具关系性的方法。您将需要3张桌子。一个用于人员,一个用于任务,另一个维护将哪些任务分配给哪些人员的列表。最后一个将是垂直的,每人/任务组合一行,并带有用于主键,任务ID和人员ID的列。

评论


您引用的成分示例在表面上是正确的;但在这种情况下,它将是纯文本格式。从编程的意义上来说,它不是一个列表(除非您的意思是字符串是一个您显然不知道的字符列表)。 OP将其数据描述为“ ID列表”(甚至只是“ [..]列表”)意味着它们在某些时候将这些数据作为单独的对象进行处理。

–更
18-11-14在11:11



@Flater:但这是一个清单。您需要能够将其重新格式化为(各种)HTML列表,Markdown列表,JSON列表等,以确保项目在(各种)网页,纯文本文档,移动设备中正确显示应用...,而纯文本确实无法做到这一点。

–凯文
18-11-14在18:48



@Kevin如果这是您的目标,那么将成分存储在表中就可以轻松得多!更不用说,以后人们是否会……哦,我不知道,例如,希望得到推荐的替代品,或者像在寻找没有花生,面筋或动物蛋白的所有食谱那样愚蠢的东西。

–丹·布朗
18-11-14在20:49



@DanBron:YAGNI。现在,我们仅使用列表,因为它使UI逻辑更容易。如果在业务逻辑层中我们需要或将需要类似列表的行为,则应将其规范化为单独的表。表和联接不一定很昂贵,但是它们不是免费的,它们会带来有关元素顺序(“我们是否关心配料的顺序?”)和进一步规范化(“您要翻个3个鸡蛋”的问题)的问题。 into('eggs',3)?'Salt,toavour',那是('salt',NULL)吗?”)。

–凯文
18-11-14在20:54



@Kevin:YAGNI在这里是完全错误的。您自己认为必须以多种方式(HTML,markdown,JSON)转换列表,因此认为您需要列表的各个元素。除非数据存储和“列表处理”应用程序是两个独立开发的应用程序(并且请注意,单独的应用程序层=单独的应用程序),否则应始终创建数据库结构来以一种易于使用的格式存储数据-同时避免其他解析/转换逻辑。

–更
18-11-15在6:45



#3 楼

您所描述的被称为“多对多”关系,在您的情况下,介于PersonTask之间。通常使用第三个表(有时称为“链接”或“交叉引用”表)来实现。例如:

create table person (
    person_id integer primary key,
    ...
);

create table task (
    task_id integer primary key,
    ...
);

create table person_task_xref (
    person_id integer not null,
    task_id integer not null,
    primary key (person_id, task_id),
    foreign key (person_id) references person (person_id),
    foreign key (task_id) references task (task_id)
);


评论


如果您可能要执行按任务过滤的查询,则可能还需要先添加带有task_id的索引。

– jpmc26
18年11月16日在22:40

也称为桥接表。另外,希望我为没有身份列提供额外的好处,尽管我会建议在每个列上都有一个索引。

– jmoreno
18年11月18日在2:06

#4 楼


...永远不会(或几乎永远不会)在字段中存储ID或类似内容的列表


唯一的一次您可能会存储多个数据项在单个字段中,是指该字段仅曾用作单个实体,而从未被视为由那些较小的元素组成。一个示例可能是存储在BLOB字段中的图像。它由许多较小的元素(字节)组成,但是这些元素对数据库没有任何意义,只能一起使用(对于最终用户来说看起来很漂亮)。

由于定义上的“列表”由较小的元素(项目)组成,因此情况并非如此,您应该对数据进行规范化。


...如果将这些任务分别保存在“人”中,则必须有几十个虚拟的“任务ID”列...


不。在人员和任务之间的相交表(也称为弱实体)中将有几行。数据库真的很擅长处理很多行。在处理很多[重复]列时,它们实际上是很垃圾的。

whatsisname给出的清晰示例。

评论


当创建现实生活系统时,“永不言败”是一个很好的规则。

–l0b0
18年11月14日在21:31

在许多情况下,以规范化形式维护或检索列表的每个元素的成本可能远远超过将项目保留为Blob的成本,因为列表的每个项目都必须拥有与其关联的主项目的标识。除了实际数据外,还关联了它,并且它在列表中的位置。即使在代码可以从无需更新整个列表的情况下更新某些列表元素而受益的情况下,将所有内容存储为blob并在需要重写任何内容时重写所有内容可能会更便宜。

–超级猫
18年11月16日在21:29

#5 楼

在某些预先计算的字段中可能是合法的。

如果某些查询很昂贵,并且您决定使用数据库触发器自动更新的预先计算的字段,那么保留列内的列表。

例如,在UI中,您要使用网格视图显示此列表,其中每行可以在双击后打开完整的详细信息(带有完整的列表):

REGISTERED USER LIST
+------------------+----------------------------------------------------+
|Name              |Top 3 most visited tags                             |
+==================+====================================================+
|Peter             |Design, Fitness, Gifts                              |
+------------------+----------------------------------------------------+
|Lucy              |Fashion, Gifts, Lifestyle                           |
+------------------+----------------------------------------------------+


当客户访问新文章或预定任务时,您将通过触发器保持第二列的更新。

您甚至可以将此类字段用于搜索(作为普通文字)。

在这种情况下,保留列表是合法的。您只需要考虑可能超过最大字段长度的情况。


此外,如果您使用的是Microsoft Access,则提供的多值字段是另一个特殊用例。它们会自动在字段中处理您的列表。

但是您总是可以退回到其他答案中所示的标准规范化形式。


摘要:数据库的常规形式是理解数据建模重要方面所需的理论模型。但是,归一化当然不会考虑性能或检索数据的其他成本。这超出了该理论模型的范围。但是实际的实现通常需要存储列表或其他预先计算(和控制)的副本。

鉴于上述情况,在实际的实现中,我们宁愿查询依赖于完美范式和运行的查询20秒或等效查询依赖于0.08 s的预先计算的值?没有人喜欢他们的软件产品被指责缓慢。

评论


即使没有预先计算的内容,它也可能是合法的。我已经做了几次将数据正确存储的操作,但是出于性能方面的考虑,将一些缓存结果填充到主记录中非常有用。

–Loren Pechtel
18-11-15在4:10

@LorenPechtel –是的,谢谢,在使用术语“预先计算的”时,我还包括了在需要时存储缓存值的情况。在具有复杂依赖性的系统中,它们是保持性能正常的方法。如果使用足够的专业知识进行编程,这些值将是可靠的并且始终保持同步。我只是不想在答案中添加缓存的情况,以使答案简单而安全。无论如何,它都被否决了。 :)

– Miroxlav
18-11-15在9:52



@LorenPechtel实际上,那仍然是一个不好的原因...缓存数据应保存在中间缓存存储中,并且在缓存仍然有效的同时,该查询也绝不能命中主数据库。

–特斯拉
18年11月16日在20:18

@Tezra不,我是说有时候从辅助表中获取一条数据通常足以使将副本放入主记录中变得有意义。 (我做过的示例-员工表包括上次进入和最后一次离开。它们仅用于显示目的,任何实际计算都来自具有进/出时钟记录的表。)

–Loren Pechtel
18-11-17在2:21

#6 楼

给定两个表;我们将它们称为Person和Task,每个都有其自己的ID(PersonID,TaskID)...基本思想是创建第三个表以将它们绑定在一起。我们将此表称为PersonToTask。至少它应该具有自己的ID,以及其他两个ID。
因此,在为某人分配任务时;您将不再需要更新Person表,只需要在PersonToTaskTable中插入新行即可。
维护变得更加容易-只需根据TaskID删除一个任务就成为DELETE,无需再更新Person表及其关联的解析

CREATE TABLE dbo.PersonToTask (
    pttID INT IDENTITY(1,1) NOT NULL,
    PersonID INT NULL,
    TaskID   INT NULL
)

CREATE PROCEDURE dbo.Task_Assigned (@PersonID INT, @TaskID INT)
AS
BEGIN
    INSERT PersonToTask (PersonID, TaskID)
    VALUES (@PersonID, @TaskID)
END

CREATE PROCEDURE dbo.Task_Deleted (@TaskID INT)
AS
BEGIN
    DELETE PersonToTask  WHERE TaskID = @TaskID
    DELETE Task          WHERE TaskID = @TaskID
END


一个简单的报告或谁都分配给一个任务呢?

CREATE PROCEDURE dbo.Task_CurrentAssigned (@TaskID INT)
AS
BEGIN
    SELECT PersonName
    FROM   dbo.Person
    WHERE  PersonID IN (SELECT PersonID FROM dbo.PersonToTask WHERE TaskID = @TaskID)
END


/>您当然可以做更多的事情;如果为TaskAssigned和TaskCompleted添加了DateTime字段,则可以完成TimeReport。一切由您决定

#7 楼

如果您说您具有人类可读的主键,并且想要一个任务编号列表,而不必处理表结构的垂直性质,那么它可能会起作用。即,更容易阅读第一个表。

------------------------  
Employee Name | Task 
Jack          |  1,2,5
Jill          |  4,6,7
------------------------

------------------------  
Employee Name | Task 
Jack          |  1
Jack          |  2
Jack          |  5
Jill          |  4
Jill          |  6
Jill          |  7
------------------------


然后问题是:任务列表是否应按需存储或生成,这在很大程度上取决于诸如以下的要求:需要多长时间使用一次列表,需要多少准确的数据行,如何使用数据,等等。。。在此之后,应进行权衡用户体验并满足需求的分析。

例如,比较调用两行所需的时间与运行将生成两行的查询的时间进行比较。如果花费的时间很长,并且用户不需要最新列表(*预计每天少于1次更改),则可以将其存储。

或者,如果用户需要分配给他们的任务的历史记录,则也可以使用列表的存储方式。因此,这实际上取决于您在做什么,永不言败。

评论


正如您所说,这完全取决于如何检索数据。如果仅通过用户名查询该表,则“列表”字段就足够了。但是,如何查询这样的表以找出谁在处理#1234567任务并保持其性能呢?几乎每种“在字段中X的任何地方查找”字符串函数都会导致对/ Table Scan /的查询,从而使抓取速度变慢。对于正确规范化,正确索引的数据,这根本不会发生。

– Phill W.
18-11-15在15:52

#8 楼

您正在获取应该是另一个表的东西,将其旋转90度并将其拔成另一个表。

就像有一个订单表,其中有itemProdcode1,itemQuantity1,itemPrice1 ... itemProdcode37,itemQuantity37 ,itemPrice37。除了笨拙地以编程方式处理之外,您还可以保证明天有人要订购38件东西。

如果“列表”不是真正的列表,我只会按照您的方式进行操作,即它是一个整体,每个单独的订单项都未引用某个清晰且独立的实体。在这种情况下,只需将其填充为足够大的某种数据类型即可。

因此,订单是一个清单,物料清单是一个清单(或清单的清单,这对于实施“侧身”而言将是一场噩梦)。但是不是注释/评论和诗歌。

#9 楼

如果“不行”,那么每个Wordpress网站都在wp_usermeta中有一个列表,其中有wp_capabilities排成一行,dismissed_wp_pointers列表排成一行,等等,这是相当糟糕的。

实际上像这样的情况可能会更快,因为您几乎总是需要列表。但是众所周知,Wordpress并不是最佳实践的完美示例。