我怎样才能避免循环引用的情况
How can I avoid a circular reference situation
关于 SQL tables、循环引用和外键的建议。
我是SQL的新手(大约一个月左右),所以请原谅任何后续不幸的天真。
我正在做一个关于故事的项目,用户可以开始一个故事,另一个用户可以添加到那个故事中。
目前,我的两个主要 table 是故事和段落。
故事由段落组成。段落只是一大块文本。
故事架构如下所示:
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),以便我可以轻松访问第一段和最后一段,但这会造成循环引用情况,因为故事将引用段落,反之亦然.这个可以吗?当我开始处理大量数据和查询时,它会变得更多吗?
我想到了一个解决方案,我有另一个 table:
故事段落分配。架构:
ID primary key
story REFERENCES stories(stid),
headpara REFERENCES paragraph(prid),
lastpara REFERENCES paragraph(prid)
出于某种原因,我不相信这个解决方案。这对我来说是多余的。这不是多对多的情况。但是段落需要引用故事,我需要能够访问故事的第一段和最后一段。
另一个可能的解决方案是在段落模式中有两个布尔列,称为头和尾,这样第一段就可以用
调用
WHERE story == stID AND head == True.
想法?当我的段落 table 非常大时,这个解决方案似乎会成为一个问题。非常感谢。
你可以用任何一种方法来解决这个问题。如果您知道首段和末段非常重要,那么在故事中引用它们就可以了。
在任何一种情况下,维护关系完整性都存在一些挑战。据推测,您希望开头和最后一段位于同一个故事中。为此,您需要一个复合键。并且您需要使用单独的 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 的名称应完整拼写(
paragraphId
或 paragraph_id
)或简单地拼写 id
。如果您使用 prid
,可能会与另一个 table. 混淆
- 并非所有数据库都支持过滤的唯一索引。在这些情况下,您需要使用触发器或其他机制。
我一开始就不愿意 table 单独的段落。
当作家编辑他们的作品时,段落对他们来说不是某种硬性划分单位。当我修改我的写作时,在段落之间移动句子,重新排列段落,合并段落,分隔段落,甚至删除整个段落是经常发生的事情。使用您设置的结构,您将很难实施这些类型的更新。这使您选择的划分有问题,而您面临的问题只是此结构相当不自然的另一个方面。
如果您需要支持编辑
如果您需要支持编辑故事,那么我可能倾向于查看 non-relational 数据库(例如 Couch 或 Mongo)。
如果我坚持使用 PostgreSQL,我可能会首先尝试包含整个故事的一个专栏。 The normal text types in PostgreSQL handles up to about 1 GB of text. This is probably big enough. Assuming each character is two bytes (an over-estimation for English with UTF-8) and each word is 10 characters and 1 space (again, an over-estimation), the column can hold stories of over 48 million 字。如果段落包含格式标记,这个数字当然会下降。
但这 运行 会带来其他问题:来回移动那么多文本可能会很慢,并且在更新时维护索引(可能是全文索引)变得昂贵。索引问题可能会通过 Lucene or Solr 这样的技术解决;来回打乱大量文本的问题更难。如果你要处理的故事比较小,普通的全文机制可能就足够了。
但最重要的是,如果故事可以编辑,按段落分解故事会使构建软件更加困难,您应该重新考虑架构。
如果您只支持读取和批量加载
但是,如果编辑不是您需要支持的功能,作为一种优化,您可以严格按段落拆分故事。在这种情况下,您将批量插入故事的所有段落,从而允许您在导入时将它们分成单独的行。 "Editing" 将包括删除 所有 段落并插入一组新段落。
在这种情况下,"linked list" 结构不再有意义。链接列表优化 edits 到一个列表(插入和删除是 O(1)),但是如果按段落分解一个故事是可行的(如上所述),那么编辑该列表是您不再需要优化的操作。相反,您将优化 reads。这可能需要某种随机访问。例如,当用户滚动浏览故事时,您可能一次阅读 5 个段落,这将要求您能够从中间某处的任意段落开始阅读。
这提出了一种完全不同且更自然的组织方式 table:在段落 table 上放置一个表示 位置 的列。该列的值可以在批量插入段落时生成。这使得按位置获取变得微不足道。例如,要在用户滚动时加载下一段,您只需跟踪为他们获取的最后一段的位置(如第 29 段),然后加载接下来的五个 (WHERE position >= 30 and position <= 34
).
通过这种安排,您的段落 table 可能如下所示:
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)
虽然没有了链表结构,查询最后一段可能就没有意义了。
链表和关系数据库
请注意,无论哪种方式,链表结构都会消失。这是有道理的。关系数据库针对随机访问进行了优化,而链表的顺序访问 运行s 反对谷物。如果您确实需要链表样式访问,关系数据库很可能不适合您的数据。图数据库非常适合链表样式的访问:它们根据节点和它们之间的边来工作。 (请注意,这不是特别常见。)
关于 SQL tables、循环引用和外键的建议。
我是SQL的新手(大约一个月左右),所以请原谅任何后续不幸的天真。 我正在做一个关于故事的项目,用户可以开始一个故事,另一个用户可以添加到那个故事中。 目前,我的两个主要 table 是故事和段落。 故事由段落组成。段落只是一大块文本。 故事架构如下所示:
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),以便我可以轻松访问第一段和最后一段,但这会造成循环引用情况,因为故事将引用段落,反之亦然.这个可以吗?当我开始处理大量数据和查询时,它会变得更多吗?
我想到了一个解决方案,我有另一个 table: 故事段落分配。架构:
ID primary key
story REFERENCES stories(stid),
headpara REFERENCES paragraph(prid),
lastpara REFERENCES paragraph(prid)
出于某种原因,我不相信这个解决方案。这对我来说是多余的。这不是多对多的情况。但是段落需要引用故事,我需要能够访问故事的第一段和最后一段。
另一个可能的解决方案是在段落模式中有两个布尔列,称为头和尾,这样第一段就可以用
调用WHERE story == stID AND head == True.
想法?当我的段落 table 非常大时,这个解决方案似乎会成为一个问题。非常感谢。
你可以用任何一种方法来解决这个问题。如果您知道首段和末段非常重要,那么在故事中引用它们就可以了。
在任何一种情况下,维护关系完整性都存在一些挑战。据推测,您希望开头和最后一段位于同一个故事中。为此,您需要一个复合键。并且您需要使用单独的 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 的名称应完整拼写(
paragraphId
或paragraph_id
)或简单地拼写id
。如果您使用prid
,可能会与另一个 table. 混淆
- 并非所有数据库都支持过滤的唯一索引。在这些情况下,您需要使用触发器或其他机制。
我一开始就不愿意 table 单独的段落。
当作家编辑他们的作品时,段落对他们来说不是某种硬性划分单位。当我修改我的写作时,在段落之间移动句子,重新排列段落,合并段落,分隔段落,甚至删除整个段落是经常发生的事情。使用您设置的结构,您将很难实施这些类型的更新。这使您选择的划分有问题,而您面临的问题只是此结构相当不自然的另一个方面。
如果您需要支持编辑
如果您需要支持编辑故事,那么我可能倾向于查看 non-relational 数据库(例如 Couch 或 Mongo)。
如果我坚持使用 PostgreSQL,我可能会首先尝试包含整个故事的一个专栏。 The normal text types in PostgreSQL handles up to about 1 GB of text. This is probably big enough. Assuming each character is two bytes (an over-estimation for English with UTF-8) and each word is 10 characters and 1 space (again, an over-estimation), the column can hold stories of over 48 million 字。如果段落包含格式标记,这个数字当然会下降。
但这 运行 会带来其他问题:来回移动那么多文本可能会很慢,并且在更新时维护索引(可能是全文索引)变得昂贵。索引问题可能会通过 Lucene or Solr 这样的技术解决;来回打乱大量文本的问题更难。如果你要处理的故事比较小,普通的全文机制可能就足够了。
但最重要的是,如果故事可以编辑,按段落分解故事会使构建软件更加困难,您应该重新考虑架构。
如果您只支持读取和批量加载
但是,如果编辑不是您需要支持的功能,作为一种优化,您可以严格按段落拆分故事。在这种情况下,您将批量插入故事的所有段落,从而允许您在导入时将它们分成单独的行。 "Editing" 将包括删除 所有 段落并插入一组新段落。
在这种情况下,"linked list" 结构不再有意义。链接列表优化 edits 到一个列表(插入和删除是 O(1)),但是如果按段落分解一个故事是可行的(如上所述),那么编辑该列表是您不再需要优化的操作。相反,您将优化 reads。这可能需要某种随机访问。例如,当用户滚动浏览故事时,您可能一次阅读 5 个段落,这将要求您能够从中间某处的任意段落开始阅读。
这提出了一种完全不同且更自然的组织方式 table:在段落 table 上放置一个表示 位置 的列。该列的值可以在批量插入段落时生成。这使得按位置获取变得微不足道。例如,要在用户滚动时加载下一段,您只需跟踪为他们获取的最后一段的位置(如第 29 段),然后加载接下来的五个 (WHERE position >= 30 and position <= 34
).
通过这种安排,您的段落 table 可能如下所示:
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)
虽然没有了链表结构,查询最后一段可能就没有意义了。
链表和关系数据库
请注意,无论哪种方式,链表结构都会消失。这是有道理的。关系数据库针对随机访问进行了优化,而链表的顺序访问 运行s 反对谷物。如果您确实需要链表样式访问,关系数据库很可能不适合您的数据。图数据库非常适合链表样式的访问:它们根据节点和它们之间的边来工作。 (请注意,这不是特别常见。)