Pengumpulan¶
Panduan topik pada Django's database-abstraction API menggambarkan cara dimana anda dapat menggunakan permintaan Django yang membuat, mengambil, memperbaharui dan menghapus obyek tersendiri. Bagaimanapun, terkadang anda akan butuh mengambil nilai yang berasal oleh meringkas atau*mengumpulkan* kumpulan dari obyek. Topik panduan ini menggambarkan cara yang mengumpulkan nilai-nilai dapat dibangkitkan dan dikembalikan menggunakan permintaan Django.
Sepanjang panduan ini, kami akan mengacu ke model berikut. Model-model ini digunakan untuk melacak inventaris untuk rangkaian dari toko buku daring:
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
age = models.IntegerField()
class Publisher(models.Model):
name = models.CharField(max_length=300)
class Book(models.Model):
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, on_delete=models.CASCADE)
pubdate = models.DateField()
class Store(models.Model):
name = models.CharField(max_length=300)
books = models.ManyToManyField(Book)
Lembar curang¶
Terburu-buru? Ini adalah bagaimana melakukan permintaan pengumpulan umum, menganggap model diatas:
# Total number of books.
>>> Book.objects.count()
2452
# Total number of books with publisher=BaloneyPress
>>> Book.objects.filter(publisher__name="BaloneyPress").count()
73
# Average price across all books, provide default to be returned instead
# of None if no books exist.
>>> from django.db.models import Avg
>>> Book.objects.aggregate(Avg("price", default=0))
{'price__avg': 34.35}
# Max price across all books, provide default to be returned instead of
# None if no books exist.
>>> from django.db.models import Max
>>> Book.objects.aggregate(Max("price", default=0))
{'price__max': Decimal('81.20')}
# Difference between the highest priced book and the average price of all books.
>>> from django.db.models import FloatField
>>> Book.objects.aggregate(
... price_diff=Max("price", output_field=FloatField()) - Avg("price")
... )
{'price_diff': 46.85}
# All the following queries involve traversing the Book<->Publisher
# foreign key relationship backwards.
# Each publisher, each with a count of books as a "num_books" attribute.
>>> from django.db.models import Count
>>> pubs = Publisher.objects.annotate(num_books=Count("book"))
>>> pubs
<QuerySet [<Publisher: BaloneyPress>, <Publisher: SalamiPress>, ...]>
>>> pubs[0].num_books
73
# Each publisher, with a separate count of books with a rating above and below 5
>>> from django.db.models import Q
>>> above_5 = Count("book", filter=Q(book__rating__gt=5))
>>> below_5 = Count("book", filter=Q(book__rating__lte=5))
>>> pubs = Publisher.objects.annotate(below_5=below_5).annotate(above_5=above_5)
>>> pubs[0].above_5
23
>>> pubs[0].below_5
12
# The top 5 publishers, in order by number of books.
>>> pubs = Publisher.objects.annotate(num_books=Count("book")).order_by("-num_books")[:5]
>>> pubs[0].num_books
1323
Membangkitkan pengumpulan terhadap QuerySet
¶
Django provides two ways to generate aggregates. The first way is to generate
summary values over an entire QuerySet
. For example, say you wanted to
calculate the average price of all books available for sale. Django's query
syntax provides a means for describing the set of all books:
>>> Book.objects.all()
What we need is a way to calculate summary values over the objects that
belong to this QuerySet
. This is done by appending an aggregate()
clause onto the QuerySet
:
>>> from django.db.models import Avg
>>> Book.objects.all().aggregate(Avg("price"))
{'price__avg': 34.35}
The all()
is redundant in this example, so this could be simplified to:
>>> Book.objects.aggregate(Avg("price"))
{'price__avg': 34.35}
Argumen pada klausa aggregate()
menggambarkan nilai pengumpulan yang kami ingin hitung - dalam kasus ini, rata-rata dari bidang price
pada model Book
. Sebuah daftar dari fungsi pengumpulan yang tersedia dapat ditemukan di QuerySet reference.
aggregate()
is a terminal clause for a QuerySet
that, when invoked,
returns a dictionary of name-value pairs. The name is an identifier for the
aggregate value; the value is the computed aggregate. The name is
automatically generated from the name of the field and the aggregate function.
If you want to manually specify a name for the aggregate value, you can do so
by providing that name when you specify the aggregate clause:
>>> Book.objects.aggregate(average_price=Avg("price"))
{'average_price': 34.35}
If you want to generate more than one aggregate, you add another argument to
the aggregate()
clause. So, if we also wanted to know the maximum and
minimum price of all books, we would issue the query:
>>> from django.db.models import Avg, Max, Min
>>> Book.objects.aggregate(Avg("price"), Max("price"), Min("price"))
{'price__avg': 34.35, 'price__max': Decimal('81.20'), 'price__min': Decimal('12.99')}
Membangkitkan pengumpulan untuk setiap barang di QuerySet
¶
The second way to generate summary values is to generate an independent
summary for each object in a QuerySet
. For example, if you are
retrieving a list of books, you may want to know how many authors contributed
to each book. Each Book has a many-to-many relationship with the Author; we
want to summarize this relationship for each book in the QuerySet
.
Per-object summaries can be generated using the
annotate()
clause. When an annotate()
clause is
specified, each object in the QuerySet
will be annotated with the
specified values.
The syntax for these annotations is identical to that used for the
aggregate()
clause. Each argument to annotate()
describes
an aggregate that is to be calculated. For example, to annotate books with the
number of authors:
# Build an annotated queryset
>>> from django.db.models import Count
>>> q = Book.objects.annotate(Count("authors"))
# Interrogate the first object in the queryset
>>> q[0]
<Book: The Definitive Guide to Django>
>>> q[0].authors__count
2
# Interrogate the second object in the queryset
>>> q[1]
<Book: Practical Django Projects>
>>> q[1].authors__count
1
As with aggregate()
, the name for the annotation is automatically derived
from the name of the aggregate function and the name of the field being
aggregated. You can override this default name by providing an alias when you
specify the annotation:
>>> q = Book.objects.annotate(num_authors=Count("authors"))
>>> q[0].num_authors
2
>>> q[1].num_authors
1
Tidak seperti aggregate()
, annotate()
adalah bukan klausa terminal. Keluaran dari klausa annotate()
adalah sebuah QuerySet
; QuerySet
ini dapat dirubah menggunakan apapun selain operasi QuerySet
, termasuk filter()
, order_by()
, atau bahkan tambahan panggilan pada annotate()
.
Memadukan banyak pengumpulan¶
Combining multiple aggregations with annotate()
will yield the
wrong results because joins are used instead of subqueries:
>>> book = Book.objects.first()
>>> book.authors.count()
2
>>> book.store_set.count()
3
>>> q = Book.objects.annotate(Count('authors'), Count('store'))
>>> q[0].authors__count
6
>>> q[0].store__count
6
Untuk kebanyakan pengumpulan, tidak ada jalan menghindari masalah ini, bagaimanapun, pengumpulan Count
mempunai sebuah parameter distinct
yang mungkin membantu:
>>> q = Book.objects.annotate(Count('authors', distinct=True), Count('store', distinct=True))
>>> q[0].authors__count
2
>>> q[0].store__count
3
Jika ragu, periksa permintaan SQL!
Untuk memahami apa yang terjadi di permintaan anda, pertimbangkan memeriksa sifat query
dari QuerySet
anda.
Gabung dan kumpulkan¶
Sejauh ini, kami telah berurusan dengan pengumpulan terhadap bidang yang milik ke model sedang diminta. Bagaimanapun, terkadang nilai anda ingin kumpulkan akan milik ke model yang terkait ke model anda sedang meminta.
Ketika anda menentukan bidang untuk dikumpulkan dalam sebuah fungsi pengumpulan, Django akan mengizinkan anda menggunakan double underscore notation sama yang digunakan ketika mengacu ke bidang terkait dalam penyaring. Django akan kemudian menangani gabungan table apapun yang dibutuhkan untuk mengambil dan mengumpulkan nilai terkait.
For example, to find the price range of books offered in each store, you could use the annotation:
>>> from django.db.models import Max, Min
>>> Store.objects.annotate(min_price=Min("books__price"), max_price=Max("books__price"))
Ini memberitahu Django untuk mengambil model Store
, gabung (melalui hubungan many-to-many) dengan model Book
, dan dikumpulkan pada bidang harga dari model buku untuk menghasilkan sebuah nilai minimal dan maksimal.
The same rules apply to the aggregate()
clause. If you wanted to
know the lowest and highest price of any book that is available for sale
in any of the stores, you could use the aggregate:
>>> Store.objects.aggregate(min_price=Min("books__price"), max_price=Max("books__price"))
Join chains can be as deep as you require. For example, to extract the age of the youngest author of any book available for sale, you could issue the query:
>>> Store.objects.aggregate(youngest_age=Min("books__authors__age"))
Mengikuti hubungan kebelakang¶
Dalam sebuah cara mirip pada Pencarian yang menjangkau hubungan, pengumpulan dan catatan pada bidang dari model atau model yang terkait ke satu anda sedang meminta dapat menyertakan lintasan hubungan "reverse". Nama huruf kecil dari model terkait dan garis bawah ganda digunakan disini juga.
For example, we can ask for all publishers, annotated with their respective
total book stock counters (note how we use 'book'
to specify the
Publisher
-> Book
reverse foreign key hop):
>>> from django.db.models import Avg, Count, Min, Sum
>>> Publisher.objects.annotate(Count("book"))
(Setiap Publisher
dalam menghasilkan QuerySet
akan mempunyai sebuah atribut tambahan disebut book__count
.)
We can also ask for the oldest book of any of those managed by every publisher:
>>> Publisher.objects.aggregate(oldest_pubdate=Min("book__pubdate"))
(Hasil kamus akan mempunyai sebuah kunci dipanggil 'oldest_pubdate'
. Jika tidak ada nama lain seperti itu telah ditentukan, itu akan agak panjang 'book__pubdate__min'
.)
This doesn't apply just to foreign keys. It also works with many-to-many
relations. For example, we can ask for every author, annotated with the total
number of pages considering all the books the author has (co-)authored (note how we
use 'book'
to specify the Author
-> Book
reverse many-to-many hop):
>>> Author.objects.annotate(total_pages=Sum("book__pages"))
(Setiap Author
dalam menghasilkan QuerySet
akan mempunyai sebuah atribut tambahan dipanggil total_pages
. Jika tidak ada nama lain itu, itu akan menjadi agak panjang book__pages__sum
.)
Or ask for the average rating of all the books written by author(s) we have on file:
>>> Author.objects.aggregate(average_rating=Avg("book__rating"))
(Hasil kamus akan mempunyai sebuah kunci dipanggil 'average_rating'
. Jika tidak ada nama lain seperti itu telah ditentukan, itu akan agak panjang 'book__rating__avg'
.)
Pengumpulan dan klausa QuerySet
lain¶
filter()
dan exclude()
¶
Pengumpulan dapat juga ikut serta dalam penyaring. filter()
(atau``exclude()``) apapun berlaku pada bidang model biasa akan mempunyai pengaruh dari membatasi obyek yang dianggap untuk pengumpulan.
When used with an annotate()
clause, a filter has the effect of
constraining the objects for which an annotation is calculated. For example,
you can generate an annotated list of all books that have a title starting
with "Django" using the query:
>>> from django.db.models import Avg, Count
>>> Book.objects.filter(name__startswith="Django").annotate(num_authors=Count("authors"))
When used with an aggregate()
clause, a filter has the effect of
constraining the objects over which the aggregate is calculated.
For example, you can generate the average price of all books with a
title that starts with "Django" using the query:
>>> Book.objects.filter(name__startswith="Django").aggregate(Avg("price"))
Penyaringan pada keterangan¶
Nilai dicatat dapat juga disaring. Nama lain untuk catatan dapat digunaan dalam klausa filter()
dan exclude()
di cara sama seperti bidang model lainnya apapun.
For example, to generate a list of books that have more than one author, you can issue the query:
>>> Book.objects.annotate(num_authors=Count("authors")).filter(num_authors__gt=1)
Permintaan ini membangkitkan sebuah kumpulan hasil keterangan, dan kemudian membangkitkan sebuah penyaring berdasarkan pada keterangan itu.
If you need two annotations with two separate filters you can use the
filter
argument with any aggregate. For example, to generate a list of
authors with a count of highly rated books:
>>> highly_rated = Count("book", filter=Q(book__rating__gte=7))
>>> Author.objects.annotate(num_books=Count("book"), highly_rated_books=highly_rated)
Each Author
in the result set will have the num_books
and
highly_rated_books
attributes. See also Pengumpulan bersyarat.
Memilih diantara filter
dan QuerySet.filter()
Hidnari menggunakan argumen filter
dengan keterangan atau pengumpulan tunggal. Itu lebih efesien menggunakan QuerySet.filter()
untuk tidak menyertakan baris. Argumen pengumpulan filter
hanya berguna ketika menggunakan dua atau lebih pengumpulan terhadap hubungan sama dengan keadaan berbeda.
Urutan dari klausa annotate()
dan filter()
¶
Ketika mengembangkan sebuah permintaan rumit yang melibatkan kedua klausa annotate()
dan filter()
, berikan perhatian khusus pada urutan dimana klausa diberlakukan pada QuerySet
.
Ketika sebuah klausa annotate()
diberlakukan pada sebuah permintaan, catatan dihitung terhadap keadaan dari permintaan sampai titik dimana catatan diminta. Impliaksi praktik dari ini adalah bahwa filter()
dan annotate()
bukan operasi komutatif.
Diberikan:
- Penerbit A mempunyai dua buku dengan nilai 4 dan 5.
- Penerbit B mempunyai dua buku dengan nilai 1 dan 4.
- Penerbit C mempunyai satu buku dengan penilaian 1.
Berikut adalah contoh dengan kumpulan Count
:
>>> a, b = Publisher.objects.annotate(num_books=Count("book", distinct=True)).filter(
... book__rating__gt=3.0
... )
>>> a, a.num_books
(<Publisher: A>, 2)
>>> b, b.num_books
(<Publisher: B>, 2)
>>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count("book"))
>>> a, a.num_books
(<Publisher: A>, 2)
>>> b, b.num_books
(<Publisher: B>, 1)
Kedua permintaan mengembalikan sebuah daftar dari penerbit yang mempunyai setidaknya satu buku dengan penilaian melebihi 3.0, karenanya penerbit C tidak disertakan.
Dalam permintaan pertama, catatan mendahului penyaring, jadi penyaring tidak mempunyai pengaruh pada catatan. distinct=True
dibutuhkan untuk menghindari query bug.
Permintaan kedua menghitung jumlah buku yang mempunyai nilai melebihi 3.0 untuk setiap penerbit. Penyaring ini mendahului catatan, jadi batasan penyaring obyek dianggap ketika menghitung catatan.
Berikut contoh lain dengan kumpulan Rata-Rata
:
>>> a, b = Publisher.objects.annotate(avg_rating=Avg("book__rating")).filter(
... book__rating__gt=3.0
... )
>>> a, a.avg_rating
(<Publisher: A>, 4.5) # (5+4)/2
>>> b, b.avg_rating
(<Publisher: B>, 2.5) # (1+4)/2
>>> a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(
... avg_rating=Avg("book__rating")
... )
>>> a, a.avg_rating
(<Publisher: A>, 4.5) # (5+4)/2
>>> b, b.avg_rating
(<Publisher: B>, 4.0) # 4/1 (book with rating 1 excluded)
Permintaan pertama meminta untuk rata-rata penilaian dari semua penerbit buku untuk penerbit yang mempunyai setidaknya satu buku ketika penilaian melebihi 3.0. Permintaan kedua meminta untuk rata-rata dari penilaian buku penerbit untuk hanya mereka penilaian melebihi 3.0.
Itu adalah sangat sulit memahami bahwa ORM akan menterjemahkan queryset rumit kedalam permintaan SQL ketika dalam keraguan, periksa SQL dengan str(queryset.query)
dan tulis jumlah besar percobaan.
order_by()
¶
Catatan dapat digunakan sebagai dasar untuk pengurutan. Ketika anda menentukan sebuah klausa order_by()
, pengumpulan anda sediakan dapat mengacu nama lain apapun ditentukan sebagai bagian dari sebuah klausa annotate()
dalam permintaan.
Misalnya, untuk mengurutkan QuerySet
buku berdasarkan jumlah penulis yang berkontribusi pada buku, anda dapat menggunakan permintaan berikut:
>>> Book.objects.annotate(num_authors=Count("authors")).order_by("num_authors")
values()
¶
Biasanya, catatan dibangkitkan pada dasar per-obyek - sebuah QuerySet
dicatat akan mengembalikan satu hasil untuk setiap obyek dalam QuerySet
asli. Bagaimanapun, ketika sebuah klausa values()
digunakan untuk membatasi kolom yang dikembalikan dalam kumpulan hasil, metode untuk menilai catatan adak sedikit berbeda. Sebagai gantinya mengembalikan sebuah hasil dicatat untuk setiap hasil dalam QuerySet
asli, hasil asli dikelompokkan menurut pada perpaduan unik dari bidang-bidang ditentukan dalam klausa values()
. Sebuah catatam kemudian disediakan untuk setiap kelompok unik; catatan dihitung terhadap semua anggota dari kelompok.
Untuk setiap contoh, pertimbangkan sebuah permintaan penulis yang berusaha menemukan rata-rata penilaian dari buku ditulis oleh setiap penulis.
>>> Author.objects.annotate(average_rating=Avg('book__rating'))
Ini akan mengembalikan satu hasil untuk setiap penulis di basisdata, diberikan keterangan dengan nilai buku rata-rata mereka.
Namun, hasilnya akan sedikit berbeda jika anda menggunakan klausa values()
:
>>> Author.objects.values("name").annotate(average_rating=Avg("book__rating"))
Dalam contoh ini, penulis akan dikelompokkan berdasarkan nama, jadi anda akan hanya mendapatkan sebuah hasil dicatata untuk setiap nama penulis unik. Ini berarti jika anda mempunyai dua penulis dengan nama sama, hasil mereka akan digabung kedalam hasil tunggal di keluaran dari permintaan; rata-rata akan dihitung sebagai rata-rata terhadap buku ditulis oleh kedua penulis.
Urutan dari klausa annotate()
dan values()
¶
Seperti klausa filter()
, urutan dimana klausa annotate()
dan values()
diberlakukan pada sebuah permintaan adalah signifikan. Jika klausa values()
mendahului annotated()
, catatan akan dihitung menggunakan pengelomppokan digambarkan oleh klausa values()
.
Bagaimanapun, jika klausa annotate()
mendahului klausa values()
, catatan akan dibangkitkan terhadap keseluruhan kumpulan permintaan. Dalam kasus ini, klausa values()
hanya membatasi bidang yang dibangkitkan pada keluaran.
Misalnya, jika kita membalikkan urutan klausa values()
dan annotate()
dari contoh kita sebelumnya:
>>> Author.objects.annotate(average_rating=Avg("book__rating")).values(
... "name", "average_rating"
... )
Ini akan sekarang menghasilkan hasil unik untuk setiap penulis; bagaimanapun, hanya nama penulis dan catatan average_rating
akan dikembalikan dalam data keluaran.
Anda harus juga catat bahwa average_rating
telah jelas disertakan dalam daftar dari nilai-nilai untuk dikembalikan. Ini dibutuhkan karena dari urutan dari klausa values()
dan annotate()
.
Jika klausa values()
mendahului klausa annotate()
, setiap catatan akan otomatis ditambahkan ke kumpulan hasil. Bagaimanapun, jika klausa values()
diberlakukan setelah klausa annotate()
, anda butuh jelas menyertakan kolom pengumpulan.
Interaksi dengan order_by()
¶
Bidang yang disebutkan di bagian order_by()
dari kumpulan permintaan digunakan saat memilih data keluaran, bahkan jika bidang tersebut tidak ditentukan lain dalam panggilan values()
. Bidang tambahan ini digunakan untuk mengelompokkan hasil "suka" bersama-sama dan mereka dapat membuat baris hasil identik tampak terpisah. Ini muncul, terutama, saat menghitung sesuatu.
Berdasarkan cara contoh, seharusnya anda mempunyai model seperti ini:
from django.db import models
class Item(models.Model):
name = models.CharField(max_length=10)
data = models.IntegerField()
If you want to count how many times each distinct data
value appears in an
ordered queryset, you might try this:
items = Item.objects.order_by("name")
# Warning: not quite correct!
items.values("data").annotate(Count("id"))
...which will group the Item
objects by their common data
values and
then count the number of id
values in each group. Except that it won't
quite work. The ordering by name
will also play a part in the grouping, so
this query will group by distinct (data, name)
pairs, which isn't what you
want. Instead, you should construct this queryset:
items.values("data").annotate(Count("id")).order_by()
...membersihkan pengurutan apapun di permintaan. Anda dapat juga mengurutkan berdasarkan, katakan, data
tanpa pengaruh berbahaya apapun, sejak itu sudah bermain sebuah peran dalam permintaan.
Perilaku ini adalah sama seperti dicatat dalam dokumentasi queryset untuk distinct()
dan aturan umum adalah sama: biasanya anda tidak ingin kolom tambahan bermain bagian dalam hasil, jadi bersihkan pengurutan, atau setidaknya pastikan itu terbatas hanya bidang-bidang tersebut anda juga pilih dalam sebuah panggilan values()
.
Catatan
Anda mungkin layak bertanya mengapa Django tidak memidnahkan kolom tidak ada hubungannya untuk anda. Alasan utama adalah ketetapan dengan distinct()`` dan tempat lain: Django tidak pernah memindahkan pengurutan yang anda telah tentukan (dan kami tidak dapat merubah perilaku metode lain tersebut, ketika itu akan melanggar kebijakan Keseimbangan API kami).
Pengumpulan catatan¶
Anda dapat juga membangkitkan sebuah pengumpulan pada hasil dari sebuah catatan. Ketika anda menentukan sebuah klausa aggregate()
, pengumpulan anda sediakan dapat mengacu setiap nama lain ditentukan sebagai bagian dari sebuah klausa annotated()
dalam permintaan.
For example, if you wanted to calculate the average number of authors per book you first annotate the set of books with the author count, then aggregate that author count, referencing the annotation field:
>>> from django.db.models import Avg, Count
>>> Book.objects.annotate(num_authors=Count("authors")).aggregate(Avg("num_authors"))
{'num_authors__avg': 1.66}
Aggregating on empty querysets or groups¶
When an aggregation is applied to an empty queryset or grouping, the result
defaults to its default parameter, typically
None
. This behavior occurs because aggregate functions return NULL
when
the executed query returns no rows.
You can specify a return value by providing the default argument for most aggregations. However, since
Count
does not support the default argument, it will always return 0
for empty querysets
or groups.
For example, assuming that no book contains web in its name, calculating the
total price for this book set would return None
since there are no matching
rows to compute the Sum
aggregation on:
>>> from django.db.models import Sum
>>> Book.objects.filter(name__contains="web").aggregate(Sum("price"))
{"price__sum": None}
However, the default argument can be set when
calling Sum
to return a different default value if
no books can be found:
>>> Book.objects.filter(name__contains="web").aggregate(Sum("price", default=0))
{"price__sum": Decimal("0")}
Under the hood, the default argument is implemented
by wrapping the aggregate function with
Coalesce
.