Django ORM, поисковые выражения и агрегатные функции

⏱ 30 мин · 🧩 заданий: 7

Поисковые выражения (lookups)

Lookup — это суффикс после двойного подчёркивания, который задаёт оператор сравнения: поле__lookup=значение. Без суффикса подразумевается __exact.

Сравнение

Book.objects.filter(pages__exact=100)  # = 100 (то же, что pages=100)
Book.objects.filter(pages__gt=100)     # > 100
Book.objects.filter(pages__gte=100)    # >= 100
Book.objects.filter(pages__lt=100)     # < 100
Book.objects.filter(pages__lte=100)    # <= 100
Book.objects.filter(pages__range=(100, 300))  # BETWEEN 100 AND 300
Book.objects.filter(id__in=[1, 3, 5])         # IN (1, 3, 5)
Book.objects.filter(isbn__isnull=True)        # IS NULL

Строки

Book.objects.filter(title__contains="Мир")     # LIKE '%Мир%' (с учётом регистра)
Book.objects.filter(title__icontains="мир")    # без учёта регистра
Book.objects.filter(title__startswith="Война")
Book.objects.filter(title__istartswith="война")
Book.objects.filter(title__endswith="мир")
Book.objects.filter(title__iexact="война и мир")
Book.objects.filter(title__regex=r"^(Война|Мир)")

SQLite и кириллица

В SQLite icontains и iexact не учитывают регистр только для латиницы. В PostgreSQL кириллица работает корректно — ещё одна причина перейти на него (будет урок).

Даты

Post.objects.filter(publish__year=2025)
Post.objects.filter(publish__month=3)
Post.objects.filter(publish__day=8)
Post.objects.filter(publish__week_day=2)         # понедельник (1 — воскресенье)
Post.objects.filter(publish__date=date(2025, 3, 8))
Post.objects.filter(publish__year__gte=2024)     # lookups комбинируются

Через связи

Book.objects.filter(author__name__startswith="Т")
Post.objects.filter(comments__author__username="anna").distinct()

Q-выражения: сложные условия

По умолчанию все условия в filter() объединяются через И. Для ИЛИ и НЕ — объекты Q:

from django.db.models import Q

Book.objects.filter(Q(pages__lt=100) | Q(pages__gt=1000))      # ИЛИ
Book.objects.filter(~Q(author__name="Толстой"))                # НЕ
Book.objects.filter(
    Q(title__icontains="война") | Q(title__icontains="мир"),
    published__year__gte=1850,                                 # И (обычный аргумент — после Q)
)

Q удобно собирать динамически — например, для поиска по нескольким полям:

query = Q()
for word in "война мир".split():
    query |= Q(title__icontains=word)
Book.objects.filter(query)

F-выражения: ссылка на поле

F("поле") — значение столбца в базе данных. Позволяет сравнивать поля между собой и делать атомарные обновления:

from django.db.models import F

Book.objects.filter(pages__gt=F("chapters") * 20)    # сравнение двух полей
Post.objects.filter(pk=1).update(views=F("views") + 1)  # атомарный счётчик
Book.objects.annotate(pages_per_chapter=F("pages") / F("chapters"))
Book.objects.order_by(F("published").desc(nulls_last=True))

Агрегатные функции

Count, Sum, Avg, Min, Max (из django.db.models):

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

# Итог по всему набору
Book.objects.aggregate(Avg("pages"), Max("pages"))

# Значение для каждого объекта
Author.objects.annotate(total=Count("books"), avg=Avg("books__pages"))

Группировка: values() + annotate()

Если поставить values() перед annotate(), Django сгруппирует результат по указанным полям:

Book.objects.values("author__name").annotate(total=Count("id")).order_by("-total")
# [{'author__name': 'Толстой', 'total': 12}, {'author__name': 'Чехов', 'total': 9}, ...]
SELECT author.name, COUNT(book.id) AS total
FROM book JOIN author ON ...
GROUP BY author.name
ORDER BY total DESC;

Условные агрегаты

Author.objects.annotate(
    total=Count("books"),
    big=Count("books", filter=Q(books__pages__gt=300)),
)

Фильтрация по агрегату

Author.objects.annotate(total=Count("books")).filter(total__gte=5)   # HAVING COUNT >= 5

Порядок filter и annotate важен

filter() до annotate() ограничивает строки, которые попадут в подсчёт. filter() после — фильтрует по уже посчитанному значению.

Другие полезные выражения

from django.db.models import Case, Value, When
from django.db.models.functions import Coalesce, Concat, Length, Lower

Book.objects.annotate(title_len=Length("title"))
Author.objects.annotate(full=Concat("first_name", Value(" "), "last_name"))
Book.objects.annotate(
    size=Case(
        When(pages__lt=100, then=Value("короткая")),
        When(pages__lt=500, then=Value("средняя")),
        default=Value("длинная"),
    )
)

Итог

  • Lookups (__gt, __icontains, __in, __range, __isnull, __year…) задают условия.
  • Q — для ИЛИ/НЕ и динамических условий, F — ссылка на значение поля в базе.
  • aggregate — итог по набору, annotate — значение для каждого объекта.
  • values() перед annotate() = GROUP BY; filter=Q(...) внутри агрегата — условный подсчёт.

Практика решено 0 из 7

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Найдите книги, в названии которых встречается слово мир без учёта регистра. Результат — в books.

Задание 2 Один вариант ответа

Какой lookup найдёт книги, у которых поле isbn не заполнено (NULL)?

Задание 3 Написать код

Найдите посты, опубликованные в 2025 году (поле publish), у которых в заголовке есть Django или которые имеют больше 1000 просмотров (views). Используйте Q. Результат — в posts.

Задание 4 Один вариант ответа

Что делает Book.objects.filter(pages__gt=F("chapters") * 20)?

Задание 5 Написать код

Для каждого автора посчитайте число его книг, у которых больше 300 страниц, используя условный агрегат (filter=Q(...) внутри Count). Поле назовите big_books, обратная связь — books. Результат — в authors.

Задание 6 Несколько вариантов

Какие lookups существуют в Django?

Задание 7 Один вариант ответа

Что получится в результате Book.objects.values("author__name").annotate(total=Count("id"))?

Войдите, чтобы отмечать пройденные уроки.