Попрошу обобщать!

Очередная веха в жизни джанги произошла. У ORM в trunk'е появились агрегации! А следовательно уже в следующем релизе 1.1 мы официально их получим. Ура!

Но что нам даст возможность агрегации? То что раньше приходилось делать либо через голые SQL запросы, либо через extra теперь можно совершенно легитимно получить от ORM. Причём всё в том же привычном виде, что и обычные запросы.

По сути добавилось всего два метода к QuerySet'у это aggregate и annotate. Через них и осуществлется вся агрегационная функциональность.

Для того чтобы понять, что эти методы делают, представим себе такую модель данных и немного подергаем запросы:

class Company(models.Model):
    name = models.CharField(max_length=50)

    def __unicode__(self):
        return self.name

class Actor(models.Model):
    name = models.CharField(max_length=50)
    num_awards = models.PositiveSmallIntegerField(default=0)

    def __unicode__(self):
        return self.name

class Movie(models.Model):
    title = models.CharField(max_length=100)
    # некий абстрактный рейтинг актера
    rating = models.PositiveSmallIntegerField(default=0) 

    company = models.ForeignKey(Company)

    actors = models.ManyToManyField(Actor)

    def __unicode__(self):
        return self.title

В базе не много данных: всего 3 компании, 6 фильмов и 10 актеров - но нам этого хватит.

В стандартной поставке доступны следующие агрегационные функции: count, avg, max, min, sum, stddev, variance. Так же есть потенциальная возможность добавлять свои. Насколько это будет удобно и применимо, со временем, я думаю, поймем.

Их все можно импортировать так:

from django.db.models import Max, Min, Avg, Sum, Count, StdDev, Variance

Для начала попросим минимальный, максимальный и средний рейтинг фильмов во всей базе:

>>> Movie.objects.aggregate(Min("rating"), Max("rating"), Avg("rating"))
{'rating__avg': 60.166666666666664, 'rating__max': 85, 'rating__min': 34}

aggregate вернул dict с нужными нам данными. Это первая особенность - aggregate сразу возвращает результат в виде словаря (как, например, делает count, возвращая сразу число строк, удовлетворяющих запросу). Вторая особенность, это стандартный способ именования полей в возвращаемом результате - {имя_поля}__{агрегационная_функция}. Можно задавать и свои имена, например:

>>> Movie.objects.aggregate(average_rating=Avg("rating"))
{'average_rating': 60.166666666666664}

Так же данные для агрегации можно предварительно отфильтровать. Попросим те же самые значения но для фильмов одной компании:

>>> Movie.objects.filter(company__name="20th Century Fox")\
...                   .aggregate(Min("rating"), Max("rating"), Avg("rating"))
{'rating__avg': 80.0, 'rating__max': 85, 'rating__min': 75}

Но такого вида агрегация это только пол дела. Самое интересно, это когда мы хотим посчитать какие-то агрегационные данные для каждой из строк выборки. Для это появился метод annotate.

Для каждого фильма попросим среднее число наград их актеров:

>>> Movie.objects.annotate(Avg("actors__num_awards"))
[<Movie: Star Wars>, <Movie: Die Hard>, <Movie: Transformers>,
 <Movie: School of Rock>, <Movie: Quantum of Solace>, <Movie: Constantine>]

Не очень информативно, да? Вот тут я хочу отметить, некоторые особенности метода annotate. Он, как например filter или exclude сразу не дает результат, а возвращает новый QuerySet над которым далее можно еще "издеваться". Второе, посчитанные агрегационные данные добавляются как атрибуты к объектам. Т.е. чтобы наглядно показать результат предыдущего примера, можно сделать так:

>>> [(m.title, m.actors__num_awards__avg) for m in Movie.objects.annotate(Avg("actors__num_awards"))] 
[(u'Star Wars', 25.5),
 (u'Die Hard', 24.0),
 (u'Transformers', 9.0),
 (u'School of Rock', 5.5),
 (u'Quantum of Solace', 7.0),
 (u'Constantine', 17.0)]

Для annotate тоже можно явным образом указывать имена результирующих полей. Так же можно эти поля просить в values/values_list:

>>> Movie.objects.annotate(average_actor_awards=Avg("actors__num_awards")).\
...                    values_list("title", "average_actor_awards")
[(u'Star Wars', 25.5),
 (u'Die Hard', 24.0),
 (u'Transformers', 9.0),
 (u'School of Rock', 5.5),
 (u'Quantum of Solace', 7.0),
 (u'Constantine', 17.0)]

