Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. ORM: Базовые запросы
orm_queries

ORM: Базовые запросы

QuerySet, filter, exclude, order_by, values, annotate, aggregate

Django ORM: базовые запросы и QuerySet

ORM связывает модели Python с таблицами базы данных. Его главная единица — не список, а QuerySet: объект, который описывает будущий SQL-запрос и умеет дополнять его новыми условиями.

Материал ниже актуален для Django 5.2 LTS и 6.0.

#QuerySet ленивый — но не всегда

Вызовы filter(), exclude() и order_by() обычно не обращаются к базе. Они возвращают новый QuerySet, поэтому условия можно собирать поэтапно:

posts = Post.objects.filter(status=Post.Status.PUBLISHED) if author_id: posts = posts.filter(author_id=author_id) posts = posts.order_by("-published_at")

SQL выполняется при вычислении результата: во время итерации, преобразования в list, вызова len() или bool(), доступа по индексу, а также при first(), exists(), count() и других методах, которые должны вернуть готовое значение.

После обычной итерации Django сохраняет полученные объекты в кэше конкретного QuerySet. Повторная итерация по тому же объекту не делает новый запрос. Однако iterator() работает иначе, а новый QuerySet, даже построенный из тех же условий, имеет собственный кэш.

#Фильтрация и lookup-выражения

Аргументы одного filter() соединяются оператором AND. exclude() добавляет отрицательное условие. Двойное подчёркивание отделяет поле от lookup-оператора или позволяет пройти по связи:

posts = ( Post.objects .filter( status=Post.Status.PUBLISHED, title__icontains="django", views__gte=100, author__is_active=True, ) .exclude(category__slug="archive") )

Частые lookup-операторы: exact, iexact, contains, icontains, gt, gte, lt, lte, in, range и isnull. Сравнение без суффикса означает exact.

Для условий с OR и NOT используют объекты Q:

from django.db.models import Q posts = Post.objects.filter( Q(title__icontains=query) | Q(body__icontains=query), status=Post.Status.PUBLISHED, )

Позиционные объекты Q должны стоять перед именованными аргументами. Иначе Python не сможет разобрать вызов.

#Даты и полуоткрытые интервалы

Если created_at — DateTimeField, условие created_at__lte=date(2026, 7, 19) обычно включает только полночь указанной даты. Надёжнее использовать полуоткрытый интервал: начало включительно, следующая граница исключительно.

from datetime import timedelta posts = Post.objects.filter( created_at__gte=start_date, created_at__lt=end_date + timedelta(days=1), )

При включённом USE_TZ сравнивайте поле с часовыми значениями, осознанно привязанными к нужной зоне. В production границы пользовательского дня часто сначала переводят в UTC.

#Сортировка и срезы

order_by("field") сортирует по возрастанию, а минус задаёт убывание. Для стабильной пагинации добавляйте уникальное поле, иначе строки с одинаковой датой могут менять порядок:

posts = Post.objects.order_by("-published_at", "-pk") page = posts[:20] # SQL LIMIT 20

order_by("?") удобен для демонстрации, но случайная сортировка большой таблицы обычно дорогая. После среза с ограничением QuerySet уже нельзя произвольно фильтровать: сначала сформируйте условия, затем применяйте пагинацию.

Если имя сортировки приходит от пользователя, его нельзя передавать в order_by() без проверки. Разрешите только известные варианты:

ORDERING = { "new": ("-published_at", "-pk"), "old": ("published_at", "pk"), "popular": ("-views", "-pk"), } ordering = ORDERING.get(request.GET.get("order"), ORDERING["new"]) posts = posts.order_by(*ordering)

Такой словарь одновременно поддерживает оба направления сортировки и не позволяет пользователю обращаться к произвольным полям.

#Получение одного объекта

get() выражает важное ожидание: условию должна соответствовать ровно одна запись. При отсутствии объекта он выбрасывает DoesNotExist, при нескольких — MultipleObjectsReturned.

