集約(Aggregation)

revision-up-to:17812 (1.4)

このトピックは Djangoのデータベース抽象API において個別のオブジェクトの作成、取得、削除を行うDjangoのクエリの使い方で 説明されています。しかし、オブジェクトのコレクションの合計を取ったり、 集約 したりすることで引き出された値を取得しなければならないことがあります。 このトピックガイドはDjangoクエリを使って集約値がどうやって生成されたり 返されたりするのかを解説します。

このガイドでは以下のモデルを使います。これらモデルはオンライン書店の一連 の在庫を管理するために使われます。

class Author(models.Model):
   name = models.CharField(max_length=100)
   age = models.IntegerField()
   friends = models.ManyToManyField('self', blank=True)

class Publisher(models.Model):
   name = models.CharField(max_length=300)
   num_awards = models.IntegerField()

class Book(models.Model):
   isbn = models.CharField(max_length=9)
   name = models.CharField(max_length=300)
   pages = models.IntegerField()
   price = models.DecimalField(max_digits=10, decimal_places=2)
   rating = models.FloatField()
   authors = models.ManyToManyField(Author)
   publisher = models.ForeignKey(Publisher)
   pubdate = models.DateField()

class Store(models.Model):
   name = models.CharField(max_length=300)
   books = models.ManyToManyField(Book)

チートシート

お急ぎですか? 上のモデルを使った場合の一般的な集約クエリは以下のようになり ます

# 書籍の合計数
>>> Book.objects.count()
2452

# publisher=BaloneyPress の場合の書籍の合計数
>>> Book.objects.filter(publisher__name='BaloneyPress').count()
73

# 全書籍の平均価格
>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg('price'))
{'price__avg': 34.35}

# 最も高額な書籍
>>> from django.db.models import Max
>>> Book.objects.all().aggregate(Max('price'))
{'price__max': Decimal('81.20')}

# 出版社ごとの書籍数を "num_books"属性で
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count('book'))
>>> pubs
[<Publisher BaloneyPress>, <Publisher SalamiPress>, ...]
>>> pubs[0].num_books
73

# トップ5の出版社を書籍数の多い順に
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count('book')).order_by('-num_books')[:5]
>>> pubs[0].num_books
1323

QuerySetに対して集約を生成する

Djangoでは集約を生成するのに2つの方法が用意されています。最初の方法は、 全 QuerySet に対して合計値を生成する物です。例えば、売り出されている 全ての書籍の平均値を計算したい場合などです。Djangoのクエリ文法では、全書籍 セットを表現するには以下の方法が用意されています

>>> Book.objects.all()

必要なのはこの QuerySet に含まれるオブジェクトに関しての合計値を計算 する方法です。これは aggreagte() 句を QuerySet に加えることで 行われます。

>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg('price'))
{'price__avg': 34.35}

all() はこの例では冗長なので、 次のように簡素化できます

>>> Book.objects.aggregate(Avg('price'))
{'price__avg': 34.35}

aggregate() 句への引数は計算したい集約値を表します – この例では、 Book モデルの price フィールドの平均になります。 利用可能な集約関数の一覧は QuerySet リファレンス にあります。

aggregate()QuerySet の最後の句になります。それが呼び出されると、 name-valueペアの辞書が返されます。nameは集約値に対する識別子です;valueは 計算された集約値です。 name はフィールド名と集約関数より自動的に生成されます。 集約値の名前を手動で指定したいのであれば、集約句を指定する際にその名前を 指定します

>>> Book.objects.aggregate(average_price=Avg('price'))
{'average_price': 34.35}

1つ以上の集約を生成したいのであれば、 aggregate() 句に別の引数を 追加するだけです。よって、全書籍の最高の定価と最低の定価を知りたいのであれば 、次のクエリを発行します

>>> from django.db.models import Avg, Max, Min, Count
>>> Book.objects.aggregate(Avg('price'), Max('price'), Min('price'))
{'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}

QuerySetの各アイテムを集約する(注釈付け)

サマリ値を生成する2つ目の方法は QuerySet の各オブジェクトに対して 個別のサマリを生成することです。例えば、書籍の一覧を取得しているとすれば、 それぞれの書籍に寄稿している著者が何名いるのかを知りたいこともあるでしょう。 各書籍は Author に対して many-to-many の関係を持っています;この QuerySet での関連をサマリ化できます。

オブジェクトごとのサマリは annotate() 句を使うことで生成することができ ます。 annotate() が指定されると、 QuerySet の各オブジェクトは 指定された値で注釈付け( annotate )されます。

これらの注釈付け ( annotation ) 構文は aggregate() 句で使われる物と 全く同じです。 annotate() への各引数は計算される注釈付けを意味します。 例えば、Bookを著者数で注釈付けするには

# 注釈付けされるクエリセットを組み立てる
>>> q = Book.objects.annotate(Count('authors'))
# クエリセットの最初のオブジェクトを取得
>>> q[0]
<Book: The Definitive Guide to Django>
>>> q[0].authors__count
2
# クエリセットの2番目のオブジェクト取得
>>> q[1]
<Book: Practical Django Projects>
>>> q[1].authors__count
1

aggregate() と同様に, 注釈(annotation)の名前は集約関数の名前と集約される フィールド名で自動的に作成されます。 注釈付けを指定する時にエイリアス名を 指定すると、このデフォルトの名前をオーバーライドすることができます

>>> q = Book.objects.annotate(num_authors=Count('authors'))
>>> q[0].num_authors
2
>>> q[1].num_authors
1

aggregate() とは違って, annotate() は最終句では ありませんannotate() 句の出力は QuerySet です; この QuerySet は他の QuerySet オペレーションによって修正可能です。 filter()order_by などが使え、また別の annotate() 呼び出しさえも可能です。

Joinと集約

ここまではクエリを受けるモデルに帰属するフィールドに対して集約することに 関して扱ってきました。しかし、集約したい値が、クエリを受けるモデルの関連 するモデルに属することもあります。

集約関数の中で集約したいフィールドを指定するときには、 Djangoではフィルタ において、関連フィールド参照するときにつかわれるのと同じ 二重アンダースコア表記 を使うことことができ ます。 そうすれば Django は関連した値を取得して集約するために必要な任意 のテーブルをjoinしてくれます。

例えば、それぞれのお店で提供されている書籍の価格帯を見つけるにはこのような 注釈付けを行うと良いでしょう

>>> Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))

こうすると Django は Store モデルを取得し( many-to-many 関連を通じて ) Bookモデルと join し、書籍のpriceフィールドに関して注釈付けして最小値と 最大値を生成してくれます。

同じルールは aggregate() 句にも適用されます。あるお店で売られている 任意の書籍の最安値と最高値を知りたいのであれば、次のような集約を使います:

>>> Store.objects.aggregate(min_price=Min('books__price'), max_price=Max('books__price'))

Joinのチェーンは必要なだけ深くできます。例えば、売り出し中の任意の書籍の 中で最も若い著者の年齢を取得するには、次のようなクエリを発行します

>>> Store.objects.aggregate(youngest_age=Min('books__authors__age'))

集約と他のQuerySet句

filter()exclude()

集約はフィルタに参加させることができます。 任意の filter() (あるいは exclude() ) を通常のフィールドに適用すれば、集約したいオブジェクトの 制約をさせる効果が得られます。

annotate() 句を使うと、 フィルタは注釈付け計算したいオブジェクト を制限できるようになります。例えば、次のクエリを使うと表題が “Django”で 始まる全ての書籍に関しての注釈付けリストを生成することが出来ます

>>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors'))

aggregate() 句を使うと集約計算したいオブジェクトを制限できるように なります。例えば、次のクエリを使うと表題が”Django”で始まる全ての書籍の 平均価格を生成することができます

>>> Book.objects.filter(name__startswith="Django").aggregate(Avg('price'))

注釈付けに対するフィルタリング

注釈付けされた値もフィルターできます。 他のモデルフィールドと同じ方法で、 filter()exclude() 句で注釈付けのエイリアスを使うことで来ます。

例えば、作家が一人以上いる書籍の一覧を生成するには、次のクエリを発行します:

>>> Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=1)

このクエリは注釈付けした結果セットを生成して、その注釈付けを元にした フィルタを生成します。

annotate() 句と filter() 句の順番

annotate() 句と fitler() 句 の両方を含んだ複雑なクエリを開発している のであれば、これらの句がその QuerySet に適用される順番に付いて特に注意 を払うべきです。

クエリに annotate() 句が適当される時は、 注釈付けはそれが要求された時点 までのクエリの状態に関して注釈付けが行われます。 これが実際に意味することは、 filter()annotate() は代替可能な 操作ではないということです – すなわち、クエリ間で結果の差があるということ です。

>>> Publisher.objects.annotate(num_books=Count('book')).filter(book__rating__gt=3.0)

と次のクエリです

>>> Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count('book'))

2つのクエリは少なくとも1つの良い書籍(すなわち評価が 3.0を超えた書籍) があるPublisher のリストを返します。しかし、最初のクエリの注釈付けでは 出版社が出版する全ての書籍の総数を返します;2番目のクエリは注釈付けされた 件数での良い本の一覧を返します。最初のクエリでは、フィルタの前に注釈付け されるので、フィルタは注釈付けに何の影響も与えません。2番目のクエリでは フィルタが注釈付けの前に行われるので、注釈付け計算が行われるべきオブジェクト をフィルタが制限します。

order_by()

注釈付けはソート順( ordering ) に基づいて行うことが可能です。 order_by() 句を定義すると、提供する注釈付けがクエリの中で annoteate() 句の部分と して定義したエイリアスを参照することができます。

例えば、書籍に寄稿した著者の数で書籍の QuerySet を並べ替えるには 次のクエリを使います。:

>>> Book.objects.annotate(num_authors=Count('authors')).order_by('num_authors')

values()

