减少Django数据库查询

时间:2016-03-23 06:49:21

标签: python django python-3.x django-templates django-views

我有非常大的数据集并且在不断增长,我需要创建许多过滤器,但它会很快失控,并希望有人可以帮助我将一些查询合并到一个调用中。以下是我的观点的开始。

调用#1 - for循环显示所有结果表

traffic = Traffic.objects.all()

致电#2 - 合并总和查询

totals = Traffic.objects.aggregate(Sum('sessions'), Sum('new_users'), Sum('reminder'), Sum('campaigns'), Sum('new_sales'), Sum('sales_renewals'))
    total_sessions = totals.get('sessions__sum')
    total_new_users = totals.get('new_users__sum')
    total_reminder = totals.get('reminder__sum')
    total_campaigns = totals.get('campaigns__sum')
    total_new_sales = totals.get('new_sales__sum')
    total_sales_renewals = totals.get('sales_renewals__sum')

呼叫#3,#4,#5,#6等等... - 按月和星期几过滤数据库

total_sessions_2014_m = Traffic.objects.filter(created__year='2014', created__week_day=2).aggregate(Sum('sessions'))

total_sessions_2014_m = Traffic.objects.filter(created__year='2014', created__week_day=3).aggregate(Sum('sessions'))

total_sessions_2014_m = Traffic.objects.filter(created__year='2014', created__week_day=4).aggregate(Sum('sessions'))

total_sessions_2014_m = Traffic.objects.filter(created__year='2014', created__week_day=5).aggregate(Sum('sessions'))

total_sessions_2014_m = Traffic.objects.filter(created__year='2014', created__week_day=6).aggregate(Sum('sessions'))

问题是,我需要创建几十个过滤器,因为我有3年的数据,每列有多个数据点,我们需要总和。

问题:

  1. 我可以将#1呼叫合并到呼叫#2
  2. 我可以使用呼叫#2查询呼叫#3的总和,因此我不必调用数据库中的所有对象来过滤它,然后再进行几次这样的操作吗?
  3. 正如您所看到的,这将很快失控。任何帮助将非常感激。谢谢。

    已更新以添加 流量模型

    class Timestamp(models.Model):
        created = models.DateField()
    
        class Meta:
            abstract = True
    
    
    class Traffic(Timestamp):
        sessions = models.IntegerField(blank=True, null=True)
        new_users = models.IntegerField(blank=True, null=True)
        reminder = models.IntegerField(blank=True, null=True)
        campaigns = models.IntegerField(blank=True, null=True)
        new_sales = models.IntegerField(blank=True, null=True)
        sales_renewals = models.IntegerField(blank=True, null=True)
    
        # Meta and String
        class Meta:
            verbose_name = 'Traffic'
            verbose_name_plural = 'Traffic Data'
    
        def __str__(self):
            return "%s" % self.created
    

3 个答案:

答案 0 :(得分:12)

有许多方法可以使用Django ORM优化数据库查询。像往常一样,Django documentation很棒并且有很好的列表。以下是查询优化的一些快速提示:

1) iterator()

如果您只访问queryset一次。例如,您可以将其用作

traffic = Traffic.objects.all()

for t in traffic.iterator():
    ...
    ...

2) db_index=True

定义models的字段时。正如Django documentation所说,

  

这是您确定后的第一优先事项   分析应添加哪些索引。使用Field.db_index或   Meta.index_together从Django添加这些。考虑添加索引   到您经常使用filter(),exclude()查询的字段,   order_by()等作为索引可能有助于加快查找速度。

因此,您可以将模型修改为

class Traffic(Timestamp):
    sessions = models.IntegerField(blank=True, null=True, db_index=True)
    new_users = models.IntegerField(blank=True, null=True, db_index=True)
    reminder = models.IntegerField(blank=True, null=True, db_index=True)
    campaigns = models.IntegerField(blank=True, null=True, db_index=True)
    new_sales = models.IntegerField(blank=True, null=True, db_index=True)

