web-dev-qa-db-ja.com

Django 1.11 Subquery Aggregateに注釈を付ける

これは最先端の機能であり、現在私は偏っていて、すぐに出血します。既存のクエリセットにサブクエリ集計に注釈を付けたい。 1.11より前にこれを行うと、カスタムSQLを実行するか、データベースをハンマーで操作することになります。 これに関するドキュメントはこちらです 、およびその例:

from Django.db.models import OuterRef, Subquery, Sum
comments = Comment.objects.filter(post=OuterRef('pk')).values('post')
total_comments = comments.annotate(total=Sum('length')).values('total')
Post.objects.filter(length__gt=Subquery(total_comments))

それらは集合体に注釈を付けています

私はこれに苦労しているので、データを持っている最も単純な実世界の例に戻って説明します。 Carparksを多く含むSpacesがあります。つかいます Book→Authorそれがあなたをより幸せにするが、今のところは、Subquery *を使用して関連するモデルの数に注釈を付けたいだけです。

spaces = Space.objects.filter(carpark=OuterRef('pk')).values('carpark')
count_spaces = spaces.annotate(c=Count('*')).values('c')
Carpark.objects.annotate(space_count=Subquery(count_spaces))

これにより、素敵なProgrammingError: more than one row returned by a subquery used as an expressionそして私の頭の中では、このエラーは完全に理にかなっています。サブクエリは、注釈付きの合計を含むスペースのリストを返します。

この例は、ある種の魔法が発生することを示唆しており、使用できる数字になります。しかし、それはここでは起きていませんか?集計サブクエリデータに注釈を付けるにはどうすればよいですか?

うーん、クエリのSQLに何かが追加されています...

新しいCarpark/Spaceモデルを作成し、それが機能しました。したがって、次のステップは、SQLを汚染しているものを特定することです。ローランのアドバイスで、私はSQLを見て、彼らが答えに投稿したバージョンのようにしようと試みました。そして、これは私が本当の問題を見つけた場所です:

SELECT "bookings_carpark".*, (SELECT COUNT(U0."id") AS "c"
FROM "bookings_space" U0
WHERE U0."carpark_id" = ("bookings_carpark"."id")
GROUP BY U0."carpark_id", U0."space"
)
AS "space_count" FROM "bookings_carpark";

ハイライトしましたが、サブクエリのGROUP BY ... U0."space"。何らかの理由で両方を再調整しています。調査は続けられます。

編集2:さて、サブクエリSQLを見るだけで、2番目のグループを見ることができます☹

In [12]: print(Space.objects_standard.filter().values('carpark').annotate(c=Count('*')).values('c').query)
SELECT COUNT(*) AS "c" FROM "bookings_space" GROUP BY "bookings_space"."carpark_id", "bookings_space"."space" ORDER BY "bookings_space"."carpark_id" ASC, "bookings_space"."space" ASC

Edit 3:OK!これらのモデルには両方ともソート順があります。これらはサブクエリに引き継がれています。これらの注文が私のクエリを膨らませ、破壊しているのです。

これはDjangoのバグかもしれませんが、これらの両方のモデルでMeta-order_byを削除するのではなく、私ができる方法はありますかunsortクエリ時のクエリ?


*この例では、カウントに注釈を付けることができます。これを使用する私の本当の目的は、はるかに複雑なフィルターカウントですが、これを動作させることさえできません。

26
Oli

Subqueryのサブクラスを作成して、出力するSQLを変更することもできます。たとえば、次を使用できます。

class SQCount(Subquery):
    template = "(SELECT count(*) FROM (%(subquery)s) _count)"
    output_field = models.IntegerField()

次に、元のSubqueryクラスと同じようにこれを使用します。

spaces = Space.objects.filter(carpark=OuterRef('pk')).values('pk')
Carpark.objects.annotate(space_count=SQCount(spaces))

このトリックを(少なくともpostgresでは)さまざまな集約関数で使用できます。値の配列を構築したり、それらを合計したりするためによく使用します。

30

シャザーム!編集ごとに、追加の列がサブクエリから出力されていました。これは、注文を容易にするためでした(COUNTでは必要ありません)。

モデルから所定のメタオーダーを削除する必要がありました。これを行うには、サブクエリに空の.order_by()を追加するだけです。私のコード用語では:

spaces = Space.objects.filter(carpark=OuterRef('pk')).order_by().values('carpark')
count_spaces = spaces.annotate(c=Count('*')).values('c')
Carpark.objects.annotate(space_count=Subquery(count_spaces))

