改进查询集中对象之间距离的计算

时间:2019-07-30 07:58:56

标签: python django gis postgis geodjango

我的Django项目中有下一个模型:

class Area(models.Model):
    name = models.CharField(_('name'), max_length=100, unique=True)
    ...

class Zone(models.Model):
    name = models.CharField(verbose_name=_('name'),
                            max_length=100,
                            unique=True)
    area = models.ForeignKey(Area,
                             verbose_name=_('area'),
                             db_index=True)
    polygon = PolygonField(srid=4326,
                           verbose_name=_('Polygon'),)
    ...

Area就像一个城市,而Zone就像一个地区。

因此,我想为每个区域高速缓存其区域中其他区域的顺序。像这样:


def store_zones_by_distance():
    zones = {}
    zone_qs = Zone.objects.all()
    for zone in zone_qs:
        by_distance = Zone.objects.filter(area=zone.area_id).distance(zone.polygon.centroid).order_by('distance').values('id', 'name', ...)
        zones[zone.id] = [z for z in by_distance]
    cache.set("zones_by_distance", zones, timeout=None)

但是问题在于它效率不高且不可扩展。我们有382个区域,此函数将383个查询发送到数据库,并且非常慢(SQL时间为3.80秒,全局时间为4.20秒)。

是否有任何有效且可扩展的方式来获取它。我曾经想到过这样的事情:

def store_zones_by_distance():
    zones = {}
    zone_qs = Zone.objects.all()
    for zone in zone_qs.prefetch_related(Prefetch('area__zone_set', queryset=Zone.objects.all().distance(F('polygon__centroid')).order_by('distance'))):
        by_distance = zone.area.zone_set.all().values('id', 'name', ...)
        zones[zone.id] = [z for z in by_distance]

这显然不起作用,但是类似这样,在SQL(与预取相关)中对有序区域(area__zone_set)进行缓存。

编辑 store_zones_by_distance将返回(或在缓存中设置)以下内容:

{
    1: [{"id": 1, "name": "Zone 1"}, {"id": 2, "name": "Zone 2"}, {"id": 2, "name": "Zone 4"}, {"id": 2, "name": "Zone 3"}],
    2: [{"id": 2, "name": "Zone 2"}, {"id": 2, "name": "Zone 4"}, {"id": 2, "name": "Zone 1"}, {"id": 2, "name": "Zone 3"}],
    ...
}

3 个答案:

答案 0 :(得分:3)

您可以进行嵌套的预取,导致3个查询。

def store_zones_by_distance():
    area_qs = Area.objects.prefetch_related(Prefetch(
        'zone_set',
        queryset=Zone.objects.annotate(
            distance=F('polygon__centroid')
        ).order_by('distance')
    ))
    zones = Zone.objects.all().prefetch_related(Prefetch(
        'area',
        queryset=area_qs,
        to_attr='prefetched_area'
    ))

    zones_dict = {}
    for zone in zones:
        zones_dict[zone.id] = zone.prefetched_area.zone_set

更新,将 @JohnMoutafis 中的功能与django.forms.model_to_dict结合使用,可在2个查询中完成预期的输出。

from django.db.models import F, Prefetch
from django.forms import model_to_dict

def store_zones_by_distance():
    zones = {}
    areas = Area.objects.prefetch_related(Prefetch(
        'zone_set',
        queryset=Zone.objects.annotate(
            distance=Centroid('polygon')
        ).order_by('distance')
    ))

    for area in areas:
        for zone in area.zone_set.all():
            zones[zone.id] = [
                model_to_dict(zone, fields=['id', 'name'])
                for zone in area.zone_set.all()
            ]

答案 1 :(得分:2)

更新:自从我们来回往返后,我相信我们可以找到解决该问题的可行方案。

您需要按区域之间的距离排列区域。据我了解,这不需要发生很多次(因此您正在使用缓存)。
本质上,您需要在服务器启动时以及每次在数据库上更新(添加,删除,打补丁等)新区域时设置一次此缓存。

我们可以使用AppConfig.ready()函数来设置服务器启动时的缓存,然后为区域更新情况创建一个post_save和一个post_delete信号。

让我们编写在这两种情况下将使用的实用程序方法:

from django.db.models import Q
from django.forms import model_to_dict