Для фильтрации уже агригированных результатов (аналог HAVING) применяется обычный filter, но он должен идти, после annotate. Это очень важно, т.к. до вызова этого метода, filter фильтрует строки до агрегации (обычный WHERE), а после уже по агрегационным результатам. Тоже самое и с сортировкой (order_by). Тут надо быть внимательней.

>>> Movie.objects.filter(company__name="20th Century Fox")\
...           .annotate(average_actor_awards=Avg("actors__num_awards")).values_list("title", "average_actor_awards")
[(u'Star Wars', 25.5), (u'Die Hard', 24.0)]

>>> Movie.objects.annotate(average_actor_awards=Avg("actors__num_awards"))\
...             .filter(average_actor_awards__lt=10).values_list("title", "average_actor_awards")
[(u'Transformers', 9.0), (u'School of Rock', 5.5), (u'Quantum of Solace', 7.0)]

>>> Movie.objects.annotate(average_actor_awards=Avg("actors__num_awards"))\
...              .order_by("average_actor_awards").values_list("title", "average_actor_awards")
[(u'School of Rock', 5.5), (u'Quantum of Solace', 7.0), (u'Transformers', 9.0),
 (u'Constantine', 17.0), (u'Die Hard', 24.0), (u'Star Wars', 25.5)]

Аннотации можно делать и для обратных отношений. Для компаний определим их средний рейтинг фильмов:

>>> Company.objects.annotate(Avg('movie__rating')).values_list("name", "movie__rating__avg")
[(u'Metro-Goldwyn-Mayer', 66.0), (u'20th Century Fox', 80.0),
 (u'Paramount Pictures', 50.5), (u'Warner Bros. Entertainment', 34.0)]

Как видите всё вполне логично и понятно.

Так же, я уже заметил, что большое количество __ в названиях полей немного сбивает с толку ORM, поэтому рекомендую по возможности давать свои имена результирующим полям. Но эти незначительные огрехи, и я надеюсь их быстро исправят.

Ну вот в общих чертах и всё. Обязательно прочтите документацию для полного обзора новый возможностей.

Вот, джанговский ORM стал ещё лучше и функциональней, что не может не радовать, так как один из аргументов недоброжелателей можно смело вычеркивать:-)

Эх, теперь даже не знаю чего следующего "большого" ждать... Про F-выражения Ваня Сагалаев сравнительно недавно писал (я повторятся не буду). Может вы знаете?

Комментарии 5

  1. Александр Соловьёв написал:

    http://code.djangoproject.com/ticket/6845 :-)

    Оставлен 15 Январь 2009 в 19:32
  2. Александр Кошелев написал:

    Да, Саш, спасибо. Валидация на уровне моделей вполне себе "большое" и нужное:-)

    Оставлен 15 Январь 2009 в 20:08
  3. igorekk написал:

    Спасибо, будем пробовать. Хотя не очень прозрачно это всё (доку пока не читал).

    Оставлен 15 Январь 2009 в 21:03
  4. Oleg написал:

    Александр!

    А каково Ваше мнение по поводу Jinja 2 ?

    Оставлен 17 Январь 2009 в 02:49
  5. Александр Кошелев написал:

    Ну раньше джинжа была шаблонным движком похожими на джанговский. То теперь, как мне кажется, это совсем другой шаблонный движок, отдаленно напоминающий прародителя. Но он имеет кучу интересных фишек - взять хотя бы компиляцию в байткод и его кеширование. Так же концепция иммутабельного контекста тоже очень интересна.

    А вообще, мне напрямую сталкиваться с ним не приходилось, поэтому что-то практическое сказать не могу.

    Оставлен 22 Январь 2009 в 10:54

Пингбеки 2

  1. От Выборка из базы по нескольким критериям 20 Март 2009 в 12:33

    [...]В django появилась возможность агрегации: - http://webnewage.org/2009/1/15/poproshu-obobschat/ - http://softwaremaniacs.org/blog/2009/01/15/django-aggregation/ Получить HAVING COUNT можно с помощью цепочки .annotate(Count()).filter()[...]

  2. От igorekk.com: И я тоже поковырял 19 Январь 2009 в 17:31

    [...]Вот и я тоже поковырял нововведение в Django: агрегации в ORM. Смотрел со стороны генерации sql-запросов. В принципе, остался доволен.Таким образом, в 90% случаев можно отойти от raw sql. Хотя[...]