from django.shortcuts import get_object_or_404 post = get_object_or_404( Post, slug=slug, status=Post.Status.PUBLISHED, )

filter(...).first() возвращает объект или None, но скрывает ошибку, если записи неожиданно продублировались. Поэтому для уникального slug лучше обеспечить ограничение в базе и использовать get() либо get_object_or_404().

#exists(), count() и len() решают разные задачи

  • exists() отвечает, существует ли хотя бы одна строка;
  • count() просит базу подсчитать строки;
  • len(queryset) вычисляет выборку и считает объекты Python.

Если объекты всё равно понадобятся сразу после проверки, отдельный exists() может дать лишний запрос. В таком случае разумно один раз вычислить QuerySet и проверить полученный список. Если нужны только факт существования или количество, не загружайте полные модели.

#values() и values_list() тоже возвращают QuerySet

Эти методы меняют форму каждой строки, но не превращают результат в готовый список:

rows = Post.objects.values("id", "title", "author__username") titles = Post.objects.values_list("title", flat=True) named_rows = Post.objects.values_list("id", "title", named=True)

rows — ленивый QuerySet словарей, titles — ленивый QuerySet отдельных значений, named_rows — ленивый QuerySet именованных кортежей. Методы модели у таких строк недоступны. Это хороший формат для отчётов, но не универсальная «оптимизация»: связи, сериализация и дальнейшая бизнес-логика могут потребовать полноценных объектов.

#Агрегация и аннотации

aggregate() вычисляет итог по всей выборке и сразу возвращает словарь. annotate() добавляет выражение к каждой строке и возвращает QuerySet:

from django.db.models import Avg, Count, Q summary = Post.objects.filter( status=Post.Status.PUBLISHED, ).aggregate(avg_views=Avg("views")) posts = Post.objects.annotate( approved_comments=Count( "comments", filter=Q(comments__is_approved=True), ), )

Осторожно с несколькими связями «многие ко многим» или обратными внешними ключами. Два Count в одном запросе могут перемножить строки при JOIN и завысить оба результата. Для простого подсчёта помогает distinct=True:

posts = Post.objects.annotate( comment_count=Count("comments", distinct=True), like_count=Count("likes", distinct=True), )

Но distinct=True не исправляет любую агрегацию. Для сумм и более сложных расчётов могут понадобиться подзапросы или отдельные запросы. Всегда смотрите сгенерированный SQL и проверяйте результат на данных, где у одной записи есть несколько объектов по обеим связям.

#Поиск с несколькими значениями параметра

QueryDict хранит несколько значений одного ключа. request.GET.dict() оставляет только одно, поэтому для тегов нужен getlist():

tags = request.GET.getlist("tag") posts = Post.objects.filter(status=Post.Status.PUBLISHED) for slug in tags: # AND: пост должен иметь каждый выбранный тег posts = posts.filter(tags__slug=slug) posts = posts.distinct()

Цикл выражает логику AND. Один фильтр tags__slug__in=tags выражал бы OR. Это разные требования, и их следует назвать в интерфейсе и тестах.

#Legacy и актуальность

Базовый API QuerySet стабилен много лет, поэтому старый код с filter(), Q, annotate() и select_related() часто остаётся рабочим. Legacy-проблемы здесь обычно связаны не с удалёнными методами, а с неверными предположениями: QuerySet принимают за список, доверяют пользовательской сортировке или не замечают умножение строк при нескольких JOIN.

Отдельно проверяйте поведение конкретной поддерживаемой СУБД: регистронезависимый поиск, регулярные выражения и порядок NULL могут отличаться. ORM переносит синтаксис, но не отменяет различия баз данных.

#Что должно остаться после урока

Вы должны уметь предсказать момент выполнения SQL, выбрать между get(), first(), exists() и count(), безопасно собрать фильтры из запроса и объяснить, почему агрегаты по нескольким связям требуют теста на корректность.

#Документация

  • QuerySet API
  • Сложные запросы с Q
  • Агрегация

Далее: ORM: Продвинутые техники