web-dev-qa-db-ja.com

DjangoのGROUP BYの注釈の集約

[〜#〜] update [〜#〜]

投稿された回答のおかげで、問題を定式化するはるかに簡単な方法を見つけました。元の質問は改訂履歴で見ることができます。

問題

SQLクエリをDjangoに変換しようとしていますが、理解できないエラーが発生しています。

Django私が持っているモデル:

class Title(models.Model):
  title_id = models.CharField(primary_key=True, max_length=12)
  title = models.CharField(max_length=80)
  publisher = models.CharField(max_length=100)
  price = models.DecimalField(decimal_places=2, blank=True, null=True)

次のデータがあります。

publisher                    title_id      price  title
---------------------------  ----------  -------  -----------------------------------
New Age Books                PS2106         7     Life Without Fear
New Age Books                PS2091        10.95  Is Anger the Enemy?
New Age Books                BU2075         2.99  You Can Combat    Computer Stress!
New Age Books                TC7777        14.99  Sushi, Anyone?
Binnet & Hardley             MC3021         2.99  The Gourmet Microwave
Binnet & Hardley             MC2222        19.99  Silicon Valley   Gastronomic Treats
Algodata Infosystems         PC1035        22.95  But Is It User Friendly?
Algodata Infosystems         BU1032        19.99  The Busy Executive's   Database Guide
Algodata Infosystems         PC8888        20     Secrets of Silicon Valley

私がやりたいことは次のとおりです。価格の2倍の注釈付きフィールドdbl_priceを導入し、結果のクエリセットをpublisherでグループ化し、各出版社について、その出版社のすべてのタイトルのすべてのdbl_price値の合計を計算します出版社。

これを行うSQLクエリは次のとおりです。

SELECT SUM(dbl_price) AS total_dbl_price, publisher
FROM (
  SELECT price * 2 AS dbl_price, publisher
  FROM title
) AS A 
GROUP BY publisher

望ましい出力は次のとおりです。

publisher                    tot_dbl_prices
---------------------------  --------------
Algodata Infosystems                 125.88
Binnet & Hardley                      45.96
New Age Books                         71.86 

Djangoクエリ

クエリは次のようになります。

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(tot_dbl_prices=Sum('dbl_price'))

エラーが発生します:

KeyError: 'dbl_price'. 

これは、クエリセットでフィールドdbl_priceが見つからないことを示します。

エラーの理由

このエラーが発生する理由は次のとおりです。 ドキュメントには

また、返される値のリストにaverage_ratingが明示的に含まれていることにも注意してください。これは、values()およびannotate()句の順序のために必要です。

Values()句がannotate()句の前にある場合、注釈は結果セットに自動的に追加されます。ただし、values()句がannotate()句の後に適用される場合、集計列を明示的に含める必要があります。

したがって、dbl_priceは、以前のannotateによって作成されたが、values()には含まれていなかったため、集約で見つかりませんでした。

ただし、values(その後に別のvaluesが続く)をグループ化デバイスとして使用するため、annotateにも含めることはできません。

Values()句がannotate()の前にある場合、values()句で記述されたグループ化を使用して注釈が計算されます。

これはDjango がSQL GROUP BY を実装する方法の基礎です。これは、values()の中にdbl_priceを含めることができないことを意味します。なぜなら、グループ化はpublisherdbl_priceの両方のフィールドの一意の組み合わせに基づいているのに対し、publisherのみでグループ化する必要があるからです。

したがって、次のクエリは、注釈付きのdbl_priceフィールドではなく、モデルのpriceフィールドを集計するという点でのみ上記と異なり、実際に機能します。

Title.objects
 .annotate(dbl_price=2*F('price'))
 .values('publisher')
 .annotate(sum_of_prices=Count('price'))

priceフィールドは注釈付きフィールドではなくモデル内にあるため、クエリセットに保持するためにvaluesに含める必要はありません。

質問

したがって、ここにあります:注釈セットのプロパティをクエリセットに保持するためにvaluesに含める必要がありますが、valuesはグループ化にも使用されるため、できません(これは余分なフィールドが間違っています)。問題は本質的に、コンテキストに応じて、valuesがDjangoで使用される2つの非常に異なる方法によるものです(valuesの後にannotateが続くかどうか)- (1)値抽出(SQLプレーンSELECTリスト)および(2)グループ化+グループの集約(SQL GROUP BY)-この場合、これら2つの方法は競合するようです。

私の質問は:この問題を解決する方法はありますか(生のSQLにフォールバックするようなことなしに)?

注意:問題の特定の例は、いくつかの回答で指摘されたannotateの後にすべてのvaluesステートメントを移動することで解決できます。ただし、3つの理由から、values()の前にannotateステートメントを保持するソリューション(または議論)に興味があります。1。より複雑な例もあります。推奨される回避策は機能しません。 2.注釈付きクエリセットが別の関数に渡され、実際にGROUP BYが実行される状況を想像できます。そのため、注釈付きフィールドの名前とその型のセットしかわかりません。 3.状況は非常に単純であるように思われ、values()の2つの異なる使用法のこの衝突が以前に気づかれず、議論されていなかった場合、私は驚きます。

24
Leonid Shifrin

これは少し遅すぎるかもしれませんが、解決策を見つけました(Django 1.11.1でテスト済み))。

