⚠️ Дисклеймер
Эта статья подготовлена по мотивам книги «Django 5 By Example» (автор: Antonio Melé, издательство: Packt Publishing, 5-е издание, ISBN: 9781805122340).
Статья не является официальным переводом книги и не копирует оригинальный текст. Все материалы в публикации — это мои личные переработки, пояснения, примеры и улучшения, сделанные с целью образовательной поддержки русскоязычного сообщества разработчиков.
Примеры кода, использованные в статье, основаны на открытом репозитории книги и распространяются по лицензии MIT, допускающей свободное использование с указанием авторства.
Все права на оригинальное содержание принадлежат автору книги и издательству Packt Publishing.
📖 Оригинальная книга доступна на официальном сайте:
https://www.packtpub.com/en-us/product/django-5-by-example-9781805122340
Введение
Материал данной статьи является продолжением статьи: Создание Блога на Django 5, PostgreSQL 16.
Настройка окружения для решения примеров подробно изложена в статьях: Установка PostgreSQL 16 для Django 5, Как ускорить поиск в Django в 100 раз.
Вступление
Эта статья — подробный справочник по фильтрации данных в Django 5, с акцентом на практическое применение поисковых выражений (field lookups). Здесь ты найдёшь:
- разбор всех популярных типов фильтрации (exact, icontains, in, gte, date и другие);
- примеры с реальными данными из модели Post (заголовки, даты, авторы);
- SQL, который генерирует Django ORM под капотом;
- объяснение, как работает поддержка временных зон, make_aware, timezone.now() и timestamptz;
- рекомендации по производительности: когда индексы работают, а когда нет, как ускорить поиск с pg_trgm и GIN.
Материал полезен всем, кто хочет уверенно использовать Django ORM в реальных проектах: от джунов, изучающих QuerySet, до медлов и синьоров, которым важно писать понятные, быстрые и масштабируемые запросы.
Синтаксис lookups: поле__тип_поиска
Интерфейс QuerySet предоставляет множество типов условий поиска, называемых поисковыми выражениями (lookups).
Тип поиска указывается с помощью двойного подчёркивания __ в формате:
имя_поля__тип_поиска
Например, следующий запрос выполнит фильтрацию по точному совпадению:
Запустите shell_plus:
python manage.py shell_plus --print-sql
Результат:
exact и iexact: точное совпадение и регистронезависимое сравнение
Выполните код:
>>> Post.objects.filter(id__exact=1)
Результат:
Если явно не указан тип поиска, Django по умолчанию использует тип exact — то есть точное совпадение.
Следующий запрос эквивалентен предыдущему:
>>> Post.objects.filter(id=1)
Результат:
Рассмотрим другие распространённые типы поисковых выражений.
Чтобы выполнить поиск без учёта регистра, можно использовать тип iexact:
>>> Post.objects.filter(title__iexact='Цветаева: Арест и правда')
Результат:
Объяснение:
- title__iexact — это поиск по полю title без учёта регистра.
- В SQL это реализуется через UPPER(...) = UPPER(...).
- Таким образом, заголовки вроде:
ЦВЕТАЕВА: АРЕСТ И ПРАВДА
цветаева: арест и правда
Цветаева: Арест И Правда— все будут считаться совпадениями. - Результат корректный: совпали все посты с таким заголовком, независимо от регистра.
- Дубликаты — потому что у нас в базе несколько постов с одинаковым title.
contains и icontains: вхождение подстроки
Вы можете фильтровать объекты, используя проверку на вхождение подстроки. Поисковое выражение contains транслируется в SQL-запрос с использованием оператора LIKE:
>>> Post.objects.filter(title__contains='Пушкин')
Результат:
Что делает этот запрос
Он фильтрует объекты модели Post, в которых поле title содержит подстроку 'Пушкин', с учётом регистра. Это значит, что слова вроде "пушкин" (с маленькой буквы) не попадут в результат, а только строго "Пушкин".
Что происходит на уровне SQL
Django транслирует __contains в SQL с LIKE '%Пушкин%':
WHERE "blog_post"."title"::text LIKE '%Пушкин%'
Это значит:
- %Пушкин% — ищем вхождение подстроки "Пушкин" в любом месте заголовка.
- LIKE чувствителен к регистру в PostgreSQL, в отличие от ILIKE.
Мы получили список постов, в названии которых встречается "Пушкин", например:
- Пушкин: Дуэль и правда
- Пушкин: Любовница и правда
- Пушкин: Письма и правда
Их много, потому что у нас в БД много таких заголовков.
Также доступна регистронезависимая версия — icontains, которая не чувствительна к регистру:
>>> Post.objects.filter(title__icontains='пушкин')
Результат:
Этот запрос найшел все записи, в которых подстрока 'пушкин' содержится в заголовке — независимо от того, написано ли слово с заглавной буквы или строчной (Пушкин, ПУШКИН, ПуШкИн и т.д.).
in: выборка по списку значений
Вы можете проверить, находится ли значение поля в заданной коллекции (часто это список, кортеж или другой QuerySet) с помощью поискового выражения in.
Пример ниже извлекает посты, у которых id равен 1 или 3:
>>> Post.objects.filter(id__in=[1, 3])
Результат:
Что делает этот запрос:
Он выбирает все объекты модели Post, у которых поле id входит в список [1, 3].
- В таблице blog_post есть две записи с id=1 и id=3.
- У этих записей заголовки:
Цветаева: Арест и правда
Цветаева: Гибель и правда
gt, gte, lt, lte: числовые сравнения
Следующий пример демонстрирует использование поискового выражения gt (greater than) — больше чем:
>>> Post.objects.filter(id__gt=3)
Результат:
Что делает этот запрос:
Он выбирает все объекты модели Post, у которых поле id строго больше 3 (gt = greater than).
Это означает, что:
- В базе есть записи с id = 4, 5, 6, ....
- Записи отсортированы по убыванию publish (ORDER BY publish DESC).
- Django по умолчанию ограничивает результат 21 объектом (вы видите LIMIT 21 в SQL).
Этот пример демонстрирует использование поискового выражения gte (greater than or equal) — больше или равно:
>>> Post.objects.filter(id__gte=3)
Результат:
Этот пример демонстрирует использование поискового выражения lt (less than) — меньше чем:
>>> Post.objects.filter(id__lt=3)
Результат:
Что делает запрос
Он выбирает все записи модели Post, у которых поле id меньше 3, т.е. id = 1 и id = 2.
- id < 3 — это простое числовое сравнение по первичному ключу.
- В таблице есть записи с id=1 и id=2.
- ORDER BY publish DESC — сортировка по дате публикации (от новых к старым).
- LIMIT 21 — Django по умолчанию ограничивает выборку (если не задан limit в Python).
Этот пример демонстрирует использование поискового выражения lte (less than or equal) — меньше или равно:
>>> Post.objects.filter(id__lte=3)
Результат:
Что делает запрос
Он возвращает все объекты модели Post, у которых поле id меньше или равно 3. Это значит: id = 1, 2, 3.
- Условие: id <= 3 (меньше или равно 3).
- Сортировка по убыванию даты публикации (publish DESC).
- Ограничение: максимум 21 результат (стандартное поведение Django без [:N]).
startswith и istartswith: начало строки
Для поиска записей, где значение поля начинается с определённой подстроки, в Django используются два типа поисковых выражений:
- startswith — чувствительный к регистру
- istartswith — нечувствительный к регистру
Пример ниже выполнит поиск заголовков, начинающихся с 'пуш', без учёта регистра:
>>> Post.objects.filter(title__istartswith='пуш')
Результат:
Что делает запрос
Он ищет все посты, у которых поле title начинается с подстроки "пуш" без учёта регистра. То есть подойдут и:
- "Пушкин..."
- "пушкин..."
- "ПУШ..."
и всё, что начинается с этих букв.
⚠️ 29 ms - плохой результат для такого запроса. Если хочешь ускорить такие запросы в 100 раз — нужно использовать GIN-индексы с pg_trgm. В статье: Как ускорить поиск в Django в 100 раз я показал как это можно сделать.
endswith и iendswith: конец строки
Для выполнения поиска по окончанию строки в Django можно использовать два типа выражений:
- endswith — чувствительный к регистру
- iendswith — нечувствительный к регистру
Пример ниже находит все посты, в заголовках которых строка заканчивается на 'правда', без учёта регистра:
>>> Post.objects.filter(title__iendswith='правда')
Результат:
Что делает этот запрос
Он ищет все объекты модели Post, у которых поле title заканчивается на "правда", без учёта регистра.
Подойдут такие варианты:
- "...и правда"
- "...и ПРАВДА"
- "...И пРаВдА"
- Все заголовки постов заканчиваются на "правда"
- Независимо от того, какая фамилия/событие в начале — фильтр сработал по окончанию строки
- ORDER BY publish DESC — сортировка по дате публикации (от новых к старым).
- LIMIT 21 — Django по умолчанию ограничивает выборку (если не задан limit в Python).
⚠️ 38 ms - плохой результат для такого запроса. Если хочешь ускорить такие запросы в 100 раз — нужно использовать GIN-индексы с pg_trgm. В статье: Как ускорить поиск в Django в 100 раз я показал как это можно сделать.
Поиск по датам и времени
__date: поиск по дате (⚠️ отключает индексы)
Существуют специальные типы поисковых выражений для работы с датами. Например, поиск по точной дате можно выполнить так:
>>> from datetime import date
>>> Post.objects.filter(publish__date=date(2024, 1, 31))
Этот запрос вернёт все посты, у которых поле publish содержит дату публикации 31 января 2024 года, вне зависимости от времени.
Результат:
>>> Post.objects.filter(publish__date=date(2024, 1, 31))
Что делает запрос
Он фильтрует записи модели Post, где поле publish (тип DateTimeField) соответствует дате 2024-01-31, без учёта времени.
- publish — это timezone-aware datetime, и мы включили USE_TZ=True в settings.py, поэтому Django:сохраняет publish в UTC;
- при фильтрации по дате конвертирует её в Europe/Moscow, чтобы сравнивать локальную дату;
- ::date — означает извлечение только даты (без времени) из поля publish.
- результат — пустой, то есть нет постов с publish-датой 2024-01-31 по московскому времени.
⚠️ Почему может быть предупреждение при выполнени такого запроса?
Возможные предупреждения:
- Naive datetime used while time zone support is active
— если бы мы использовалм datetime(...) без make_aware. - Сравнение дат в разных зонах
— если база содержит publish в UTC, а сравнение идёт по местной зоне.
Этот пример демонстрирует, как фильтровать поле DateField или DateTimeField по году:
>>> Post.objects.filter(publish__year=2024)
Этот запрос вернёт все посты, у которых значение поля publish (тип DateTimeField) соответствует любому дню в 2024 году, независимо от месяца, дня или времени.
Вы можете фильтровать DateField или DateTimeField по месяцу, используя выражение __month. Пример:
>>> Post.objects.filter(publish__month=1)
Этот запрос выберет все посты, у которых поле publish приходится на январь — вне зависимости от года, числа и времени.
⚠️ Минус этого подхода:
- EXTRACT(MONTH FROM ...) делает использование индексов невозможным.
- Поэтому при большом объёме данных лучше использовать диапазон по времени.
Альтернатива: __range с make_aware и временными зонами
✅ Альтернатива с диапазоном (индексируемая):
from datetime import datetime
from zoneinfo import ZoneInfo
from django.utils.timezone import make_aware
tz = ZoneInfo("Europe/Moscow")
start = make_aware(datetime(2024, 1, 1), tz)
end = make_aware(datetime(2024, 2, 1), tz)
Post.objects.filter(publish__gte=start, publish__lt=end)
Результат:
Что делает запрос
Вы фильтруете посты, дата публикации которых (publish) попадает в январь 2024 года, по московскому времени (Europe/Moscow), включительно с полуночью 1 января и до полуночи 1 февраля, исключая сам 1 февраля.
Это правильный, точный и индексируемый способ фильтрации по месяцу.
- Оба значения (start, end) — aware datetime с зоной +03:00, так как ты явно задал Europe/Moscow через ZoneInfo + make_aware.
- PostgreSQL корректно интерпретирует эти значения как timestamptz.
⚡ Execution time: 0.007692s ≈ 7.7 мс
— это быстро, даже с 20+ совпадениями, потому что используется простой индексный диапазон без функций над колонками.
Результат:
<QuerySet [
<Post: Любовницы Михаил>,
<Post: Пушкин: Поездка и правда>,
...
]>
— все посты, у которых publish попадает в январь 2024 года по Москве.
__year, __month, __day: фильтрация по компонентам даты
Можно фильтровать поле DateField или DateTimeField по дню месяца с помощью __day. Пример:
>>> Post.objects.filter(publish__day=1)
Этот запрос вернёт все посты, у которых дата публикации (publish) приходится на первое число любого месяца, независимо от года и месяца.
Результат:
В Django можно комбинировать (chain) дополнительные lookup‑выражения, в том числе к дате, году, месяцу, дню.
Пример ниже показывает, как выполнить фильтрацию по дате, большей чем указанная:
>>> from datetime import date
>>> Post.objects.filter(publish__date__gt=date(2024, 1, 1))
Результат:
Что делает этот запрос
Он выбирает все посты, у которых дата поля publish (без учёта времени) строго позже 2024-01-01.
Пример: фильтрация по дате и времени публикации (Europe/Moscow)
Работа с временными зонами (USE_TZ=True)
timezone.make_aware()
Задача: найти посты, опубликованные после 13 июня 2025 года, 16:30 по Москве.
Шаг 1: Импортируйте timezone из Django
from django.utils import timezone
from datetime import datetime
Шаг 2: Создайте aware-дату
aware_dt = timezone.make_aware(datetime(2025, 6, 13, 16, 30))
Шаг 3: Применяйте фильтрацию по времени МСК безопасно
Post.objects.filter(publish__gt=aware_dt)
Результат:
Что делает этот запрос
1. datetime(2025, 6, 13, 16, 30) — наивный объект datetime, без часового пояса.
2. timezone.make_aware(...) — делает его timezone-aware с использованием текущей зоны, указанной в settings.py, а именно:
TIME_ZONE = "Europe/Moscow"
USE_TZ = True
aware_dt теперь представляет собой 2025-06-13 16:30:00+03:00 (MSK) — осознанное время с учётом временной зоны.
3. publish__gt=aware_dt — Django сравнивает DateTimeField (publish) строго позже этого момента времени.
WHERE "blog_post"."publish" > '2025-06-13 16:30:00+03:00'::timestamptz
- timestamptz (timestamp with time zone) — PostgreSQL корректно понимает сравнение aware-времени.
- Django автоматически проставил +03:00, потому что aware_dt был создан в зоне "Europe/Moscow".
<QuerySet [
<Post: Лермонтов: Поездка и правда>,
<Post: Маяковский: Арест и правда>,
...
]>
— это все посты, опубликованные позже 16:30 (MSK) 13 июня 2025 года.
✅ Почему в этом примере всё работает корректно
- USE_TZ = True:
В базе publish хранится в UTC.
Django и PostgreSQL делают сравнение корректно — через timestamptz. - make_aware(...):
Обеспечивает, что вы не получите RuntimeWarning о "наивном времени при активной поддержке временных зон".
Конвертация происходит правильно: 2025-06-13 16:30:00+03:00 будет сравниваться с UTC внутри PostgreSQL. - Прозрачность:
Django ORM делает всё за вас: от преобразования в правильную зону до генерации валидного SQL.
Рекомендации на уровне Senior:
- Лучше всегда использовать make_aware или timezone.now() при работе с DateTimeField в USE_TZ = True.
- Для точных фильтров по дате — использовать publish__range вместо publish__date, т.к. __date отбрасывает время и приводит к ::date, отключая индекс.
- На проде — добавить индекс по publish (если его нет):
CREATE INDEX idx_post_publish ON blog_post (publish DESC);
Вот как должен выглядеть код индекса в модели Post:
models.Index(fields=['-publish'], name='idx_post_publish'),
Что происходит под капотом?
- Когда USE_TZ=True, все даты в базе считаются в UTC.
- datetime(2025, 6, 13, 16, 30) считается naive (без tzinfo).
- timezone.make_aware(...) добавляет таймзону (обычно settings.TIME_ZONE, например, Europe/Moscow).
datetime.now()
Альтернатива: использовать timezone.now() для динамики:
Post.objects.filter(publish__gt=timezone.now())
Результат:
Что делает этот запрос
Он выбирает все посты, у которых значение поля publish (тип DateTimeField) строго позже текущего времени, возвращённого timezone.now().
Что вернёт timezone.now()
Поскольку в settings.py у нас:
USE_TZ = True
TIME_ZONE = "Europe/Moscow"
то:
- timezone.now() возвращает aware datetime в UTC (а не в Europe/Moscow);
- PostgreSQL будет сравнивать publish как timestamptz с этим UTC‑временем.
WHERE "blog_post"."publish" > '2025-06-15 14:25:45.428019+00:00'::timestamptz
- Время '2025-06-15 14:25:45+00:00' — это текущий UTC-момент на момент запроса.
- Поле publish тоже хранится как timestamptz, так что PostgreSQL корректно сравнивает их без дополнительных преобразований.
- Индекс idx_post_publish работает здесь эффективно.
<Post: Лермонтов: Поездка и правда>,
<Post: Маяковский: Арест и правда>,
...
— это все посты, у которых publish позже 2025-06-15 14:25:45 UTC
(то есть позже 17:25:45 по Москве, так как Москва = UTC+3).
Время выполнения:
Execution time: 0.003003s
— мгновенно, потому что:
- сравнение по индексу publish;
- нет преобразования даты (::date);
- нет кастомных функций или LIKE.
Что стоит помнить
- Такой запрос — идеальный кандидат для виджета "Будущие публикации", планирования отложенных постов или рассылок.
- Если вы будете использовать этот фильтр часто — ваш индекс -publish уже покрывает сортировку по убыванию, но фильтрация __gt по возрастающему также работает.
Фильтрация по связанным моделям
Один уровень связи: author__username
В Django для фильтрации по связанным моделям (foreign key, one-to-one и т.д.) используется нотация с двумя подчёркиваниями (__).
Пример:
>>> Post.objects.filter(author__username='alex')
Результат:
Что делает этот запрос
Ты выбираешь все объекты модели Post, у которых связанный объект author (внешний ключ на User) имеет username='alex'.
Это классический join-фильтр по связанному объекту с использованием __ в поле фильтра.
Подробности:
- INNER JOIN: выбираются только те посты, у которых author_id соответствует существующему пользователю auth_user.id;
- Фильтрация по auth_user.username = 'alex';
- Результат отсортирован по дате публикации по убыванию;
- Лимит по умолчанию: 21 запись.
Комбинированные фильтры: author__username__startswith
Вы можете использовать дополнительные lookup и для связанных объектов, комбинируя их через двойное подчёркивание (__).
Пример:
>>> Post.objects.filter(author__username__startswith='ale')
Результат:
Что делает запрос
Запрос выбирает все посты, у которых внешний ключ author указывает на пользователя, чьё имя пользователя (username) начинается с "ale" (например, alex, aleksei, alena и т.д.), с учётом регистра.
- Все найденные посты были написаны пользователями, чьи username начинаются с ale (в твоей базе таких пользователей несколько);
- Возможно, автор — это alex или aleksey и т.д
Давайте точно проверим, кто автор?
Post.objects.filter(author__username__startswith='ale').values('author__username').distinct()
Результат:
Комбинирование фильтров
Несколько условий: .filter(...).filter(...)
В Django можно легко фильтровать по нескольким полям одновременно, включая поля самой модели и связанных объектов.
Пример:
>>> Post.objects.filter(publish__year=2025, author__username='alex')
Результат:
Что делает этот запрос
Он выбирает все посты (Post), которые соответствуют двум условиям одновременно:
- publish__year=2025 — дата публикации относится к 2025 году;
- author__username='alex' — автор поста имеет имя пользователя "alex".
Чередование фильтров
В Django результат любого .filter() — это QuerySet, поэтому можно цепочкой применять несколько фильтров, шаг за шагом сужая выборку.
Пример:
>>> Post.objects.filter(publish__year=2025).filter(author__username='alex')
Результат:
Исключения: .exclude(...)
В Django ORM можно исключать объекты из результата запроса с помощью метода .exclude().
Пример:
>>> Post.objects.filter(publish__year=2025).exclude(title__startswith='Пуш')
Результат:
Что делает запрос
Запрос извлекает все посты, которые:
- Были опубликованы в 2025 году.
- Не начинаются с подстроки "Пуш" в заголовке (title).
- Используем .exclude(...) после .filter(): сначала выбираем, потом убираем лишнее.
Сортировка результатов
order_by(): сортировка по полям
Порядок сортировки по умолчанию задаётся в параметре ordering класса Meta модели.
Можно переопределить этот порядок, используя метод order_by() менеджера.
Например, чтобы получить все объекты, отсортированные по их заголовку (title), используйте:
>>> Post.objects.order_by('title')
Результат:
Что делает запрос
Он выбирает все посты, отменяя порядок по умолчанию (ordering = ['-publish'] из Meta), и сортирует их по полю title в алфавитном порядке (по возрастанию).
Порядок сортировки по возрастанию (ascending) используется по умолчанию.
Чтобы указать сортировку по убыванию, можно добавить знак минус - перед именем поля.
Пример:
>>> Post.objects.order_by('-title')
Результат:
Это вернёт посты, отсортированные по полю title в обратном алфавитном порядке — от "Я" к "А".
Можно сортировать по нескольким полям одновременно. В следующем примере объекты сначала сортируются по полю author, а затем — по полю title внутри каждой группы автора.
Пример:
>>> Post.objects.order_by('author', 'title')
order_by('?'): случайный порядок
Чтобы отсортировать объекты в случайном порядке, используй строку '?' в методе order_by, вот так:
>>> Post.objects.order_by('?')
Результат:
Ограничение и выборка(QuerySets)
Срезы и индексация в QuerySet
Можно ограничить количество результатов в QuerySet, используя синтаксис срезов Python (как в массивах).
Например, следующий запрос вернёт только 5 объектов:
>>> Post.objects.all()[:5]
Результат:
Это переводится в SQL-выражение LIMIT 5. Обратите внимание: отрицательные индексы не поддерживаются.
>>> Post.objects.all()[3:6]
Результат:
Что делает этот запрос?
Он извлекает объекты с индексами 3, 4 и 5 из полной выборки Post.objects.all(). Так как ordering = ["-publish"] объекты упорядочены по дате публикации по убыванию (от самого нового к более старому).
Предыдущий пример преобразуется в SQL-запрос с OFFSET 3 LIMIT 6, чтобы вернуть с четвёртого по девятый объекты (всего шесть).
Получение одного объекта по индексу
Чтобы получить один объект, можно использовать индексацию, а не срез. Например, следующий запрос возвращает первый объект из выборки постов в случайном порядке:
>>>Post.objects.order_by('?')[0]
Результат:
Подсчёт и проверка объектов
.count() — подсчёт количества записей
Метод count() подсчитывает общее количество объектов, соответствующих условиям QuerySet, и возвращает целое число. Этот метод преобразуется в SQL-запрос вида SELECT COUNT(*).
Пример ниже возвращает общее число постов, у которых id меньше 3:
>>>Post.objects.filter(id__lt=3).count()
Результат:
.exists() — проверка на наличие
Метод exists() позволяет проверить, содержит ли QuerySet какие-либо результаты.
Он возвращает True, если в QuerySet есть хотя бы один объект, и False — если нет.
Например, вы можете проверить, существуют ли посты с заголовком, начинающимся на "Пуш", следующим образом:
Post.objects.filter(title__startswith='Пуш').exists()
Результат:
Этот метод особенно полезен, если нужно проверить наличие, а не загружать данные из БД. Он генерирует более эффективный SQL-запрос вида SELECT (1) AS "a" FROM ... LIMIT 1.
Удаление объектов
Удаление через .delete()
Если нужно удалить объект, это можно сделать через экземпляр объекта, используя метод delete(), вот так:
post = Post.objects.get(id=333)
post.delete()
post = Post.objects.get(id=333)
Результат:
Сложные запросы с объектами Q
Объединение условий через |, &, ~
Обычно фильтрация через filter() объединяет условия с помощью SQL-оператора AND.
Например:
Post.objects.filter(field1='foo', field2='bar')
получит все объекты, у которых field1 = 'foo' и field2 = 'bar' одновременно.
Если нужно составить более сложные запросы, например, с OR (ИЛИ), используй объекты Q.
Объекты Q позволяют инкапсулировать одно или несколько условий фильтрации и объединять их через операторы:
- & — логическое И (AND)
- | — логическое ИЛИ (OR)
- ^ — исключающее ИЛИ (XOR)
Получим посты, у которых заголовок начинается на пуш или лер (без учёта регистра).
Пример:
from django.db.models import Q
starts_push = Q(title__istartswith='пуш')
starts_ler = Q(title__istartswith='лер')
Post.objects.filter(starts_push | starts_ler)
Результат:
В этом случае используется оператор |, чтобы построить SQL-запрос с условием OR:
WHERE title ILIKE 'пуш%' OR title ILIKE 'лер%'
Это особенно полезно, когда нужно комбинировать разные условия фильтрации в одном QuerySet.
Вы можете подробнее прочитать об объектах Q по ссылке:
👉 https://docs.djangoproject.com/en/5.0/topics/db/queries/#complex-lookups-with-q-objects
Тема называется: «Сложные запросы с использованием объектов Q».
Там подробно объясняется, как использовать Q для создания гибких и выразительных условий фильтрации, особенно при необходимости использовать OR, AND, NOT и комбинации условий.
Как работает Django QuerySet под капотом
Ленивое выполнение (lazy evaluation)
Создание объекта QuerySet не вызывает обращения к базе данных, пока он не будет оценён (evaluated). QuerySet сам по себе является отложенным (ленивым) — он возвращает другой QuerySet, не выполняя SQL-запрос.
Вы можете свободно объединять фильтры и выстраивать цепочки .filter(), .exclude(), .order_by() и другие — SQL-запрос не будет отправлен в базу данных, пока не произойдёт оценка (evaluation).
Когда выполняется SQL-запрос
QuerySet выполняется в следующих случаях:
При первом переборе (итерации), например:
>>> posts = Post.objects.all()[:10]
>>> for post in posts:
... print(post.title)
Результат:
Мы выполнили классический пример ограниченного запроса к базе через Django ORM и получили первые 10 записей из модели Post, отсортированных по дате публикации в порядке убывания (по умолчанию, т.к. в Meta.ordering у нас ['-publish']).
Разберём пошагово.
Команда:
posts = Post.objects.all()[:10]
Что делает:
- Post.objects.all() — создаёт QuerySet всех объектов из таблицы blog_post.
- [:10] — срез QuerySet-а (slice) — Django переводит это в SQL LIMIT 10.
- До этой строки никаких запросов в БД не происходит — Django ленивый (lazy evaluation).
Итерация:
for post in posts:
print(post.title)
Что делает:
- Здесь QuerySet оценился, т.е. выполнился реальный SQL-запрос к базе.
Полезные ссылки о QuerySet
Мы будем использовать QuerySet на протяжении всех примеров проекта blog.
Полную справку по API QuerySet можно найти здесь:
👉 https://docs.djangoproject.com/en/5.0/ref/models/querysets/
Больше о создании запросов с помощью Django ORM читайте здесь:
👉 https://docs.djangoproject.com/en/5.0/topics/db/queries/
Создание кастомных менеджеров моделей
Менеджер по умолчанию для каждой модели — это менеджер objects. Этот менеджер извлекает все объекты из базы данных. Однако вы можете определить собственные (кастомные) менеджеры для моделей.
Давайте создадим пользовательский менеджер, который будет извлекать только посты со статусом PUBLISHED.
Существует два способа добавить или изменить менеджеры моделей:
- Можно добавить дополнительные методы к существующему менеджеру.
- Или создать новый менеджер, изменив начальный QuerySet, который возвращает менеджер.
Первый способ даёт возможность использовать вызовы вроде Post.objects.my_manager(), а второй — использовать нотацию вроде Post.my_manager.all().
Мы выберем второй способ, чтобы реализовать менеджер, который позволит извлекать записи с помощью выражения Post.published.all().
Откройте файл models.py приложения blog и добавьте туда пользовательский менеджер следующим образом (новые строки выделены жирным):
class PublishedManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(status=Post.Status.PUBLISHED)
class Post(models.Model):
# поля модели
# ...
objects = models.Manager() # Менеджер по умолчанию
published = PublishedManager() # Кастомный менеджер
class Meta:
ordering = ['-publish']
indexes = [
models.Index(fields=['-publish']),
]
def __str__(self):
return self.title
Менеджер, объявленный первым в модели, становится менеджером по умолчанию. Вы можете указать другой менеджер по умолчанию с помощью атрибута Meta.default_manager_name.
Если менеджер не определён явно, Django автоматически создаёт менеджер objects. Но если вы объявляете хотя бы один менеджер вручную и хотите сохранить менеджер objects, вы должны добавить его в модель явно, как показано выше.
Метод get_queryset() возвращает QuerySet, который будет выполняться. Мы переопределили этот метод, чтобы создать собственный QuerySet, фильтрующий посты по статусу и возвращающий только те, что имеют статус PUBLISHED.
Теперь у нас есть пользовательский менеджер для модели Post. Давайте его протестируем.
Запустите сервер разработки снова, введя следующую команду в терминале:
python manage.py shell_plus --print-sql
Теперь можно импортировать модель Post и получить все опубликованные посты, заголовок которых начинается с Пуш, используя следующий запрос:
>>> from blog.models import Post
>>> Post.published.filter(title__startswith='Пуш')
Результат:
Уровень Django ORM: что происходит
Мы обращаемся не к стандартному objects, а к кастомному менеджеру, который мы заранее описали как:
Это означает: все последующие фильтры применяются уже к опубликованным статьям.
.filter(title__startswith='Пуш')
Мы добавляем дополнительное условие: заголовок должен начинаться с «Пуш». Django ORM превратит это в SQL-условие с LIKE 'Пуш%'.
Слой SQL: какой запрос сгенерировался
Детали:
- "blog_post"."status" = 'PB' — это Post.Status.PUBLISHED, значение 'PB' указывается в самой модели в choices.
- "title"::text LIKE 'Пуш%' — это реализация startswith='Пуш', то есть поиск по префиксу.
- ORDER BY "publish" DESC — сортировка по дате публикации, как указано в Meta.ordering = ['-publish'].
- LIMIT 21 — Django по умолчанию выставляет LIMIT 21 при выводе queryset в интерактивной оболочке, чтобы не забивать консоль.
Результат:
Мы получили список опубликованных постов, заголовки которых начинаются на «Пуш». Все они отсортированы по дате публикации от новых к старым.
Что можно улучшить?
- Добавить полнотекстовый поиск или pg_trgm, чтобы обрабатывать не только префиксы (startswith), но и неточные совпадения (similarity, icontains).
- Добавить индекс GIN или btree на title, если часто ищем по заголовкам.
- Профилировать через EXPLAIN ANALYZE, если база вырастет — убедиться, что не происходит Seq Scan.
Полнотекстовый поиск с нечётким совпадением через TrigramSimilarity
Давайте добавим полнотекстовый поиск с нечётким совпадением через TrigramSimilarity из django.contrib.postgres.search.
Это мощный инструмент для поиска по заголовкам (и другим текстам), даже если пользователь сделал опечатку или ввёл неполное имя поэта.
Что нужно для работы TrigramSimilarity
1. Включить расширение pg_trgm в базе PostgreSQL:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
Вы можете выполнить это через:
psql -U postgres -d your_database_name -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"
2. Добавить GIN-индекс на поле title:
Это позволяет PostgreSQL эффективно обрабатывать TrigramSimilarity.
3. Создать отдельный файл: blog/querysets.py и добавить поиск в PostQuerySet.
4. Создать отдельный файл: blog/managers.py
5. В файле blog/models.py (фрагмент модели Post)
Как использовать
Теперь можно вызывать метод similar_title() так:
from blog.models import Post
similar_posts = Post.objects.similar_title("пушкин")
for post in similar_posts:
print(post.title, post.similarity)
Результат:
Время исполнения запроса теперь в 10 раз быстрее: 7.7 ms.
Вопросы, которые задают на собеседованиях
🟢 Junior (начальный уровень)
- Что делает метод filter() в Django ORM?
- В чём разница между filter() и get()?
- Что произойдёт, если filter() не найдёт ни одной записи?
- Что вернёт get() при отсутствии объекта? А при наличии нескольких?
- Можно ли использовать filter() без аргументов?
- Что такое field lookup в Django? Пример с title__icontains.
- Чем отличается exact от iexact?
- Для чего нужны contains и icontains?
- Как сделать выборку объектов по списку значений (__in)?
- Как отсортировать записи по возрастанию и убыванию?
🟡 Middle (средний уровень)
- Что происходит на уровне SQL, когда вы используете icontains или startswith?
- Почему icontains может быть медленным? Как это исправить?
- Чем __date=date(...) хуже, чем __range=(start, end)?
- Как правильно работать с DateTimeField, если USE_TZ = True?
- Как фильтровать объекты по дате публикации за январь 2024 года индексируемым способом?
- Что делает метод exclude()? Как его комбинировать с filter()?
- Объясни разницу между filter(...).count() и len(filter(...)).
- Что делает exists() и в каких случаях его использовать?
- Что делает order_by('?')? Когда это может быть плохо?
- Как работает Q-объект? Как объединить условия OR и AND в одном запросе?
🔴 Senior (продвинутый уровень)
- Объясни, когда filter(publish__month=1) отключает индекс. Как сделать индексируемую версию?
- Какие индексы и расширения нужны в PostgreSQL для ускорения поиска с icontains, startswith, iendswith?
- Почему icontains='пушкин' срабатывает медленно на 100k+ записей? Как это профилировать?
- Как работает timezone.make_aware() и зачем он нужен при включённой временной зоне?
- В чём отличие timezone.now() и datetime.now() при USE_TZ = True?
- Когда QuerySet выполняется? Как работает ленивое выполнение (lazy evaluation)?
- Как избежать генерации ::date, чтобы сохранить индекс?
- Что делает Django ORM при обращении к связанному объекту через author__username__icontains? Какой SQL при этом генерируется?
- Какую стратегию ты применишь для оптимизации выборки, если фильтрация идёт по нескольким связанным таблицам?
- Как реализовать полнотекстовый поиск по нескольким полям с использованием Django ORM и PostgreSQL (SearchVector, SearchRank, TrigramSimilarity)?
✅ Заключение
В этой статье мы разобрали весь арсенал фильтрации данных в Django ORM: от базовых выражений (exact, icontains, in) до продвинутой работы с датами, временными зонами, связанными моделями и объектами Q.
Узнали:
- как устроены поисковые выражения в Django и как они транслируются в SQL;
- как профилировать и оптимизировать запросы с учётом индексов PostgreSQL;
- как правильно фильтровать по DateTimeField, когда включена поддержка временных зон (USE_TZ=True);
- как избежать ошибок, которые часто совершают новички — например, потеря индекса из-за __date или медленные icontains без GIN + pg_trgm.
Эти знания критичны не только для производительности, но и для читаемости и поддержки кода в продакшене. Django ORM даёт мощные возможности, но их нужно применять осознанно, с пониманием, что происходит под капотом.
Если ты хочешь двигаться дальше — изучи:
Если статья была полезной
📌 Если статья оказалась полезной — не забудь поставить лайк, чтобы я понял, что такой формат интересен и стоит продолжать.
✍️ Буду рад твоему комментарию: расскажи, какие поисковые выражения используешь чаще всего или с какими были сложности — обсудим вместе.
📤 А если в команде кто-то только осваивает Django ORM — поделись с ним ссылкой. Эта статья поможет сэкономить много времени на практике.
Подписывайся, чтобы не пропустить новые разборы — впереди статьи о продвинутых техниках оптимизации запросов, индексах и полнотекстовом поиске на PostgreSQL 16.
До встречи в следующих публикациях!