【问题标题】:Django - Complex queryDjango - 复杂查询
【发布时间】:2012-10-25 15:53:36
【问题描述】:

假设我有两个模型:

class Profile(models.Model):
    #some fields here

class Ratings(models.Model):
    profile = models.ForeignKey(profile)
    category = models.IntegerField()
    points = models.IntegerField()

假设以下 MySQL 表“评级”示例:

profile    |    category    |    points
   1                1               10
   1                1               4
   1                2               10
   1                3               0
   1                4               10
   1                4               10
   1                4               10
   1                5               0

我的 POST 数据和其他字段值中有以下值:

category_1_avg_val = 7
category_2_avg_val = 5
category_3_avg_val = 5
category_4_avg_val = 7
category_5_avg_val = 9

我想过滤平均评分高于或等于所需值的类别的配置文件。

一些过滤器最初应用为:

q1 = [('associated_with', search_for),
      ('profile_type__slug__exact', profile_type),
      ('gender__in', gender),
      ('rank__in', rank),
      ('styles__style__in', styles),
      ('age__gte', age_from),
      ('age__lte', age_to)]
q1_list = [Q(x) for x in q1 if x[1]]

q2 = [('user__first_name__icontains', search_term),
      ('user__last_name__icontains', search_term),
      ('profile_type__name__icontains', search_term),
      ('styles__style__icontains', search_term),
      ('rank__icontains', search_term)]
q2_list = [Q(x) for x in q2 if x[1]]

if q1_list:
    objects = Profile.objects.filter(
        reduce(operator.and_, q1_list))

if q2_list:
    if objects:
        objects = objects.filter(
            reduce(operator.or_, q2_list))
    else:
        objects = Profile.objects.filter(
            reduce(operator.or_, q2_list))

if order_by_ranking_level == 'desc':
    objects = objects.order_by('-ranking_level').distinct()
else:
    objects = objects.order_by('ranking_level').distinct()

现在我想过滤其(平均点数)(按类别分组)>=(帖子中类别的平均值)的配置文件

我试着一一做到这一点

objects = objects.filter(
    ratings__category=1) \
    .annotate(avg_points=Avg('ratings__points'))\
    .filter(avg_points__gte=category_1_avg_val)


objects = objects.filter(
    ratings__category=2) \
    .annotate(avg_points=Avg('ratings__points'))\
    .filter(avg_points__gte=category_2_avg_val)

但我认为这是错误的。请帮帮我。如果 return 是一个很好的查询集。

已编辑 使用hynekcer 发布的答案,我想出了稍微不同的解决方案,因为我已经查询了需要根据评级进行更多过滤的配置文件集。

def check_ratings_avg(pr, rtd):
    ok = True
    qr = Ratings.objects.filter(profile__id=pr.id) \
        .values('category')\
        .annotate(points_avg=Avg('points'))
    qr = {i['category']:i['points_avg'] for i in qr}

    for cat in rtd:
        val = rtd[cat]
        if qr[cat] >= val:
            pass
        else:
            ok = False
            break
    return ok


rtd = {1: category_1_avg_val, 2: category_2_avg_val, 3: category_3_avg_val,
       4: category_4_avg_val, 5: category_5_avg_val}
objects = [i for i in objects if check_ratings_avg(i, rtd)]

