Агрегирующие функции в Django ORM

С агрегирующими функциями в SQL вы знакомы: они возвращают из БД сумму, максимум или среднее арифметическое по определённому полю для всех записей в таблице или для выборки: «SQL, выбери в таблице joke все записи за 1.04.2001 и примени к ним агрегирующую функцию: найди запись с максимальным значением в столбце likes». База данных подсчитает и выведет полученные данные в отдельный столбец результирующей выборки.Для группировки строк используется специальный запрос GROUP BY, в котором перечисляются столбцы, по которым должна идти группировка. Это значит, что если в таблице несколько строк с одним и тем же значением в указанном поле, то такие строки объединятся в одну, а над другими столбцами можно провести групповые операции. Слоноводы несколько дней кормят слонов ивовыми вениками и записывают, кто сколько съел.

IDDATENAMEBROOM
12019-08-17Аристарх43
22019-08-17Джульетта38
32019-08-17Кузя26
42019-08-18Аристарх35
52019-08-18Джульетта33
62019-08-18Кузя19
72019-08-19Аристарх35
82019-08-19Джульетта41
92019-08-19Кузя9
102019-08-20Аристарх34
112019-08-20Джульетта42
122019-08-20Кузя18
132019-08-21Аристарх44
142019-08-21Джульетта39
152019-08-21Кузя23

Затем слоноводы хотят найти для каждого слона день, когда он съел больше всего, и для этого делают запрос к базе.
Ниже приведен запрос, при помощи которого можно получить желаемый результат.
Обратите внимание, что данное решение является особенностью SQLite, и в других СУБД его реализация может отличаться.

SELECT date, name, MAX(broom) 
FROM brooms
GROUP BY name; 

«Под капотом» происходит следующее: сначала в колонке name будут найдены и сгруппированы все совпадающие записи, получится три группы: все записи по Аристарху:

IDDATENAMEBROOM
12019-08-17Аристарх43
42019-08-18Аристарх35
72019-08-19Аристарх35
102019-08-20Аристарх34
132019-08-21Аристарх44

все записи по Джульетте:

IDDATENAMEBROOM
22019-08-17Джульетта38
52019-08-18Джульетта33
82019-08-19Джульетта41
112019-08-20Джульетта42
142019-08-21Джульетта39

и все записи по Кузе:

IDDATENAMEBROOM
32019-08-17Кузя26
62019-08-18Кузя19
92019-08-19Кузя9
122019-08-20Кузя18
152019-08-21Кузя23

Затем функция MAX(broom) в каждой из этих групп найдет максимальное значение в колонке broom, и в результирующую выборку будут выведены имена и найденные значения. Результат запроса будет таким:

DATENAMEMAX(BROOM)
2019-08-21Аристарх44
2019-08-20Джульетта42
2019-08-17Кузя26

Если в БД какого-нибудь магазина в таблице хранятся чеки покупок, можно сгруппировать записи по id покупателя, после чего применить агрегирующую функцию к полю, где указана цена чека — и посчитать максимальную или среднюю сумму заказов определённого покупателя. А можно просто сложить все value по отдельности для AB и C:

Если не указать GROUP BY — агрегирующая функция будет применена ко всей таблице. Например SELECT SUM(Value) FROM sample_table; вернет значение 140 для примера на иллюстрации, а SELECT MAX(broom) FROM brooms; вернёт 44, максимальное значение во всей колонке broom из таблицы со слонами и вениками.

Метод count()

Задача подсчёта записей в таблице или в выборке столь популярна, что в Django ORM она получила несколько вариантов решения. Чтобы узнать количество полученных строк, можно вызвать методcount() для objects, дописав его к конструкции, делающей выборку:

#  выбираем посты, опублибликованные позже (gt) июня 1854 года, затем пересчитываем их
>>> Post.objects.filter(pub_date__month__gt=6, pub_date__year=1854).count()

(0.012) SELECT COUNT(*) AS "__count" FROM "posts_post" WHERE (django_datetime_extract('month', "posts_post"."pub_date", 'UTC') > 6 AND "posts_post"."pub_date" BETWEEN '1854-01-01
00:00:00' AND '1854-12-31 23:59:59.999999'); args=(6, '1854-01-01 00:00:00', '1854-12-31 23:59:59.999999')
30 

Когда нужно узнать лишь число записей, но сами записи не нужны — не вызывайте метод all(), применяйте count(). Метод all() заставит базу прочитать и передать в код весь объем данных, а count() выполнит всю работу на стороне базы и вернет лишь одно число. Загрузка и обработка всех данных в такой ситуации — это пустой расход ресурсов.

# Проверка: есть ли данные в таблице

# Неправильный способ: этот код загружает вообще все данные из таблицы
if User.objects.all():
    print("Пользователи есть")

# Правильный способ: этот код просит базу вернуть число
if User.objects.count():
    print("Пользователи есть") 

Метод aggregate()

Метод aggregate() применяет агрегирующие функции к определённой выборке или ко всей таблице.В Django есть несколько агрегирующих функций, вот самые популярные из них:

  • Avg: вернёт среднее значение по указанной колонке в выборке
  • Count: вернёт количество записей в выборке, как и метод count(), описанный выше
  • Max: вернёт максимальное значение по указанной колонке в выборке
  • Min: вернёт минимальное значение по указанной колонке в выборке
  • Sum: вернёт сумму значений по указанной колонке в выборке

Эти функции хранятся в модуле django.db.models, перед применением их надо импортировать в код. Найдём самый большой id и пересчитаем объекты в модели Post при помощи метода aggregate():

>>> from django.db.models import Max, Count
#  найти максимальное значение id для объектов Post
>>> Post.objects.aggregate(Max("id"))
(0.000) SELECT MAX("posts_post"."id") AS "id__max" FROM "posts_post"; args=()
{'id__max': 43}

#  пересчитать объекты id в модели Post
>>> Post.objects.aggregate(Count("id"))
(0.000) SELECT COUNT("posts_post"."id") AS "id__count" FROM "posts_post"; args=()
{'id__count': 37}
37 

Связи между таблицами

При создании модели Post мы добавили в неё ссылку на автора, на модель User, и указали related_name="posts".

class Post(models.Model):
    # ... какой-то код
    author = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name='posts'
    ) 