そしてそれは動作します。素晴らしい。とても迷惑。

37
Oli

私は非常によく似たケースにぶつかりました。そこでは、予約ステータスがキャンセルされないイベントの座席予約を取得する必要がありました。何時間も問題を解明しようとした後、私は問題の根本原因として見たものをここにあります:

序文:これはMariaDB、Django 1.11。

クエリに注釈を付けると、選択したフィールドを含む_GROUP BY_句を取得します(基本的に、values()クエリ選択の内容)。クエリ結果でNULLsまたはNonesを取得している理由をMariaDBコマンドラインツールで調査したところ、_GROUP BY_句がCOUNT()を引き起こすという結論に達しました。 NULLsを返します。

それから、QuerySetインターフェースに飛び込み、DBクエリから_GROUP BY_を手動で強制的に削除する方法を確認し、次のコードを思い付きました。

_from Django.db.models.fields import PositiveIntegerField

reserved_seats_qs = SeatReservation.objects.filter(
        performance=OuterRef(name='pk'), status__in=TAKEN_TYPES
    ).values('id').annotate(
        count=Count('id')).values('count')
# Query workaround: remove GROUP BY from subquery. Test this
# vigorously!
reserved_seats_qs.query.group_by = []

performances_qs = Performance.objects.annotate(
    reserved_seats=Subquery(
        queryset=reserved_seats_qs,
        output_field=PositiveIntegerField()))

print(performances_qs[0].reserved_seats)
_

したがって、基本的に、実行時に_group_by_が付加されないようにするには、サブクエリのクエリセットの_GROUP BY_フィールドを手動で削除/更新する必要があります。また、Djangoは自動的に認識できず、クエリセットの最初の評価で例外を発生させるため、サブクエリの出力フィールドを指定する必要があります。 2番目の評価はそれなしで成功します。

これはDjangoバグ、またはサブクエリの非効率性だと思います。それについてのバグレポートを作成します。

編集: バグレポートはこちら

11
karolyi

私が正しく理解していれば、Spaceで利用可能なCarparksをカウントしようとしています。これに対してサブクエリはやり過ぎだと思われます。古き良き注釈aloneがトリックを行うはずです:

Carpark.objects.annotate(Count('spaces'))

これには、結果にspaces__count値が含まれます。


OK、メモを見ました...

また、手元にある他のモデルでも同じクエリを実行できました。結果は同じなので、例のクエリは問題ないようです(Django 1.11b1でテスト済み):

activities = Activity.objects.filter(event=OuterRef('pk')).values('event')
count_activities = activities.annotate(c=Count('*')).values('c')
Event.objects.annotate(spaces__count=Subquery(count_activities))

あなたの「最も単純な実世界の例」は単純すぎるかもしれません...モデルや他の情報を共有できますか?

3
eillarra

一般的な集計で機能するソリューションは、Django 2.0のWindowクラスを使用して実装できます。これをDjangoトラッカーに追加しましたチケットも。

これにより、(GROUP BY句内の)外部クエリモデルに基づいてパーティションの集計を計算し、そのデータにサブクエリクエリセットのすべての行に注釈を付けることで、注釈付きの値の集計が可能になります。サブクエリは、返された最初の行の集計データを使用し、他の行を無視できます。

Performance.objects.annotate(
    reserved_seats=Subquery(
        SeatReservation.objects.filter(
            performance=OuterRef(name='pk'),
            status__in=TAKEN_TYPES,
        ).annotate(
            reserved_seat_count=Window(
                expression=Count('pk'),
                partition_by=[F('performance')]
            ),
        ).values('reserved_seat_count')[:1],
        output_field=FloatField()
    )
)
3
Melipone

「私のために働く」はあまり役に立ちません。しかし。便利なモデル(_Book -> Author_タイプ)で例を試してみましたが、Django 1.11b1でうまく動作します。

これを正しいバージョンのDjangoで実行していると確信していますか?これはあなたが実行している実際のコードですか?実際にcarparkではなく、より複雑なモデルでこれをテストしていますか?

print(thequery.query)を試して、データベースで実行しようとしているSQLを確認してください。以下は私のモデルで得たものです(あなたの質問に合うように編集されています):

_SELECT (SELECT COUNT(U0."id") AS "c"
FROM "carparks_spaces" U0
WHERE U0."carpark_id" = ("carparks_carpark"."id")
GROUP BY U0."carpark_id") AS "space_count" FROM "carparks_carpark"
_

本当の答えではありませんが、うまくいけば助けになります。

1
Laurent S