在关系数据库中使用列表可以吗?


94

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

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

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

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

对于这样的问题,是否可以保存采用一种形式或另一种形式的ID列表,或者我只是不考虑另一种可以在不违反原则的情况下实现的方式?


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

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

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

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

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

Answers:


249

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

您要做的不是向人员或任务表添加有关分配的信息,而是向该表添加具有相关关系的分配信息的新表。

例如,您具有下表:

人数:

+---+----------+
| ID | 姓名|
+ ==== + ========== +
| 1 | 阿尔弗雷德|
| 2 | 杰比迪亚|
| 3 | 雅各布|
| 4 | 以西结书
+---+----------+

任务:

+---+-------------
| ID | 姓名|
+ ==== + ================== +
| 1 | 喂鸡
| 2 | 犁|
| 3 | 奶牛|
| 4 | 养一个谷仓|
+---+-------------

然后,您将使用赋值创建第三个表。该表将模型化人员与任务之间的关系:

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

然后,我们将具有外键约束,以便数据库将强制PersonId和TaskId必须是那些外来项的有效ID。第一行,我们可以看到PersonId is 1,所以阿尔弗雷德,被分配到TaskId 3挤奶

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

如果您想返回用名称和任务替换ID的查询,那么您将学习如何联接表。


86
您想要搜索以了解更多信息的关键字是“ 多对多关系
BlueRaja-Danny Pflughoeft

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

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

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

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

35

您在这里问两个问题。

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

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


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

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

12
@Kevin如果这是您的目标,那么将成分存储在表格中就可以轻松而轻松地实现!更不用说,以后人们是否会……哦,我不知道,是想寻找推荐的替代品,还是像傻瓜般地寻找所有没有花生,面筋或动物蛋白的食谱 ...
丹布朗

10
@丹布朗:YAGNI。现在,我们仅使用列表,因为它使UI逻辑更容易。如果我们需要还是会需要在业务逻辑层列表类似的行为,那么它应该被归到一个单独的表。表和联接不一定很昂贵,但是它们不是免费的,它们会带来有关元素顺序(“我们是否关心成分的顺序?”)和进一步规范化(“您要翻个3个鸡蛋”的问题)的问题。 into('eggs',3)?'Salt,toavour',那('salt',NULL)呢?”)。
凯文

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

22

您所描述的是所谓的“多对多”关系,在您Person和之间Task。通常使用第三个表(有时称为“链接”或“交叉引用”表)来实现。例如:

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)
);

2
task_id如果您正在执行按任务过滤的查询,则可能还需要先添加索引。
jpmc26 '18

1
也称为桥接表。另外,希望我能为您提供一个额外的优势,因为它没有身份列,尽管我会建议在每个列上都有一个索引。
jmoreno

13

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

唯一可能在一个字段中存储多个数据项的时间是该字段仅用作单个实体,并且永远不认为该字段由那些较小的元素组成。一个示例可能是存储在BLOB字段中的图像。它由很多较小的元素(字节)组成,但是这些元素对数据库没有任何意义,只能一起使用(对于最终用户来说看起来很漂亮)。

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

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

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

whatsisname给出的很好的清晰示例。


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

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

4

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

如果您的某些查询价格昂贵,并且您决定使用数据库触发器自动更新预先计算的字段,那么将列表保留在列中可能是合法的。

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

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

当客户访问新文章或计划任务时,您将通过触发器来更新第二列。

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

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


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

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


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

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


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

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

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

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

0

给定两个表;我们将它们称为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。这一切都取决于你


0

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

------------------------  
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.

0

您正在拿应该是另一张桌子的东西,将它旋转90度,然后将它拔成另一张桌子。

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

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

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


0

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

实际上,在这种情况下,速度可能会更好,因为您几乎总是需要列表。但是,众所周知,Wordpress并不是最佳实践的完美范例。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.