6

私が2つのモデルを持っていると仮定します:

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)]
4

2 に答える 2

9

複雑なクエリには、原則としてサブクエリが必要です。考えられる解決策は次のとおりです。

  • 「余分な」クエリセット メソッドまたは生の SQLクエリによって作成されたサブクエリ。これは DRY ではなく、MySQL の一部のバージョンなど、一部の db バックエンドではサポートされていませんでしたが、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 によって生成されたサブクエリがほとんどサポートされていないことが原因です。サブクエリを作成するのは、より複雑なクエリのドキュメントの例だけです。(例: aggregateafterannotateまたはcountafterannotateまたはaggregateafter distinct、ただし after または after はありませんannotate)distinct複雑annotateなネストされた集計は、予期しない 1 つのクエリに簡略化されます。

最初のクエリによってフィルタリングされたすべてのオブジェクトに対して新しい個別の SQL クエリを実行する他のすべてのソリューションは、より優れたソリューションの結果をテストするのに非常に役立ちますが、本番環境では推奨されません。

于 2012-11-09T00:04:17.927 に答える
0

マネージャーにメソッドを追加できます

# 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
于 2012-11-05T19:29:04.830 に答える