如何避免循环引用情况

时间:2018-03-11 14:26:00

标签: sql database postgresql circular-dependency

关于SQL表,循环引用和外键的建议。

我对SQL很新(大约一个月左右),所以请原谅任何后来不幸的天真。 我在一个关于故事的项目上工作,用户可以开始讲故事,另一个用户可以添加到故事中。 目前,我的两个主要表格是故事和段落。 故事由段落组成。段落只是一大块文字。 故事模式如下所示:

stid varchar not null primary key,
title text not null,
description text,
created_at timestamptz DEFAULT now()

段落架构如下所示:

prid bigint not null primary key,
story varchar not null REFERENCES stories(stid),
maintext text,
writer text not null REFERENCES users(username),
parentpr bigint, //the previous paragraph
childpr bigint, //the next paragraph
created_at timestamptz DEFAULT now()

我正在考虑在故事模式中添加headpara和lastpara列(使用ALTER),因此我可以轻松访问第一段和最后一段,但这会创建循环引用情况,因为故事将引用段落,反之亦然。这个可以吗?当我开始处理大量数据和查询时,它会变得更多吗?

我想到了一个解决方案,我有另一张桌子: 故事段落分配。架构:

ID primary key
story REFERENCES stories(stid),
headpara REFERENCES paragraph(prid),
lastpara REFERENCES paragraph(prid)

出于某种原因,我不相信这个解决方案。对我来说感觉多余。这不是一个多对多的情况。但段落需要引用故事,我需要能够访问故事的第一段和最后一段。

另一个可能的解决方案是在段落模式中有两个布尔列,称为head和tail,因此可以使用

调用第一段
WHERE story == stID AND head == True. 

思考?当我的段表非常大时,这个解决方案似乎是一个问题。非常感谢提前。

2 个答案:

答案 0 :(得分:2)

我实际上不愿意首先找到一个单独的段落表。

当作家编辑他们的作品时,段落不是他们的某种硬分割单位。当我修改我的写作时,在段落之间移动句子,重新排列段落,合并段落,分隔段落,甚至删除整个段落都是经常发生的事情。使用您已设置的结构实现这些类型的更新将非常困难。这使你所选择的部门有问题,而你所面临的问题只是这种结构相当不自然的另一个方面。

如果您需要支持编辑

如果您需要支持编辑故事,那么我可能倾向于查看非关系数据库(例如,Couch或Mongo)。

如果我被PostgreSQL困住了,我可能会先试用一个包含整个故事的专栏。 PostgreSQL中的The normal text types最多可处理大约1 GB的文本。这可能足够大了。假设每个字符是两个字节(对UTF-8的英语过高估计)并且每个单词是10个字符和1个空格(同样是over-estimation),该列可以包含超过48 million的故事话。如果段落包含格式标记,那么该数字当然会下降。

但是这会遇到其他问题:来回移动大量文本可能会很慢并且维护索引更新(可能是全文)会变得很昂贵。索引问题可能会使用LuceneSolr等技术解决;来回移动大量文本的问题更难。如果您必须处理的故事相对较小,那么正常的全文机制可能对您来说足够了。

但最重要的是,如果可以编辑故事,那么逐段打破故事会使得构建软件更加困难,你应该重新考虑这个体系结构。

如果您仅支持读取和批量加载

但是,如果编辑不是您需要支持的功能,那么您可以通过段落严格地将故事分解为优化。在这种情况下,您将批量插入所有故事的段落,允许您在导入时将它们分成单独的行。 "编辑"将包括删除所有段落并插入一组新段落。

在这种情况下,"链接列表"结构停止了很多意义。链接列表优化编辑到列表(插入和删除是O(1)),但如果按段落分解故事是可行的(如上所述),那么列表中的编辑是您不再需要优化的操作。相反,您要优化读取。这可能需要某种随机访问。例如,当用户滚动浏览故事时,您可能一次阅读5个段落,这将要求您能够在中间某处的任意段落开始阅读。

这表明了一种完全不同且更自然的组织表格的方法:在段落表格上放置一个代表位置的列。批量插入段落时,可以生成此列的值。这使得按位置获取是微不足道的。例如,要在用户滚动时加载下一个段落,您只需跟踪为其获取的最后一个段落的位置(如第29段),然后加载下五个段落(WHERE position >= 30 and position <= 34)。

通过这种安排,您的段落表可能如下所示:

CREATE TABLE paragraph (
    paragraph_id SERIAL PRIMARY KEY,
    story_id INTEGER NOT NULL REFERENCES stories (story_id),
    position INTEGER NOT NULL,
    -- Other columns
    created_at TIMESTAMPTZ DEFAULT now()
)

这确实留下了一个问题,这实际上是你原来的问题。如何使用此设置获取 last 段落?这其实并不是很难:

SELECT *
FROM paragraph
WHERE story_id = 30
ORDER BY position DESC
LIMIT 1

这里的关键是以相反的顺序ORDER BY位置,然后使用LIMIT告诉DB您只需要排序后的第一行。这是一个非常有效的查询。如果经常运行它,在故事的ID和优化此查询的位置之间创建组合索引可能是有意义的:

CREATE INDEX ON paragraphs (story_id, position)

虽然链表结构消失了,查询最后一段可能没有意义了。

链接列表和关系数据库

请注意,无论哪种方式,链表结构都会消失。这是有道理的。关系数据库针对随机访问进行了优化,链接列表的顺序访问针对粒度运行。如果您确实需要链接列表样式访问,那么关系数据库很可能不适合您的数据。图表DB非常适合链接列表样式访问:它们根据节点和它们之间的边缘工作。 (请注意,这并不常见。)

答案 1 :(得分:1)

您可以采用任何一种方式解决问题。如果您知道头段和最后一段非常重要,那么在故事中引用它们就可以了。

在任何一种情况下,维护关系完整性都存在一些挑战。据推测,你希望头部和最后几段都在同一个故事中。为此,您需要一个复合键。您需要使用单独的alter table语句添加密钥。所以:

alter table paragraph add constraint unq_paragraph_story_prid unique (story, prid);

alter table stories add constraint fk_stories_headpara
    foreign key (stid, headpara) references paragraph(story, prid);

alter table stories add constraint fk_stories_lastpara
    foreign key (stid, lastpara) references paragraph(story, prid);

同样,如果使用标志,则需要确保每种类型集中只有一个标志。更新时可能会有点痛苦。这种约束看起来像:

create unique index unq_paragraph_headpara paragraph(story) where head = 1;

create unique index unq_paragraph_lastpara paragraph(story) where last = 1;

关于命名和其他事项的说明:

  • id s应为数字,如果可以的话。这简化了外键引用。
  • ID的名称应完整拼写(paragraphIdparagraph_id)或id。如果您使用prid,则可能会与另一个表格混淆。
  • 并非所有数据库都支持过滤的唯一索引。在这些情况下,您需要使用触发器或其他机制。