本来、注釈付けはオブジェクト毎に行われるのが基本です。– 注釈付けされる QuerySet は元の QuerySet での各オブジェクトに対して1つの結果を 返します。しかし、結果セットで返されるカラムを制限するために values() が使われると、注釈付けの評価方法少しかわります。元の QuerySet の 各結果に対して注釈付けの結果を返すのではなく、 元の結果が values() 句 で指定されたフィールドのユニークな組み合わせに対してグループ化されます。 そして注釈付けが各ユニークグループに対して行われます;注釈付けはグループの 全てのメンバに対して計算されるのです。

例えば、それぞれの著者によって書かれた書籍の平均評価を見つけるために著者の クエリを次のように考えてください

>>> Author.objects.annotate(average_rating=Avg('book__rating'))

これはデータベースにある各著者に対して、彼らの平均書籍評価で注釈付けして 1つの結果を返します。

しかし、 values() 句を使えばこの結果は少し異なります

>>> Author.objects.values('name').annotate(average_rating=Avg('book__rating'))

この例では、 著者が name でグループ化されるので、各 ユニークな 著者の name に対して注釈付けされた値のみが取得されます。つまり、同じ名前の二人の著者が 居ると、1つのクエリの結果に結合されます;平均は二人の著者で書かれた本の平均 として計算されるのです。

annotate() 句と values() 句の順番

filter() 句を使うと、 クエリに適用される annotate() 句と values() 句の順番が重要です。 values() 句が annotate() 句の前にあれば、注釈 付けは values() 句で表現されたグループ化を使って計算されます。

しかし、 annotate() 句が values() の前にあると、注釈付けは全 クエリセットに対して生成されます。この場合、 values() 句は出力に生成 されるフィールドのみを制限します。

例えば、 values() 句と annotate() 句の順番を前の例で反対にすると:

>>> Author.objects.annotate(average_rating=Avg('book__rating')).values('name', 'average_rating')

これは各著者に対する1つのユニークな結果を生成します;しかし、著者の name と average_ration の注釈付けのみが出力データに返されます。

average_ragint が返されるべき値リストに明示的に含まれていることも注目 すべきです。これは values() 句と annotate() 句の順番の為に必要なの です。

もしも values() 句が annotate() 句の前にあるならば、注釈付けは 自動的に結果セットに加えられます。しかし、 values() 句が annotate() 句の後に加えられるのであれば、明示的に注釈付けカラムを含める必要があります。

デフォルトのソート順あるいは order_by() との相互作用

クエリセットの order_by() で示されたフィールド(もしくはモデルに対する デフォルトのソート順で使われるフィールド)はたとえ values() の呼び出しで 指定されていなかったとしても出力データを選択する時には使われます。 このような追加フィールドは”like” な結果を一緒にグループかするのに使われたり、 同じ結果行を分離するのに使われたりします。 特に、件数を数えるときに使われます。

例えば、 このようなモデルがあるとします

class Item(models.Model):
    name = models.CharField(max_length=10)
    data = models.IntegerField()

    class Meta:
        ordering = ["name"]

ここでの重要な部分は、 name フィールドのデフォルトのソート順です。 もしも、ユニークな data の値が何回現れるのかを数えたいのであれば、この ようにしてもいいでしょう

# 警告: 完全に正しい訳ではない!
Item.objects.values("data").annotate(Count("id"))

...こうすると共通の data の値で Item オブジェクトがグループ化され て、各グループの id の値の件数が数えられます。ただし、完全には動作しま せん。 name のデフォルトのソート順はグルーピングでも有効ですので、この クエリはユニークは (data,name) ペアでグループされてしまい、求める結果 とは異なってしまいます。代わりに次のクエリセットを作るべきです

Item.objects.values("data").annotate(Count("id")).order_by()

...このクエリでは任意のソート順を明らかにしています。 つまり data を使って 有害な影響なく並べ替えることができました。クエリの中でその役目を既に果たして いるからです。

この振る舞いは distinct() に対する クエリセットのドキュメントに書かれていることと同じで、一般ルールも同じです。 :普通は結果の中で追加フィールドが役目を果たすことはないので、ソート順を明確 にしないならば、少なくとも values() 呼び出しで選択するこれらのフィールドに 対して制限するようにしないといけません。

Note

Django が無関係のカラムを取り除いてくれないのはなぜなのか?と思うのも もっともです。主な理由は distinct() や他の場所との一貫性です : Django は指定されたそーと順制約を 決して 取り除くことはしません ( 他のメソッドの振る舞いも変更することができません。 API の安定性 にあるポリシーに違反するからです )

注釈付けを集約する

注釈付けの結果を集約することもできます。 aggregate() 句を定義する時は その集約は、クエリの annotate() の部分で定義された任意のエイリアスを 参照することができます。

例えば、書籍とに著者数の平均を計算したいのであれば、まず書籍の集合に対して 著者数で注釈付けし、それから注釈フィールドを参照して著者数を集約します。

>>> Book.objects.annotate(num_authors=Count('authors')).aggregate(Avg('num_authors'))
{'num_authors__avg': 1.66}