def store_zones_by_distance():
    zones = {}
    areas = Area.objects.prefetch_related(`zone_set`).all()

    for area in areas:
        for zone in area.zone_set.all():
            ordered_zones = area.zone_set.filter(~Q(id=zone.id)).distance(
                zone.polygon.centroid
            ).order_by('distance')

            zones[zone.id] = [
               model_to_dict(ordered_zone, fields=['id', 'name'])
               for ordered_zone in ordered_zones
            ]
    cache.set("zones_by_distance", zones, timeout=None)

方法说明:

  • ordered_zones将返回除我们当前正在检查的区域以外的所有区域(因此filter(~Q(id=zone.id))转换为“过滤ID为 NOT 的ID的区域)当前区域”),按其质心到当前区域质心的距离排序。
  • 利用@bdoubleu model_to_dict的建议,我们以字典表示形式创建模型实例的列表。
  • 每个区域的最终结果如下:[{"id": 1, "name": "Zone 1"}, {"id": 2, "name": "Zone 2"}, ...]

现在,我们需要创建post_savepost_delete信号并将所有内容连接到AppConfig.ready()函数(基本上,我们将按照此处描述的步骤进行操作:Django Create and Save Many instances of model when another object are created扭曲)。

我假设store_zones_by_distance是在your_app/utils.py中创建的(您可以在任意位置创建它)

  1. post_save中创建post_deleteyour_app/signals.py信号:

    from django.db.models.signals import post_save, post_delete
    from django.dispatch import receiver
    
    from your_app.models import Zone
    from your_app.utils import store_zones_by_distance
    
    
    @receiver(post_save, sender=Zone)
    def update_added_zone_cache(sender, instance, created, **kwargs):
        store_zones_by_distance()
    
    @receiver(post_delete, sender=Zone)
    def update_removed_zone_cache(sender, instance, *args, **kwargs):
        store_zones_by_distance()
    
  2. 在服务器启动时运行store_zones_by_distance,并在your_app/app.py中连接信号:

    class YourAppConfig(AppConfig):
        name = 'your_project.your_app'
    
        def ready(self):
            import your_project.your_app.signals
            # Run it once at server start
            store_zones_by_distance()
    

您将不会为此节省很多时间,但是您将准备好缓存,并且在更新之前不会阻塞任何端点。


出于遗留评论的原因,我将其保留在此处,但这不是@Goin想要的解决方案。

我相信您已经接近一个好的解决方案。
正如您已经在尝试一种更优化的解决方案那样,you can access the foreign key related objects with the _set notation。您可以使用ZonesArea访问zones_set
_set允许您像往常一样在其上应用任何queryset方法。

现在,为了避免多个数据库命中,我们需要构造a custom Prefetch,否则我们将添加polygon__centroid距离作为注释。
这么说吧,让我们实现它:

def store_zones_by_distance():
    zones = {}
    areas = Area.objects.prefetch_related(
        Prefetch(
            `zone_set`,
            queryset=Zone.object.all().annotate(
                centroid_distance=Centroid('polygon')
            ).order_by('centroid_distance')
        )
    ).all()

    for area in areas:
        for zone in area.zone_set.all():
            zones[zone.id] = area.zone_set.all().values_list('id', 'name', ...)

这将导致对数据库的单个查询,该查询将获取方法所需的一切。
编辑: 正如@bdoubleu所述,values_list将在每个区域引起一个额外的查询,因此您可能希望放弃该查询并将查询集保留在字典{{1}中}
请记住,尽管使用2 zones[zone.id] = area.zone_set.all()可能仍然很耗时。

答案 2 :(得分:-2)

对不起,我不能发表评论,因为我很新,所以我必须在这里写下建议。 在您的第一个示例中:

def store_zones_by_distance():
    zones = {}
    zone_qs = Zone.objects.all()
    for zone in zone_qs:
        by_distance = Zone.objects.filter(area=zone.area_id).distance(zone.polygon.centroid).order_by('distance').values('id', 'name', ...)
        zones[zone.id] = [z for z in by_distance]
    cache.set("zones_by_distance", zones, timeout=None)

更改时需要多长时间会很有趣:

zone_qs = Zone.objects.all()

to:

zone_qs = Zone.objects.all().prefetch_related("area")

by_distance = Zone.objects.filter(area=zone.area_id).distance...

to:

by_distance = zone_qs.objects.filter(area=zone.area_id).distance...

希望我可以为这个话题提供一些有用的信息。