存储树数据的快速关系方法(例如文章的线程评论)
Posted
技术标签:
【中文标题】存储树数据的快速关系方法(例如文章的线程评论)【英文标题】:Fast Relational method of storing tree data (for instance threaded comments on articles) 【发布时间】:2010-10-25 04:13:53 【问题描述】:我有一个 cms,它根据文章存储 cmets。这些 cmets 既可以是螺纹的,也可以是非螺纹的。尽管从技术上讲,它们是相同的,只是在未线程化时将回复列留空。我的应用程序适用于 sqlLite、mysql 和 pgsql,所以我需要相当标准的 SQL。
我目前有一个评论表
comment_id
article_id
user_id
comment
timestamp
thread (this is the reply column)
我的问题是弄清楚如何最好地表示数据库中的线程化 cmets。也许在一个单独的表格中,它支持没有内容的树集和一个简单的表格来保存文本?也许它已经是这样了?也许是另一种方式?
如果 cmets 没有线程,我可以轻松地按时间戳排序。
如果它们是线程化的,我会这样排序
ORDER BY SUBSTRING(c.thread, 1, (LENGTH(c.thread) - 1))
从 ORDER BY 中可以看出,注释查询永远不会使用索引作为基于函数的索引,仅真正存在于 Oracle 中。帮助我快速创建评论页面。
【问题讨论】:
“最佳”实现取决于数据结构和读/写特性。要获得近乎全面的社区更新选项列表,以考虑最适合您的选项:***.com/questions/4048151 【参考方案1】:我知道答案有点晚了,但是对于树数据使用闭包表,这是正确的关系方式。 http://www.slideshare.net/billkarwin/models-for-hierarchical-data
它描述了4种方法:
Adjcency 列表(简单的父外键) 路径枚举(已接受答案中提到的 Drupal 策略) 嵌套集 闭包表(将祖先/后代事实存储在单独的关系 [表] 中,并带有可能的距离列)与其他选项相比,最后一个选项具有简单的 CRUD 操作的优点。成本是空间,在最坏的情况下,在数字树节点中是 O(n^2) 大小,但在实践中可能并没有那么糟糕。
【讨论】:
非常酷!闭包表看起来很有希望。原始答案可能应该提供来自资源的实际信息,而不仅仅是指向资源的链接。我进行了编辑以包含关键要点。【参考方案2】:您可以在邻接集模型和嵌套集模型之间进行选择。文章Managing Hierarchical Data in MySQL 做了一个很好的介绍。
有关理论讨论,请参阅 Celko 的 Trees and Hierarchies。
如果您的数据库支持窗口函数,则实现线程列表相当容易。您所需要的只是目标数据库表中的递归引用,例如:
create Tablename (
RecordID integer not null default 0 auto_increment,
ParentID integer default null references RecordID,
...
)
然后您可以使用递归公用表表达式来显示线程视图。 here 提供了一个示例。
【讨论】:
【参考方案3】:实际上,我只是自己做的!我使用嵌套集模型来表示关系数据库中的分层数据。
Managing Hierarchical Data in MySQL 对我来说是纯金。嵌套集是该文章中描述的第二种模型。
【讨论】:
嵌套集的糟糕之处在于,以任何方式修改树结构都是昂贵的【参考方案4】:我真的很喜欢Drupal 解决这个问题的方式。它为每个评论分配一个线程 ID。对于第一条评论,此 id 从 1 开始。如果对此评论添加回复,则会为其分配 ID 1.1
。回复评论1.1
的线程ID 为1.1.1
。注释 1.1
的兄弟姐妹被赋予线程 ID 1.2
。你明白了。添加评论时,只需一次查询即可轻松计算这些线程 ID。
当线程被渲染时,属于该线程的所有 cmets 都在一个查询中被获取,并按线程 ID 排序。这会按升序为您提供线程。此外,使用线程ID,您可以找到每个评论的嵌套级别,并相应地缩进。
1
1.1
1.1.1
1.2
1.2.1
有几个问题需要解决:
如果线程 id 的一个组成部分增长到 2 位,则按线程 id 排序将不会产生预期的顺序。一个简单的解决方案是确保线程 ID 的所有组件都用零填充以具有相同的宽度。 按线程 ID 降序排序不会产生预期的降序。Drupal 使用称为 vancode 的编号系统以更复杂的方式解决了第一个问题。至于第二个问题,它是通过在按降序排序时在线程ID后附加一个反斜杠(其ASCII码高于数字)来解决的。您可以通过查看comments module 的源代码找到有关此实现的更多详细信息(请参阅函数comment_get_thread 之前的大注释)。
【讨论】:
【参考方案5】:不幸的是,纯 SQL 方法很慢。
@Marc W
提出的 NESTED SETS
非常优雅,但如果您的树枝达到范围,则可能需要更新整棵树,这可能会很慢。
请参阅我的博客中的这篇文章,了解如何在MySQL
中快速做到这一点:
Oracle
的 CONNECT BY
您需要创建一个函数:
CREATE FUNCTION hierarchy_connect_by_parent_eq_prior_id(value INT) RETURNS INT
NOT DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE _id INT;
DECLARE _parent INT;
DECLARE _next INT;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET @id = NULL;
SET _parent = @id;
SET _id = -1;
IF @id IS NULL THEN
RETURN NULL;
END IF;
LOOP
SELECT MIN(id)
INTO @id
FROM t_hierarchy
WHERE parent = _parent
AND id > _id;
IF @id IS NOT NULL OR _parent = @start_with THEN
SET @level = @level + 1;
RETURN @id;
END IF;
SET @level := @level - 1;
SELECT id, parent
INTO _id, _parent
FROM t_hierarchy
WHERE id = _parent;
END LOOP;
END
并在这样的查询中使用它:
SELECT hi.*
FROM (
SELECT hierarchy_connect_by_parent_eq_prior_id(id) AS id, @level AS level
FROM (
SELECT @start_with := 0,
@id := @start_with,
@level := 0
) vars, t_hierarchy
WHERE @id IS NOT NULL
) ho
JOIN t_hierarchy hi
ON hi.id = ho.id
这当然是 MySQL
特定的,但它真的很快。
如果您希望它在 PostgreSQL
和 MySQL
之间可移植,您可以使用 PostgreSQL
的 contrib for CONNECT BY
并将查询包装到两个系统同名的存储过程中。
【讨论】:
【参考方案6】:其实还是要在读写之间取得平衡。
如果您可以在每次插入时更新一堆行,那么嵌套集(或等效的)将为您提供轻松、快速的读取。
除此之外,父级上的简单 FK 将为您提供超简单的插入,但很可能是检索的噩梦。
我想我会使用嵌套集,但要注意预期的数据量和使用模式(为每个插入更新两个索引列(用于左右信息)上的几行,也许很多行可能在某些时候成为问题)。
【讨论】:
以上是关于存储树数据的快速关系方法(例如文章的线程评论)的主要内容,如果未能解决你的问题,请参考以下文章