問題は、グループ化を提供するために必要な.values('publisher')を呼び出すと、.values()fields paramに含まれないすべての注釈が削除されることです。

また、_dbl_price_をfields paramに含めることはできません。別の_GROUP BY_ステートメントが追加されるためです。

注釈付きフィールドを最初に必要とするすべての集計を作成するソリューションは、.values()を呼び出し、その集計をfields paramに含めます(これは_GROUP BY_を追加しませんそれらは集約です)。次に、任意の式で.annotate()を呼び出す必要があります。これにより、クエリ内の非集計フィールドのみを使用して、SQLクエリに_GROUP BY_ステートメントを追加するDjango パブリッシャー

_Title.objects
    .annotate(dbl_price=2*F('price'))
    .annotate(sum_of_prices=Sum('dbl_price'))
    .values('publisher', 'sum_of_prices')
    .annotate(titles_count=Count('id'))
_

このアプローチの唯一のマイナス点-注釈付きフィールドを使用するもの以外の他の集計が必要ない場合は、とにかくいくつかを含める必要があります。 .annotate()への最後の呼び出しがなければ(そして、少なくとも1つの式を含める必要があります!)、DjangoはSQLクエリに_GROUP BY_を追加しません。これに対処する1つのアプローチはフィールドのコピーを作成するには:

_Title.objects
    .annotate(dbl_price=2*F('price'))
    .annotate(_sum_of_prices=Sum('dbl_price')) # note the underscore!
    .values('publisher', '_sum_of_prices')
    .annotate(sum_of_prices=F('_sum_of_prices')
_

また、QuerySetの順序に注意する必要があることにも言及してください。 .order_by()を呼び出して、パラメーターを指定せずに順序をクリアするか、_GROUP BY_フィールドを使用することをお勧めします。結果のクエリに他のフィールドによる順序付けが含まれる場合、グループ化は間違っています。 https://docs.djangoproject.com/en/1.11/topics/db/aggregation/#interaction-with-default-ordering-or-order-by

また、その偽の注釈を出力から削除したい場合がありますので、再度.values()を呼び出してください。したがって、最終的なコードは次のようになります。

_Title.objects
    .annotate(dbl_price=2*F('price'))
    .annotate(_sum_of_prices=Sum('dbl_price'))
    .values('publisher', '_sum_of_prices')
    .annotate(sum_of_prices=F('_sum_of_prices')
    .values('publisher', 'sum_of_prices')
    .order_by('publisher')
_
16

これは、Djangoの group_byの動作 の方法から予想されます。すべての注釈付きフィールドは、GROUP BY句に追加されます。ただし、このように書かれた理由についてはコメントできません。

クエリを次のように動作させることができます。

Title.objects
  .values('publisher')
  .annotate(total_dbl_price=Sum(2*F('price'))

次のSQLを生成します。

SELECT publisher, SUM((2 * price)) AS total_dbl_price
FROM title
GROUP BY publisher

ちょうどあなたのケースで動作します。

私はこれがあなたが探していた完全な解決策ではないかもしれないと理解していますが、 CombinedExpressions (私は願っています!).

3
user2485594

問題はvalues()に続き、annotate()が原因です。順序が重要です。これは、[注釈と値の句の順序]( https://docs.djangoproject.com/en/1.10/topics/db/aggregation/#order-of-annotate-and-values-節

.values('pub_id')pub_idでクエリセットフィールドを制限します。したがって、incomeに注釈を付けることはできません

Values()メソッドは、オプションの位置引数* fieldsを取ります。これは、SELECTを制限するフィールド名を指定します。

2
Wilfried

@alexandrによるこのソリューションは、適切に対処します。

https://stackoverflow.com/a/44915227/6323666

必要なものはこれです:

from Django.db.models import Sum

Title.objects.values('publisher').annotate(tot_dbl_prices=2*Sum('price'))

理想的には、最初にそれらを合計してから2倍にして、ここでシナリオを逆にしました。あなたはそれを2倍にして、合計しようとしました。これでいいことを願っています。

1
Thulasi Ram