Managing hierarchies in SQL: MPTT/nested sets vs adjacency lists vs storing paths
有一段时间我一直在努力解决如何最好地处理SQL中的层次结构。由于邻接列表的限制和MPTT /嵌套集的复杂性而感到沮丧,我开始考虑简单地存储密钥路径,作为一个简单的node_key/node_key/...字符串。我决定编译这三种技术的优点和缺点:
创建/删除/移动节点所需的呼叫数:
-
邻接= 1
-
MPTT = 3
-
Path = 1(用包含该路径的所有节点上的新节点路径替换旧节点路径)
获取树所需的调用次数:
获取节点/祖先路径所需的调用次数:
获取子节点数所需的调用次数:
-
邻接= [子级数]
-
MPTT = 0(可以从右/左值计算)
-
路径= 1
获取节点深度所需的调用次数:
需要DB字段:
-
邻接= 1(父)
-
MPTT = 3(父,右,左)
-
路径= 1(路径)
结论
除了一个用例之外,存储的路径技术使用与每个用例中的其他技术相同或更少的调用。通过这种分析,存储路径是明显的赢家。更不用说,它实现起来要简单得多,人类可读等等。
所以问题是,不应该将存储路径视为比MPTT更强大的技术吗?为什么存储路径不是更常用的技术,为什么不在给定实例中使用MPTT?
另外,如果您认为此分析不完整,请告诉我们。
更新:
这里至少有两件事MPTT可以开箱即用,存储的路径解决方案不会:
允许计算每个节点的子节点数,而无需任何其他查询(如上所述)。
在给定级别的节点上强加订单。其他解决方案是无序的。
-
实际上,在邻接模型中需要[子级别数]的调用是不正确的。 您可以执行以下操作:SELECT * FROM adjacency a /* for($number_of_levels_required.. $prevname = $level > 1 ? 'a'+$level-1 : 'a'; */ INNER JOIN adjacency a/*$level*/ ON a/*$level*/.parent_id = /*$prevname*/.id /* endfor*/
-
你如何质疑一个孩子& 所有后代分别在Path中。 我认为对于Path的最大缺点是referential integrity正如Bill Karwin在他的回答中提到的那样。
-
@buffer -"如何查询孩子和所有后代"? 这是使用路径最简单的部分:SELECT * FROM items WHERE path LIKE 'my/child/path%';。 是的,参照完整性受到影响,但权衡是更有效的查询。
您可能还会考虑我在回答中所描述的Closure Table设计什么是将平台解析成树的最有效/优雅的方法?
创建/删除/移动节点所需的调用:
获得树所需的电话:
获取节点/祖先路径所需的调用:
获取子节点数所需的调用:
获取节点深度所需的调用:
需要DB字段:
-
Adjancency = 1个字段/行
-
Path = 1个字段/行
-
MPTT = 2或3个字段/行
-
Closure =额外表格中的2或3个字段。该表的最坏情况为O(n ^ 2)行,但远少于大多数实际情况。
还有其他一些注意事项:
支持无限深度:
支持参照完整性:
-
邻接=是
-
MPTT =没有
-
路径=没有
-
关闭=是的
我还在我的演示文稿中介绍了Closure Table,其中包括SQL和PHP的分层数据模型,以及我的书,SQL Antipatterns:避免数据库编程的陷阱。
-
为什么路径不支持无限深度?
-
比尔 - 谢谢,从未听说过关闭表 - 看起来非常有趣的东西,我会调查。有一点 - 为什么你会说Path技术不支持无限深度 - 你是说它是字段中允许的字符串长度的函数吗?
-
是的,路径设计的最大深度受用于存储路径的字符串长度的限制。因此,例如,如果您的节点标识符是9位数字,则VARCHAR(255)可以存储深度为25的路径(假设您使用单字符分隔符)。
-
@BillKarwin - 他可以使用varchar(max),抽象地没有限制(IMO),我们可以说其他的受到磁盘大小的限制并且不是无限制的。
-
但我承认其他设计的深度也有限,至少在用于节点id或MPTT左/右字段的数据类型的有限范围内。和磁盘空间一样,正如你所说。尽管如此,路径长度的硬限制是其他设计没有的约束。
-
比尔,你建议只记录"直接"的亲子关系,还是建议记录整个关闭?前者在查询时需要递归SQL(不是最容易编写的东西,也许是大多数引擎 - 便宜的 - 只是不支持它),而后者意味着一个巨大的完整性执行问题。
-
唯一没有完整性脆弱性的设计是Adjacency List("直接父级"设计)。这取决于您使用的RDBMS品牌。如果使用Microsoft,Oracle 11,DB2,PostgreSQL或Apache Derby,则可以运行递归CTE查询。如果您运行MySQL,SQLite,Informix,Firebird等,则不支持递归查询。 MPTT,Path Enumeration和Closure都有一些完整性执行问题。
-
闭包表的一个重要问题是它需要在单独的表中使用O(n^2)行
-
@dnozay,是的,但仅限于最糟糕的情况。实际的例子会少得多。我有一个约500k节点的层次结构,在闭包表中只需要约4.6m的行。这远不及500k平方。
-
不同的例子:500k元素的链表大约需要500k * (500k + 1) / 2,大约需要1250亿行; 500k平方的一半。
-
@dnozay,是的,但我说实际的例子。如果要对500k元素的链表进行建模,则传递闭包可能不是正确的数据结构。每个解决方案都有利弊。
你得出的结论是,它忽略了使用树木时涉及的大多数问题。
通过将技术的有效性降低到"调用次数",您可以有效地忽略所有理解数据结构和算法试图解决的问题;也就是说,执行速度最快,内存和资源占用量低。
SQL服务器的"调用次数"似乎是一个很好的度量标准("少看代码"),但如果结果是一个永远不会完成,运行缓慢或占用太多空间的程序,那么它就是实际上是一个无用的指标。
通过存储每个节点的路径,您不会创建树数据结构。相反,您正在创建一个列表。树被设计为优化的任何操作都将丢失。
使用小日期集可能很难看到(在很多小树的情况下,列表更好),尝试一些大小为500,1000,10k的数据集的示例 - 您将很快看到为什么存储整个路径不是一个好主意。
-
@Hogan-你是什么意思"你会很快看到为什么存储整条路径不是一个好主意"?我会看到什么?并且"树的任何优化设计都会丢失。"那是什么意思?这些是模糊的陈述 - 你能举例吗?
-
@Yarin - 3岁的帖子...我会尝试,快速回顾:存储整个路径会产生巨大的存储需求,以指数方式增加存储大小。在存储方面,树结构允许您通过树的节点分布数据,因此仅存储一次通过树的路径上的排列的数据。
-
@ Hogan-假设您的意思与邻接列表有关(MPTT和闭包技术都需要额外的行,因此需要更大的整体存储要求)。不过,我没有看到巨大的增长问题,它只是一个字符串字段而不是整数字段。即使您的深度级别为20,您仍然可以在VARCHAR(255)内轻松适应所有树路径,最多可达100亿行。
-
@Yarin - 我看到实现这样一棵树的最后一个系统开始"死",深度大约为6行,不超过50,000行 - 当然程序员不是很好而且不是我的项目所以它可能刚刚无能(我无法深入分析 - 双关语)。如果你的方法有效,那很好。