在关系数据库中存储分层数据有哪些选项?
良好的概述
一般来说,您要在快速读取时间(例如嵌套集)或快速写入时间(邻接列表)之间做出决定。通常,您最终会得到最适合您需求的以下选项组合。以下提供了一些深入阅读:
- 再一个嵌套间隔与邻接列表比较:我发现的邻接列表、物化路径、嵌套集和嵌套间隔的最佳比较。
- 分层数据模型:幻灯片,对权衡和示例用法进行了很好的解释
- < a href="http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/" rel="noreferrer">在 MySQL 中表示层次结构:非常好的嵌套集概述,特别是
- RDBMS 中的分层数据:我见过的最全面且组织良好的链接集见过,但没有太多解释
选项
我知道的选项和一般功能:
- 邻接列表:
- 列:ID、ParentID
- 易于实现。
- 便宜的节点移动、插入和删除。
- 寻找等级、血统和等级的成本很高。后代,路径
- 在支持它们的数据库中通过 通用表表达式 避免 N+1
- 列:左、右
- 便宜的祖先,后代
- 非常昂贵
O(n/2)
由于易失性编码而移动、插入、删除
- 桥接表(又名闭包表 /w 触发器)
- 使用带有祖先、后代、深度(可选)的单独联接表
- 便宜的祖先和后代
- 写入成本
O(log n)
(子树的大小)用于插入、更新、删除 - 规范化编码:有利于 RDBMS 统计和删除连接中的查询规划器
- 每个节点需要多行
- 列:谱系(例如 /parent/child/grandchild/etc...)
- 通过前缀查询廉价后代(例如
LEFT(lineage, #) = '/enumerated/path'
) - 写入成本
O(log n)
(子树的大小)用于插入、更新、删除 - 非关系:依赖于数组数据类型或序列化字符串格式
- 与嵌套集类似,但使用实数/浮点/小数,以便编码不是易失性的(廉价的移动/插入/删除
- )实数/浮点/十进制表示/精度问题
- 矩阵编码变体 添加祖先编码(物化路径)“免费”,但增加了线性代数的复杂性。
- 修改后的邻接列表,为每个记录添加级别和等级(例如排序)列。
- 迭代/分页成本低
- 移动和删除成本
- 高 良好用途:线程讨论 - 论坛/博客评论
- 列:一列对于每个谱系级别,指的是直到根为止的所有父级,从项目级别向下的级别设置为 NULL
- 便宜的祖先、后代、级别
- 便宜的叶子插入、删除、移动
- 昂贵的内部节点的插入、删除、移动
- 层次结构深度的硬性限制
数据库特定说明
MySQL/MariaDB
使用邻接列表的会话变量- 在 MySQL 8.0 或 MariaDB 10.2 中使用 CTE
Oracle
- 使用 CONNECT BY 遍历邻接列表
PostgreSQL
- ltree 数据类型 用于物化路径
SQL Server
- 总体摘要
- 2008 年优惠HierarchyId 数据类型似乎有助于沿袭列方法并扩展可表示的深度。
Good Overviews
Generally speaking, you're making a decision between fast read times (for example, nested set) or fast write times (adjacency list). Usually, you end up with a combination of the options below that best fit your needs. The following provides some in-depth reading:
- One more Nested Intervals vs. Adjacency List comparison: the best comparison of Adjacency List, Materialized Path, Nested Set, and Nested Interval I've found.
- Models for hierarchical data: slides with good explanations of tradeoffs and example usage
- Representing hierarchies in MySQL: very good overview of Nested Set in particular
- Hierarchical data in RDBMSs: a most comprehensive and well-organized set of links I've seen, but not much in the way of explanation
Options
Ones I am aware of and general features:
- Columns: ID, ParentID
- Easy to implement.
- Cheap node moves, inserts, and deletes.
- Expensive to find the level, ancestry & descendants, path
- Avoid N+1 via Common Table Expressions in databases that support them
- Columns: Left, Right
- Cheap ancestry, descendants
- Very expensive
O(n/2)
moves, inserts, deletes due to volatile encoding
- Bridge Table (a.k.a. Closure Table /w triggers)
- Uses separate join table with ancestor, descendant, depth (optional)
- Cheap ancestry and descendants
- Writes costs
O(log n)
(size of the subtree) for insert, updates, deletes - Normalized encoding: good for RDBMS statistics & query planner in joins
- Requires multiple rows per node
- Lineage Column (a.k.a. Materialized Path, Path Enumeration)
- Column: lineage (e.g. /parent/child/grandchild/etc...)
- Cheap descendants via prefix query (e.g.
LEFT(lineage, #) = '/enumerated/path'
) - Writes costs
O(log n)
(size of the subtree) for insert, updates, deletes - Non-relational: relies on Array datatype or serialized string format
- Like nested set, but with real/float/decimal so that the encoding isn't volatile (inexpensive move/insert/delete)
- Has real/float/decimal representation/precision issues
- Matrix encoding variant adds ancestor encoding (materialized path) for "free", but with the added trickiness of linear algebra.
- A modified Adjacency List that adds a Level and Rank (e.g. ordering) column to each record.
- Cheap to iterate/paginate over
- Expensive move and delete
- Good Use: threaded discussion - forums / blog comments
- Columns: one for each lineage level, refers to all the parents up to the root, levels down from the item's level are set to NULL
- Cheap ancestors, descendants, level
- Cheap insert, delete, move of the leaves
- Expensive insert, delete, move of the internal nodes
- Hard limit to how deep the hierarchy can be
Database Specific Notes
MySQL/MariaDB
Use session variables for Adjacency List- Use CTEs in MySQL 8.0 or MariaDB 10.2
Oracle
- Use CONNECT BY to traverse Adjacency Lists
PostgreSQL
- ltree datatype for Materialized Path
SQL Server
- General summary
- 2008 offers HierarchyId data type that appears to help with the Lineage Column approach and expand the depth that can be represented.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
我最喜欢的答案是本主题第一句话所建议的。使用邻接列表来维护层次结构并使用嵌套集来查询层次结构。
到目前为止的问题是,从邻接列表到嵌套集的转换方法非常慢,因为大多数人使用称为“推栈”的极端 RBAR 方法来进行转换,并且被认为是一种昂贵的方法通过邻接列表实现维护的简单性和嵌套集的出色性能。结果,大多数人最终不得不选择其中一个,特别是当节点数量超过 100,000 个左右时。使用推栈方法可能需要一整天的时间才能对传销者认为的小型百万节点层次结构进行转换。
我想通过提出一种以看似不可能的速度将邻接表转换为嵌套集的方法来给 Celko 一些竞争。这是我的 i5 笔记本电脑上推栈方法的性能。
这是新方法的持续时间(括号中是推栈方法)。
是的,这是正确的。 100 万个节点在一分钟内完成转换,100,000 个节点在 4 秒内完成转换。
您可以通过以下 URL 了解新方法并获取代码副本。
http://www.sqlservercentral.com/articles/Hierarchy/94040/
我还开发了一个“预使用类似的方法聚合”层次结构。传销者和制作物料清单的人会对本文特别感兴趣。
http://www.sqlservercentral.com/articles/T-SQL/94570/
如果您这样做停下来看看这两篇文章,跳到“加入讨论”链接,让我知道您的想法。
My favorite answer is as what the first sentence in this thread suggested. Use an Adjacency List to maintain the hierarchy and use Nested Sets to query the hierarchy.
The problem up until now has been that the coversion method from an Adjacecy List to Nested Sets has been frightfully slow because most people use the extreme RBAR method known as a "Push Stack" to do the conversion and has been considered to be way to expensive to reach the Nirvana of the simplicity of maintenance by the Adjacency List and the awesome performance of Nested Sets. As a result, most people end up having to settle for one or the other especially if there are more than, say, a lousy 100,000 nodes or so. Using the push stack method can take a whole day to do the conversion on what MLM'ers would consider to be a small million node hierarchy.
I thought I'd give Celko a bit of competition by coming up with a method to convert an Adjacency List to Nested sets at speeds that just seem impossible. Here's the performance of the push stack method on my i5 laptop.
And here's the duration for the new method (with the push stack method in parenthesis).
Yes, that's correct. 1 million nodes converted in less than a minute and 100,000 nodes in under 4 seconds.
You can read about the new method and get a copy of the code at the following URL.
http://www.sqlservercentral.com/articles/Hierarchy/94040/
I also developed a "pre-aggregated" hierarchy using similar methods. MLM'ers and people making bills of materials will be particularly interested in this article.
http://www.sqlservercentral.com/articles/T-SQL/94570/
If you do stop by to take a look at either article, jump into the "Join the discussion" link and let me know what you think.
邻接模型 + 嵌套集模型
我选择它是因为我可以轻松地将新项目插入到树中(您只需要一个分支的 id 即可向其中插入新项目)并且查询速度也相当快。
parent
列即可。lft
位于父级的lft
和rgt
之间的项目。lft
低于该节点的lft
和rgt
的项目大于节点的rgt
并按parent
排序。我需要比插入更快地访问和查询树,这就是我选择这个的原因
唯一的问题是修复
左
和右
列插入新项目时。好吧,我为它创建了一个存储过程,并在每次插入新项目时调用它,这在我的情况下很少见,但速度非常快。我从 Joe Celko 的书中得到了这个想法,DBA SE 中解释了存储过程以及我是如何想出它的
https://dba.stackexchange.com/q/89051/41481
虽然此解决方案允许快速搜索来定位对于处理需要频繁插入或删除的大型数据集来说,由于其在这些操作中的性能较慢,它并不理想。因此,它最适合不经常更换的表。
Adjacency Model + Nested Sets Model
I went for it because I could insert new items to the tree easily (you just need a branch's id to insert a new item to it) and also query it quite fast.
parent
column.lft
betweenlft
andrgt
of parent.lft
lower than the node'slft
andrgt
bigger than the node'srgt
and sort the byparent
.I needed to make accessing and querying the tree faster than inserts, that's why I chose this
The only problem is to fix the
left
andright
columns when inserting new items. well I created a stored procedure for it and called it every time I inserted a new item which was rare in my case but it is really fast.I got the idea from the Joe Celko's book, and the stored procedure and how I came up with it is explained here in DBA SE
https://dba.stackexchange.com/q/89051/41481
Although this solution allows for rapid searches to locate descendants, it is not ideal for handling large datasets that require frequent inserts or deletes due to its slow performance in these operations. Therefore, it is best suited for tables that won't chnage frequently.
这个设计还没提到:
多血统列
虽然它有局限性,但如果你能忍受的话,它非常简单,非常高效的。特点:
下面是一个示例 - 鸟类分类树,因此层次结构为类/目/科/属/种 - 物种是最低级别,1 行 = 1 个分类单元(对应于叶节点的物种):
以及数据示例:
这很棒,因为这样您就可以以非常简单的方式完成所有需要的操作,只要内部类别不会改变其在树中的级别。
This design was not mentioned yet:
Multiple lineage columns
Though it has limitations, if you can bear them, it's very simple and very efficient. Features:
Here follows an example - taxonomic tree of birds so the hierarchy is Class/Order/Family/Genus/Species - species is the lowest level, 1 row = 1 taxon (which corresponds to species in the case of the leaf nodes):
and the example of the data:
This is great because this way you accomplish all the needed operations in a very easy way, as long as the internal categories don't change their level in the tree.
这是对您问题的非常片面的回答,但我希望仍然有用。
Microsoft SQL Server 2008 实现了两个对于管理分层数据非常有用的功能:
看看 " Model Your Data Hierarchies With SQL Server 2008”,作者:MSDN 上的 Kent Tegels。另请参阅我自己的问题:SQL Server 2008 中的递归同表查询
This is a very partial answer to your question, but I hope still useful.
Microsoft SQL Server 2008 implements two features that are extremely useful for managing hierarchical data:
Have a look at "Model Your Data Hierarchies With SQL Server 2008" by Kent Tegels on MSDN for starts. See also my own question: Recursive same-table query in SQL Server 2008
如果您的数据库支持数组,您还可以将沿袭列或具体化路径实现为父 ID 数组。
具体来说,使用 Postgres,您可以使用集合运算符来查询层次结构,并通过 GIN 索引获得出色的性能。这使得在单个查询中查找父母、孩子和深度变得非常简单。更新也非常易于管理。
如果您好奇的话,我有一篇关于使用 用于物化路径的数组的完整文章。
If your database supports arrays, you can also implement a lineage column or materialized path as an array of parent ids.
Specifically with Postgres you can then use the set operators to query the hierarchy, and get excellent performance with GIN indices. This makes finding parents, children, and depth pretty trivial in a single query. Updates are pretty manageable as well.
I have a full write up of using arrays for materialized paths if you're curious.
这确实是一个方钉圆孔的问题。
如果关系数据库和 SQL 是您拥有或愿意使用的唯一锤子,那么迄今为止发布的答案就足够了。但是,为什么不使用专门用于处理分层数据的工具呢? 图数据库非常适合复杂的分层数据。
与图数据库解决方案可以轻松解决相同问题相比,关系模型的低效率以及将图/分层模型映射到关系模型的任何代码/查询解决方案的复杂性是不值得的。
将物料清单视为常见的分层数据结构。
两个子组件之间的最短路径:简单的图遍历算法。可接受的路径可以根据标准进行限定。
相似度:两个程序集之间的相似程度是多少?对两个子树执行遍历,计算两个子树的交集和并集。相似百分比是交集除以并集。
传递闭包:遍历子树并总结感兴趣的字段,例如“子组件中有多少铝?”
是的,您可以使用 SQL 和关系数据库来解决问题。然而,如果您愿意使用正确的工具来完成工作,还有更好的方法。
This is really a square peg, round hole question.
If relational databases and SQL are the only hammer you have or are willing to use, then the answers that have been posted thus far are adequate. However, why not use a tool designed to handle hierarchical data? Graph database are ideal for complex hierarchical data.
The inefficiencies of the relational model along with the complexities of any code/query solution to map a graph/hierarchical model onto a relational model is just not worth the effort when compared to the ease with which a graph database solution can solve the same problem.
Consider a Bill of Materials as a common hierarchical data structure.
Shortest path between two sub-assemblies: Simple graph traversal algorithm. Acceptable paths can be qualified based on criteria.
Similarity: What is the degree of similarity between two assemblies? Perform a traversal on both sub-trees computing the intersection and union of the two sub-trees. The percent similar is the intersection divided by the union.
Transitive Closure: Walk the sub-tree and sum up the field(s) of interest, e.g. "How much aluminum is in a sub-assembly?"
Yes, you can solve the problem with SQL and a relational database. However, there are much better approaches if you are willing to use the right tool for the job.
我正在使用带有闭包表的 PostgreSQL 作为我的层次结构。
我有一个适用于整个数据库的通用存储过程:
然后,对于具有层次结构的每个表,我创建一个触发器
为了从现有层次结构填充闭包表,我使用此存储过程:
闭包表由 3 列定义 - ANCESTOR_ID、DESCENDANT_ID , 深度。可以(我什至建议)存储具有相同 ANCESTOR 和 DESCENDANT 值以及 DEPTH 值为零的记录。这将简化检索层次结构的查询。它们确实非常简单:
I am using PostgreSQL with closure tables for my hierarchies.
I have one universal stored procedure for the whole database:
Then for each table where I have a hierarchy, I create a trigger
For populating a closure table from existing hierarchy I use this stored procedure:
Closure tables are defined with 3 columns - ANCESTOR_ID, DESCENDANT_ID, DEPTH. It is possible (and I even advice) to store records with same value for ANCESTOR and DESCENDANT, and a value of zero for DEPTH. This will simplify the queries for retrieval of the hierarchy. And they are very simple indeed:
MySQL 现在支持 JSON 数据类型:
https://dev.mysql。 com/doc/refman/8.0/en/json.html
MySQL now supports the JSON data type:
https://dev.mysql.com/doc/refman/8.0/en/json.html