У модели User автоматически появится свойство posts, оно ссылается на все записи текущего автора.

>>> leo = User.objects.get(id=2)
(0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined" FROM "auth_user" WHERE "auth_user"."id" = 2; args=(2,)
>>> leo.username
'leo'

>>> leo.posts.count()
#  leo.posts — это выборка тех объектов из модели Post, 
#  у которых в поле author_id стоит "2" (которые связаны с leo), 
#  потому что leo — это объект User с id=2
(0.000) SELECT COUNT(*) AS "__count" FROM "posts_post" WHERE "posts_post"."author_id" = 2; args=(2,)
36 

Свойство posts у объекта модели User появилось в результате того, что модель Post ссылается на User. И, благодаря магии ORM, мы можем получить все записи автора, обратившись к свойству posts.Как только вы создаёте в модели поле со ссылкой на другую модель — у второй модели появляется новое свойство, «обратная связь», и к этому свойству можно обращаться, как к любому другому.Свойства, ссылающиеся на объекты, имеют специальный тип: менеджер объектов. До сих пор мы работали с менеджером объектов objects, делая запросы вида User.objects.get(id=2) или Post.objects.all().Свойство posts — такой же менеджер объектов, как и objects, разница лишь в том, что оно было создано при связывании моделей.Для менеджеров объектов можно вызывать метод count(), который пересчитает связанные объекты. Django сам разберется, как правильно составить запрос в этом случае. Чтобы применить агрегирующие функции к связанным данным из других таблиц, запрос делается через аннотирование, методом annotate().

Метод annotate()

Чтобы получить количество записей, созданных каждым пользователем, нужно сгруппировать несколько таблиц и добавить объекту новое свойство, которое будет содержать количество связанных с ним объектов в другой таблице. Конечно, можно было бы в цикле пройтись по всем пользователям, вызывая метод count() менеджера объектов posts. Но это создало бы огромную нагрузку на базу и каскад однотипных запросов. В чистом SQL вопрос решается так: в результирующую выборку попадают запрошенные столбцы исходных таблиц и дополнительные столбцы с результатами вычислений. Похожим образом работает метод annotate() в ORM, но в результате к полученным объектам добавляется новое свойство, содержащее результат вычисления.В следующем примере аргумент posts_count — это имя нового свойства объекта, оно появится у объектов модели User:

# Достать из модели User все объекты, 
    # создать свойство posts_count и записать в него число постов, связанных с автором.
    # posts — это свойство модели User, менеджер объектов
    >>>> annotated_results = User.objects.annotate(posts_count = Count('posts'))
    >>> 
    >>> annotated_results
    (0.001) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined", COUNT("posts_post"."id") AS "posts_count" FROM "auth_user" LEFT OUTER JOIN "posts_post" ON ("auth_user"."id" = "posts_post"."author_id") GROUP BY "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined"  LIMIT 21; args=()
    <QuerySet [<User: admin>, <User: leo>]> 
#  перебрать в цикле список пользователей annotated_results 
#  и для каждого объекта вывести свойство name
#  и новое свойство posts_count, которое хранит число постов пользователя
>>> for item in annotated_results:
...     print(f'Постов у пользователя {item.username}: {item.posts_count}')
    ... 
(0.000) SELECT "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined", COUNT("posts_post"."id") AS "posts_count" FROM "auth_user" LEFT OUTER JOIN "posts_post" ON ("auth_user"."id" = "posts_post"."author_id") GROUP BY "auth_user"."id", "auth_user"."password", "auth_user"."last_login", "auth_user"."is_superuser", "auth_user"."username", "auth_user"."first_name", "auth_user"."last_name", "auth_user"."email", "auth_user"."is_staff", "auth_user"."is_active", "auth_user"."date_joined"; args=()
Постов у пользователя admin: 2
Постов у пользователя leo: 36
#  хорошо, что у нас пока что не 100500 авторов 

Разница между annotate() и aggregate()

Метод annotate()возвращает объекты и добавляет к ним новые свойства:

>>> rez = User.objects.annotate(written_posts = Count('posts'))
>>> rez[1].written_posts
36
#  у объекта класса User появилось свойство written_posts, 
#  хотя в модели User оно не описано 

Метод aggregate() отдает только значение, результат работы агрегирующей функции:

>>> Checks.objects.aggregate(average_price=Avg('price'))
{'average_price': 127.01} 




Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: