【问题标题】:efficient function to retrieve a queryset of ancestors of an mptt queryset检索 mptt 查询集祖先查询集的高效函数
【发布时间】:2011-09-22 05:11:53
【问题描述】:

有没有人有一个有效的算法来检索一个 mptt 查询集的所有祖先?到目前为止我能想到的最好的是这样的:

def qs_ancestors(queryset):
    if isinstance(queryset, EmptyQuerySet):
        return queryset
    queryset_aggs = queryset.values_list('tree_id', 'level').annotate(max_lft=Max('lft'), min_rght=Min('rght'))
    new_queryset = queryset.none()
    for tree_id, level, max_lft, min_rght in queryset_aggs:
        ancestors = MyModel.objects.filter(
           tree_id=tree_id,
           level__lt=level, 
           lft__lte=max_lft,
           rght__gte=min_rght,
        )
        new_queryset = ancestors | new_queryset
    return new_queryset

这种方法有两个问题:

  1. 如果存在不相邻的分支(即它实际上不起作用),则会失败
  2. 它的效率非常低,因为它最终在最终查询中包含number_of_trees*number_of_levels 子句,它可以很快变得非常大

我愿意在其他地方缓存祖先,但我想不出一种有效的方法。我考虑添加一个以逗号分隔的祖先 id 列表的字段,然后在额外内容中执行 GROUP_CONCAT(我在 MySQL 中),但我认为这可能会变得巨大/缓慢。

【问题讨论】:

    标签: python django hierarchical-data django-mptt mptt


    【解决方案1】:

    我曾经写过一个类似的算法。我有一个显示 MPTT 树的视图,它是一个非常大的树,所以我无法在 HTML 模板中加载它的所有数据。所以我在初始加载时只显示了根节点,并使用 Ajax 加载其他节点。

    在我的老板要求我实施“搜索”选项之前,它一直运行良好。搜索必须查看所有节点并在找到匹配项时分解树。我花了一段时间才弄清楚这一点,但我终于明白了。这是a提出的解决方案:

    from django.db.models import Q
    
    def get_parents(self, qs):
        tree_list = {}
        query = Q()
        for node in qs:
            if node.tree_id not in tree_list:
                tree_list[node.tree_id] = []
    
            parent =  node.parent.pk if node.parent is not None else None,
    
            if parent not in tree_list[node.tree_id]:
                tree_list[node.tree_id].append(parent)
    
                query |= Q(lft__lt=node.lft, rght__gt=node.rght, tree_id=node.tree_id)
    
        return YourModel.objects.filter(query)
    

    它只需要运行两个查询,初始qs 作为参数传递,最后一个查询集由函数返回。 tree_list 是一个字典,用于存储已添加到查询集中的节点,它是一种优化,算法不需要工作。但由于我使用的是一棵相对较大的树,所以我不得不将它包括在内。

    我想你可以把这个方法变成一个管理器,让它更通用,即让它适用于任何 MPTT 模型,而不仅仅是YourModel

    【讨论】:

    • 非常聪明——我最终在我的 mptt 模型上添加了一个文本字段,称为祖先 ID,它有一个逗号分隔的 id 列表——然后只需这样做:queryset_ancestor_ids = set([]); for a in queryset.values_list('ancestor_ids', flat=True): if a: queryset_ancestor_ids.update(a.split(",")); return SegmentNode.objects.filter(pk__in=queryset_ancestor_ids)我会做一些基准测试,看看哪个更快。
    【解决方案2】:

    怎么样:

    def qs_ancestors(queryset):
        if isinstance(queryset, EmptyQuerySet):
            return queryset
        new_queryset = queryset.none()
        for obj in queryset:
            new_queryset = new_queryset | obj.get_ancestors()
    return new_queryset
    

    它仍然是 len(queryset) 子句。您可以通过执行预处理传递来潜在地减少子句的数量,该传递删除作为查询集中其他对象的祖先的对象,例如:

    min_obj_set = []
    for obj in queryset.order_by('tree_id', '-level'):
        for obj2 in min_obj_set:
            if obj.is_ancestor_of(obj2):
                break
        else:
            min_obj_set.append(obj)
    

    虽然上面的 sn-p 只是一个示例,但如果您的 querset 包含大量对象,您可能希望使用 BST。

    不过,您必须测试与更大的数据库查询相比,这是否会提高速度。

    【讨论】:

    • 这种方法的问题是需要对每个对象进行查询。 obj.get_ancestors() 执行查询,不是吗?所以你最终得到 len(qs) + 1 没有优化的查询。
    • @bacon 不,obj.get_ancestors() 返回一个惰性求值的 QuerySet 对象。 QuerySet 在循环中被 ORed 在一起——在这种情况下,where 子句将被 ORed——然后仅在您稍后尝试访问这些值时执行。只会执行两个查询,一个用于原始 QuerySet,一个用于获取所有祖先。
    • 啊——抱歉,好电话。所以你的和@Cesar的大致相同?
    • 不过,您可以使用 .select_related 消除额外的查询。
    • 我从来没有研究过 django 如何在一个通过 ForeignKey 字段链接到自身的模型上处理 select_related。除非使用深度约束,否则它很可能会导致无限循环。在这种情况下,虽然不需要。 node.parent_id 等同于 node.parent.pk,没有 django docs 中描述的额外查询。带有 select_related 的查询可能会返回更多列,因此效率会降低。
    猜你喜欢
    • 2014-01-26
    • 2015-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-04
    • 1970-01-01
    相关资源
    最近更新 更多