3) prefetch_related()select_related()

如果您在models内有关系,则可以选择使用prefetch_relatedselect_related。根据{{​​3}},

select_related通过创建SQL join并在SELECT语句中包含相关对象的字段来工作。因此,select_related获取相同数据库查询中的相关对象。但是,为了避免加入“多”关系会产生更大的结果集,select_related仅限于单值关系 - 外键和一对一。

另一方面,

prefetch_related对每个进行单独查找 关系,并在Python中“加入”。这允许它预取 多对多和多对一对象,无法使用 select_related,以及select_related支持的外键和一对一关系。

select_related执行joinprefetch_related执行两次单独的查询。使用它们可以使您的查询速度提高30%。

4) Django documentation

如果您的template设计允许您在多个页面中显示结果,则可以使用Pagination

5) Django Pagination

您还需要了解Django Querysets是惰性的,这意味着它不会查询数据库,直到它被使用/评估。 Django中的查询集表示数据库中的多个行,可选地由查询过滤。例如,

traffic = Traffic.objects.all()

上面的代码不会运行任何数据库查询。您可以使用traffic查询集并应用其他过滤器,或将其传递给函数,并且不会将任何内容发送到数据库。这很好,因为查询数据库是显着减慢Web应用程序速度的因素之一。要从数据库中获取数据,您需要遍历查询集:

for t in traffic.iterator():
    print(t.sessions)

6) Querysets are Lazy

Django Debug Toolbar是一组可配置的面板,显示有关当前请求/响应的各种调试信息,点击后,显示有关面板内容的更多详细信息。这包括:

  • 请求计时器
  • SQL查询,包括执行时间和指向EXPLAIN每个查询的链接

修改代码 :(记住查询集是懒惰的

traffic = Traffic.objects.all()
totals = traffic.aggregate(Sum('sessions'), Sum('new_users'), Sum('reminder'), Sum('campaigns'), Sum('new_sales'), Sum('sales_renewals'))
total_sessions = totals.get('sessions__sum')
total_new_users = totals.get('new_users__sum')
total_reminder = totals.get('reminder__sum')
total_campaigns = totals.get('campaigns__sum')
total_new_sales = totals.get('new_sales__sum')
total_sales_renewals = totals.get('sales_renewals__sum')

t_2014 = traffic.filter(created__year='2014')
t_sessions_2014_wd2 = t_2014.filter(created__week_day=2).aggregate(Sum('sessions'))
...
...

对于模板中的调用#1 (for循环显示所有结果的表格):

{% for t in traffic.iterator %}
    {{ t.sessions }}
    ...
    ...
{% endfor %}

答案 1 :(得分:3)

对于问题1,在第一次调用时重用查询集不应该是一个问题。

traffic = Traffic.objects.all()
totals = traffic.aggregate(Sum('sessions'), Sum('new_users'), Sum('reminder'), Sum('campaigns'), Sum('new_sales'), Sum('sales_renewals'))

这样可以免除对数据库的额外调用。

关于问题2,您可以再次重用第一次调用中的查询集,并过滤年份,这会为您提供一个新的查询集,例如

traffic_2014 = traffic.filter(created__year='2014')

然后,您可以继续过滤日期并使用此新的查询集进行聚合,就像之前一样,或者为每天创建新的查询集,假设您每天聚合多个属性,从而节省了十几个数据库调用。

我希望这会对你有所帮助。

答案 2 :(得分:2)

不直接解决问题,但我认为你应该考虑采用不同的方法。

根据我的理解:

  • 可能经常要求查看。
  • 数据应该很少改变。
  • 需要复杂的数据操作(按年,月,日等汇总字段)。

每次有人请求查看时都不需要执行相同的查询。

一步加载所有数据并在视图内执行操作。您可以使用Pandas之类的库来创建复杂的数据集。该视图现在将受CPU限制,因此请使用Redis之类的缓存系统来避免重新计算。数据更改时无效。

另一种方法:使用Celery之类的任务队列定期执行计算并填充Redis。