Postgres 物化路径 - 使用 ltree 有什么好处?

Postgres Materialized Path - What are the benefits of using ltree?

物化路径是一种在 SQL 中表示层次结构的方法。每个节点都包含路径本身及其所有祖先 (grandparent/parent/self)。

django-treebeard 实现 MP (docs):

  1. 为了保持一致的性能,路径的每一步都是固定长度。

  2. 每个节点包含 depthnumchild 字段(以最小的写入成本快速读取)。

  3. 路径字段被索引(使用标准的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_ancestorslink)的实施:

将节点与包含当前路径子集的路径匹配(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_descendantslink)的实施:

匹配深度大于自身的节点和以当前路径开始的路径。

return cls.objects.filter(
    path__startswith=parent.path,
    depth__gte=parent.depth
).order_by(
    'path'
)

这种方法的潜在缺点:

  1. 深度嵌套的层次结构会导致路径过长,从而影响读取性能。
  2. 移动节点需要更新所有后代的路径。

Postgres 包含 ltree 扩展,它提供了自定义 GiST index (docs)。

我不清楚 ltreedjango-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?

https://news.ycombinator.com/item?id=709970

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 将在这一点上获胜。