Django Aggregation (Part I)
Trước đây, tôi đã có một loạt bài dịch về QuerySet trong Django: A Survey On QuerySet In Django (Part I) A Survey On QuerySet In Django (Part II) Các bài viết này trình bày về cách chúng ta sử dụng các câu query để thực hiện CRUD trong Django. Tuy nhiên, đôi khi chúng ta sẽ cần lấy ra các giá ...
Trước đây, tôi đã có một loạt bài dịch về QuerySet trong Django:
A Survey On QuerySet In Django (Part I)
A Survey On QuerySet In Django (Part II)
Các bài viết này trình bày về cách chúng ta sử dụng các câu query để thực hiện CRUD trong Django. Tuy nhiên, đôi khi chúng ta sẽ cần lấy ra các giá trị được dẫn xuất thông qua các phép toán tập hợp (Aggregation Operation). Và bài viết này sẽ hướng dẫn chúng ta giải quyết vấn đề đó.
Giả sử chúng ta có các model như bên dưới. Các model này được sử dụng để theo dõi việc kiểm kê ở một chuỗi cửa hàng sách online:
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) num_awards = models.IntegerField() 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) pubdate = models.DateField() class Store(models.Model): name = models.CharField(max_length=300) books = models.ManyToManyField(Book) registered_users = models.PositiveIntegerField()
Cheat sheet
Đây là cách thực hiện các aggregate query phổ biến, áp dụng với các model bên trên:
# 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. from django.db.models import Avg Book.objects.all().aggregate(Avg('price')) => {'price__avg': 34.35} # Max price across all books. from django.db.models import Max Book.objects.all().aggregate(Max('price')) => {'price__max': Decimal('81.20')} # Cost per page from django.db.models import F, FloatField, Sum Book.objects.all().aggregate( price_per_page=Sum(F('price')/F('pages'), output_field=FloatField())) => {'price_per_page': 0.4470664529184653} # 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 # 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
Generating aggregates over a QuerySet
Django cung cấp hai cách để sinh ra aggregate. Cách đầu tiền là sinh ra các summary value cho cả một QuerySet. Ví dụ như bạn muốn tính giá bán trung bình của các quyển sách. Câu query bên dưới sẽ lấy ra tập tất cả các cuốn sách:
Book.objects.all()
Cái chúng ta cần là một cách để tính summary value qua các object của QuerySet này. Rất đơn giản, chúng ta chỉ việc thêm mệnh đề aggregate() như sau:
from django.db.models import Avg Book.objects.all().aggregate(Avg('price')) => {'price__avg': 34.35}
Tham biến truyền vào mệnh đề aggregate() mô tả aggregate value mà chúng ta muốn tính toán - trong trường hợp này chính là giá trị trung bình của trường price trong model Book. Bạn có thể tham các hàm aggregate ở đây.
Kết quả trả về khi sử dụng aggregate() sẽ là một dictionary - gồm các cặp name / value. Name là định danh cho aggregate value; còn value là kết quả được tính toán. Name sẽ được sinh tự động từ tên field tham gia aggregate và tên hàm aggregate. Nếu bạn muốn tự chỉ định name cho các aggregate value, bạn có làm như sau:
Book.objects.aggregate(average_price=Avg('price')) => {'average_price': 34.35}
Nếu bạn muốn sinh ra nhiều hơn một aggregate, bạn chỉ cần truyền thêm các tham biến khác vào mệnh đề aggregate(). Như vậy, nếu chúng ta muốn biết giá bán cao nhất và thấp nhất trong tất cả các sách, chúng ta có thể sử dụng câu query sau:
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')}
Generating aggregates for each item in a QuerySet
Cách thứ hai để tạo ra summary value là sinh một summary độc lập (independent summary) đối với mỗi object trong QuerySet. Ví dụ, nếu bạn đã lấy được danh sách các cuốn sách, có thể bạn lại muốn biết có bao nhiêu tác giả đã đóng góp cho mỗi cuốn sách. Model Book có quan hệ many-to-many với model Author; chúng ta muốn summarise quan hệ này đối với mỗi object Book trong QuerySet.
Summary kiểu này có thể được sinh ra bằng việc sử dụng mệnh đề annotate(). Mỗi object trong QuerySet trả về sẽ được chú thích thêm bằng các giá trị được chỉ định.
Về mặt cú pháp, không có gì khác so với khi sử dụng mệnh đề aggregate(). Mỗi tham biến mô tả một aggregate cần tính toán. Ví dụ, để annotate các cuốn sách với số lượng tác giả:
# 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
Như với aggregate(), name của các annotation sẽ được dẫn xuất tự động từ name của hàm aggregate và name của field tham gia aggregate. Bạn có thể override name mặc định bằng cách cấp cho nó một alias khi khai báo annotation:
q = Book.objects.annotate(num_authors=Count('authors')) q[0].num_authors => 2 q[1].num_authors => 1
Không giống như aggregate(), annotate() sẽ trả về một QuerySet.
Combining multiple aggregations
Sử dụng annotate() với nhiều bảng sẽ thu lại một kết quả sai bởi vì các lệnh JOIN sẽ được sử dụng thay cho các subquery:
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
Với hầu hết các aggregate, không có cách nào có thể tránh được vấn đề này. Tuy nhiên, aggregate Count có tham số distinct có thể giải quyết vấn đề này:
q = Book.objects.annotate(Count('authors', distinct=True), Count('store', distinct=True)) q[0].authors__count => 2 q[0].store__count => 3
-
Nếu nghi ngờ, hãy kiểm tra câu lệnh SQL
Để hiểu những gì xảy ra với câu query của bạn, hãy kiểm thuộc tính query của QuerySet.
Phần I của loạt bài viết về Django Aggreagation xin được dừng lại tại đây