Postgres 物化路径 - 使用 ltree 有什么好处?
Postgres Materialized Path - What are the benefits of using ltree?
物化路径是一种在 SQL 中表示层次结构的方法。每个节点都包含路径本身及其所有祖先 (grandparent/parent/self
)。
django-treebeard
实现 MP (docs):
为了保持一致的性能,路径的每一步都是固定长度。
每个节点包含 depth
和 numchild
字段(以最小的写入成本快速读取)。
路径字段被索引(使用标准的b树索引):
The materialized path approach makes heavy use of LIKE in your database, with clauses like WHERE path LIKE '002003%'. If you think that LIKE is too slow, you’re right, but in this case the path field is indexed in the database, and all LIKE clauses that don’t start with a % character will use the index. This is what makes the materialized path approach so fast.
get_ancestors
(link)的实施:
将节点与包含当前路径子集的路径匹配(steplen
是一步的固定长度)。
paths = [
self.path[0:pos]
for pos in range(0, len(self.path), self.steplen)[1:]
]
return get_result_class(self.__class__).objects.filter(
path__in=paths).order_by('depth')
get_descendants
(link)的实施:
匹配深度大于自身的节点和以当前路径开始的路径。
return cls.objects.filter(
path__startswith=parent.path,
depth__gte=parent.depth
).order_by(
'path'
)
这种方法的潜在缺点:
- 深度嵌套的层次结构会导致路径过长,从而影响读取性能。
- 移动节点需要更新所有后代的路径。
Postgres 包含 ltree
扩展,它提供了自定义 GiST index (docs)。
我不清楚 ltree
比 django-treebeard
的实施有哪些好处。 article 认为只有 ltree
可以回答 get_ancestors
问题,但如前所述,找出节点的祖先(或后代)是微不足道的。
[顺便说一句,如果找到这个 Django ltree
库 - https://github.com/mariocesar/django-ltree].
两种方法都使用索引(django-treebeard
使用 b-tree,ltree
使用自定义 GiST)。我有兴趣了解 ltree
GiST 的实现以及为什么对于这个特定用例(物化路径)它可能是比标准 b 树更有效的索引。
附加链接
What are the options for storing hierarchical data in a relational database?
TL;DR 无法对多个后代节点(或尚未检索路径的单个节点)进行可重用标签、复杂搜索模式和祖先搜索使用物化路径索引完成。
对于那些对血腥细节感兴趣的人...
首先,只有当您没有在节点描述中重复使用任何标签时,您的问题才有意义。如果是,l-tree 确实是两者中唯一的选择。但是物化路径实现通常不需要这个,所以让我们把它放在一边。
一个明显的区别在于 l-tree 为您提供的搜索类型的灵活性。考虑这些示例(来自您问题中链接的 ltree
文档):
foo Match the exact label path foo
*.foo.* Match any label path containing the label foo
*.foo Match any label path whose last label is foo
第一个查询显然可以通过物化路径实现。最后一个也是可以实现的,您可以将查询调整为兄弟查找。然而,中间情况不能通过单个索引查找直接实现。您要么必须将其分解为两个查询(所有后代 + 所有祖先),要么求助于 table 扫描。
还有像这样的非常复杂的查询(也来自文档):
Top.*{0,2}.sport*@.!football|tennis.Russ*|Spain
物化路径索引在这里没有用,需要完整的 table 扫描来处理这个问题。如果您想将此作为 SARGable 查询来执行,l-tree 是唯一的选择。
但是对于标准的分层操作,找到以下任何一个:
- parent
- children
- 后代
- 根节点
- 叶节点
物化路径与 l-tree 一样有效。与 article linked above 相反,使用 b-tree 搜索一个共同祖先的所有后代是非常可行的。查询格式 WHERE path LIKE 'A.%'
是可搜索的,前提是你的索引准备得当(我必须用 varchar_pattern_ops
显式标记我的路径索引才能让它工作)。
此列表中缺少的是找到后代的所有 祖先。不幸的是,查询格式 WHERE 'A.B.C.D' LIKE path || '.%'
不会使用索引。一些库实现的一种解决方法是从路径中解析出祖先节点,并直接查询它们:WHERE id IN ('A', 'B', 'C')
。但是,这仅在您以已检索其路径的特定节点的祖先为目标时才有效。 l-tree 将在这一点上获胜。
物化路径是一种在 SQL 中表示层次结构的方法。每个节点都包含路径本身及其所有祖先 (grandparent/parent/self
)。
django-treebeard
实现 MP (docs):
为了保持一致的性能,路径的每一步都是固定长度。
每个节点包含
depth
和numchild
字段(以最小的写入成本快速读取)。路径字段被索引(使用标准的b树索引):
The materialized path approach makes heavy use of LIKE in your database, with clauses like WHERE path LIKE '002003%'. If you think that LIKE is too slow, you’re right, but in this case the path field is indexed in the database, and all LIKE clauses that don’t start with a % character will use the index. This is what makes the materialized path approach so fast.
get_ancestors
(link)的实施:
将节点与包含当前路径子集的路径匹配(steplen
是一步的固定长度)。
paths = [
self.path[0:pos]
for pos in range(0, len(self.path), self.steplen)[1:]
]
return get_result_class(self.__class__).objects.filter(
path__in=paths).order_by('depth')
get_descendants
(link)的实施:
匹配深度大于自身的节点和以当前路径开始的路径。
return cls.objects.filter(
path__startswith=parent.path,
depth__gte=parent.depth
).order_by(
'path'
)
这种方法的潜在缺点:
- 深度嵌套的层次结构会导致路径过长,从而影响读取性能。
- 移动节点需要更新所有后代的路径。
Postgres 包含 ltree
扩展,它提供了自定义 GiST index (docs)。
我不清楚 ltree
比 django-treebeard
的实施有哪些好处。 article 认为只有 ltree
可以回答 get_ancestors
问题,但如前所述,找出节点的祖先(或后代)是微不足道的。
[顺便说一句,如果找到这个 Django ltree
库 - https://github.com/mariocesar/django-ltree].
两种方法都使用索引(django-treebeard
使用 b-tree,ltree
使用自定义 GiST)。我有兴趣了解 ltree
GiST 的实现以及为什么对于这个特定用例(物化路径)它可能是比标准 b 树更有效的索引。
附加链接
What are the options for storing hierarchical data in a relational database?
TL;DR 无法对多个后代节点(或尚未检索路径的单个节点)进行可重用标签、复杂搜索模式和祖先搜索使用物化路径索引完成。
对于那些对血腥细节感兴趣的人...
首先,只有当您没有在节点描述中重复使用任何标签时,您的问题才有意义。如果是,l-tree 确实是两者中唯一的选择。但是物化路径实现通常不需要这个,所以让我们把它放在一边。
一个明显的区别在于 l-tree 为您提供的搜索类型的灵活性。考虑这些示例(来自您问题中链接的 ltree
文档):
foo Match the exact label path foo
*.foo.* Match any label path containing the label foo
*.foo Match any label path whose last label is foo
第一个查询显然可以通过物化路径实现。最后一个也是可以实现的,您可以将查询调整为兄弟查找。然而,中间情况不能通过单个索引查找直接实现。您要么必须将其分解为两个查询(所有后代 + 所有祖先),要么求助于 table 扫描。
还有像这样的非常复杂的查询(也来自文档):
Top.*{0,2}.sport*@.!football|tennis.Russ*|Spain
物化路径索引在这里没有用,需要完整的 table 扫描来处理这个问题。如果您想将此作为 SARGable 查询来执行,l-tree 是唯一的选择。
但是对于标准的分层操作,找到以下任何一个:
- parent
- children
- 后代
- 根节点
- 叶节点
物化路径与 l-tree 一样有效。与 article linked above 相反,使用 b-tree 搜索一个共同祖先的所有后代是非常可行的。查询格式 WHERE path LIKE 'A.%'
是可搜索的,前提是你的索引准备得当(我必须用 varchar_pattern_ops
显式标记我的路径索引才能让它工作)。
此列表中缺少的是找到后代的所有 祖先。不幸的是,查询格式 WHERE 'A.B.C.D' LIKE path || '.%'
不会使用索引。一些库实现的一种解决方法是从路径中解析出祖先节点,并直接查询它们:WHERE id IN ('A', 'B', 'C')
。但是,这仅在您以已检索其路径的特定节点的祖先为目标时才有效。 l-tree 将在这一点上获胜。