【问题讨论】:

  • 我认为类的名称(模型 Profile 和 Ratings)应该像在 Django 中一样大写。否则字段名称profile很容易被同名的模型混淆。
  • 是的,你说得对,实际上类名在我的 models.py 文件中是大写的。只是忘了在这里做。关于 acuall 问题的任何想法?
  • 如果第一个“黑盒”过滤器会选择数百万个配置文件,您建议的通过编辑添加的第二个解决方案可能会非常慢。运行数百万个 SQL 查询是一场噩梦。尽可能多的应该由一个查询集完成。请问,您对第一个“黑匣子”过滤器有什么看法?它是仅基于一张表(配置文件)还是更多?能不能只写更多filterexclude类型的条件,最终也有Q和F对象条件,但是没有任何聚合函数?请在您的问题中描述这些附加限制的类型。
  • @hynekcer 我已经更新了我的问题,请检查some filters are applied initially as: 问题部分中的黑盒过滤器。如您所见,如果用户只选择free text searchq1_list 将是空的,并且会有很多配置文件。如果q2_list 将被使用,那么配置文件将更少。但仍然不确定会有多少个人资料。所以需要最优解。
  • Conditional Annotations我想你了:(

标签: python django


【解决方案1】:

您可以向经理添加方法

# Untested code
class ProfileManager(models.Manager):
    def with_category_average(self, cat, avg):
        # Give each filter a unique annotation key
        key = 'avg_pts_' + str(cat)
        return self.filter(ratings__category=cat) \
                   .annotate(**{key: Avg('ratings__points')}) \
                   .filter(**{key + '__gte': avg})

    # Expects a dict of `cat: avg` pairs
    def filter_by_averages(self, avg_dict):
        qs = self.get_query_set()
        for key, val in avg_dict.items():
            qs &= self.with_category_average(key, val)
        return qs

【讨论】:

  • 虽然你的代码有一些错误,但是我测试了它,这和我在问题中提到的一样做注释。问题是,以这种方式,查询集将为每个类别单独评估,我需要将所有类别组合在一起。
  • 这一行qs.with_category_average(key, val)应该是self.with_category_average(key, val)
  • 嗯,这应该是你所说的 AND,但我发现我对我的 ChainableManagers 太习惯了。这个带有&=的版本怎么样?
  • 感谢您的帮助,但它仍然产生错误的结果,不应该在查询集中的对象仍然存在:(
  • 当一个配置文件满足条件 >= 对于所有 5 个评级类别,那么它应该出现在查询集中,如果任何类别平均值不符合条件 >= 它应该被排除,这不会发生目前
【解决方案2】:

您的复杂查询原则上需要一个子查询。可能的解决方案是:

  • 'extra' queryset methodraw SQL 编写的子查询 查询。它不是 DRY,并且某些数据库后端不支持它,例如在某些版本的 MySQL 中,子查询从 Django 1.1 开始以某种有限的方式使用。
  • 将中间结果保存到数据库中的临时表中。这在 Django 中并不好。
  • 在 Python 中通过循环模拟外部查询。最好的通用解决方案。在 Python 中对由第一个查询聚合的数据库数据进行循环可以足够快地聚合和过滤数据。

A) Python 模拟的子查询

from django.db.models import Q, Avg
from itertools import groupby
from myapp.models import Profile, Ratings

def iterator_filtered_by_average(dictionary):
    qr = Ratings.objects.values('profile', 'category', 'points').order_by(
            'profile', 'category').annotate(points_avg=Avg('points'))
    f = Q()
    for k, v in dictionary.iteritems():
        f |= Q(category=k, points_avg__gte=v)
    for profile, grp in groupby(qr.filter(f).values('profile')):
        if len(list(grp)) == len(dictionary):
            yield profile

#example
FILTER_DATA = {1:category_1_avg_val, 2:category_2_avg_val, 3:category_3_avg_val,
               4:category_4_avg_val, 5:category_5_avg_val}
for row in iterator_filtered_by_average(FILTER_DATA):
    print row

这是原始问题的简单解决方案,无需后续额外要求。

B) 带有子查询的解决方案
对于更详细的问题版本是必要的,因为如果初始过滤器基于 ManyToManyField 类型的某些字段,并且还因为它包含 distinct 子句:

# objects:  QuerySet that you get from your initial filters. Not yet executed.
if rtd:
    # Method `as_nested_sql` removes the `order_by` clase, unlike `as_sql`
    subquery3 = objects.values('id').query \
            .get_compiler(connection=connection).as_nested_sql()
    subquery2 = ("""SELECT profile_id, category, avg(points) AS points_avg
          FROM myapp_ratings
          WHERE profile_id in
          ( %s
          ) GROUP BY profile_id, category
            """ % subquery3[0], subquery3[1]
    )
    where_sql = ' OR '.join(
            'category = %d AND points_avg >= %%s' % cat for cat in rtd.keys()
    )
    subquery = (
        """SELECT profile_id
        FROM
        ( %s
        ) subquery2
        WHERE %s
        GROUP BY profile_id
        HAVING count(*) = %s
        """ % (subquery2[0], where_sql, len(rtd)),
        subquery2[1] + tuple(rtd.values())
    )
    assert order_by_ranking_level in ('asc', 'desc')
    mainquery = ("""SELECT myapp_profile.* FROM myapp_profile
      INNER JOIN
      ( %s
      ) subquery ON subquery.profile_id=myapp_profile.id
      ORDER BY ranking_level %s"""
        % (subquery[0], order_by_ranking_level), subquery[1]
    )
    objects = Profile.objects.raw(mainquery[0], params=mainquery[1])
return objects

请将所有字符串 myapp 替换为 name_of_your_application

此代码生成的 SQL 示例

SELECT myapp_profile.* FROM myapp_profile
  INNER JOIN
  ( SELECT profile_id
    FROM
    ( SELECT profile_id, category, avg(points) AS points_avg
      FROM myapp_ratings
      WHERE profile_id IN
      ( SELECT U0.`id` FROM `myapp_profile` U0 WHERE U0.`ranking_level` >= 4
      ) GROUP BY profile_id, category
    ) subquery2
    WHERE category = 1 AND points_avg >= 7 OR category = 2 AND points_avg >= 5
       OR category = 3 AND points_avg >= 5 OR category = 4 AND points_avg >= 7
       OR category = 5 AND points_avg >= 9
    GROUP BY profile_id
    HAVING count(*) = 5
  ) subquery ON subquery.profile_id=myapp_profile.id
  ORDER BY ranking_level asc

(此 SQL 是为了提高可读性,手动解析字符串 %s 并替换为参数,但出于安全原因,数据库引擎接收未解析的参数。)


您的问题是由于对 Django 生成的子查询的支持很少。只有来自更复杂查询文档的示例才会创建子查询。 (例如,aggregateannotate 之后或countannotateaggregatedistinct 之后,但在distinctannotate 之后没有annotate)复杂的嵌套聚合被简化为一个查询出乎意料。

对于生产环境中不鼓励为第一个查询过滤的每个对象执行新的单个 SQL 查询的所有其他解决方案,尽管它们对于测试任何更好解决方案的结果非常有用。

【讨论】:

  • 在效率方面有多好Emulation of outer query by loop in python??考虑 Profile 表中的 500 万条记录和 Rating 表中的 5*500 万行?
  • 速度取决于参数category_*_avg_val 的值,这会极大地影响单个条件的选择性(例如,您主要选择 90% 或 10% 的配置文件?)以及一个配置文件的典型评级数(个人资料的评分略多于一个或数百个)。最好在预期数据上比较模拟和原始查询解决方案。 (对我来说并不像现在对你那么简单。我在几个小时前删除了测试应用程序。)
  • 实际上这个查询会出现在搜索引擎中,所以我必须选择所有遵循所有参数的配置文件。我的做法与您的方法略有不同,因为我已经过滤了配置文件的查询集,我需要过滤更多。
  • 请检查我更新的问题,我添加了一个解决方案,但我不知道它对效率有多大影响。
  • 显然,如果我没有得到任何最佳解决方案,我会选择你的答案:)
猜你喜欢
  • 2011-06-08
  • 1970-01-01
  • 1970-01-01
  • 2011-10-20
  • 2013-06-09
  • 2011-10-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多