filter() и exclude() в Django ORM: lookups, Q и F
Как фильтровать данные в Django ORM: filter и exclude, таблица lookups, фильтр по связям и датам, Q для OR и NOT, F-выражения, distinct и частые ошибки.
Содержание · 12 разделов
- Модели и данные для примеров
- filter() и exclude()
- Таблица lookups
- Фильтр по связям
- Обратные связи и дубликаты
- Фильтр по дате и времени
- Q-объекты: OR, NOT и вложенные условия
- F-выражения: сравнение полей между собой
- Цепочка filter() на многозначных связях
- exclude() и NULL
- Частые ошибки
- FieldError: Cannot resolve keyword into field
- FieldError: Unsupported lookup
- Дубликаты в результате
- Поиск не находит кириллицу
- Фильтр в цикле
- Частые вопросы
- Как передать имя поля в filter() из переменной?
- Как найти записи с пустым полем: NULL или пустой строкой?
- Как отфильтровать по количеству связанных объектов?
- Как фильтровать по ключу в JSONField?
- Как фильтровать по параметрам из URL?
- Итог
Фильтрация в Django ORM строится на двух методах: filter() оставляет записи, подходящие под условие, exclude() — убирает их. Условие записывается как поле__lookup=значение, несколько аргументов объединяются через AND, а для OR и NOT нужны объекты Q:
Book.objects.filter(author__country="RU", rating__gte=4.6)
Book.objects.exclude(author__country="RU")
Book.objects.filter(Q(rating__gte=4.7) | Q(views__gte=12000))
Ниже — полный разбор Django ORM filter: все основные lookups, фильтр по прямым и обратным связям, по датам, Q и F, поведение цепочек на связях «один-ко-многим» и ошибки, которые встречаются чаще всего. Примеры проверены на Django 6.1 и SQLite и работают в Django 5.2 LTS. Обзор остальных методов QuerySet — в шпаргалке по Django ORM.
Модели и данные для примеров
Две модели: автор и книга. Поле pages допускает NULL — это понадобится, чтобы показать ловушку с exclude(). Как описывать поля и связи, разобрано в статье про модели Django.
from django.db import models
from django.utils import timezone
class Author(models.Model):
name = models.CharField(max_length=200)
country = models.CharField(max_length=2, blank=True)
class Book(models.Model):
title = models.CharField(max_length=300)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
published = models.DateField()
rating = models.DecimalField(max_digits=3, decimal_places=1, default=0)
views = models.PositiveIntegerField(default=0)
likes = models.PositiveIntegerField(default=0)
pages = models.PositiveIntegerField(null=True, blank=True)
created_at = models.DateTimeField(default=timezone.now)
В базе шесть книг:
| title | author | published | rating | views | likes | pages |
|---|---|---|---|---|---|---|
| Война и мир | Лев Толстой | 1869-01-01 | 4.8 | 15000 | 2100 | 1300 |
| Анна Каренина | Лев Толстой | 1877-01-01 | 4.6 | 12000 | 900 | 860 |
| Преступление и наказание | Фёдор Достоевский | 1866-01-01 | 4.7 | 14000 | 1600 | 670 |
| Идиот | Фёдор Достоевский | 1869-01-01 | 4.4 | 8000 | 500 | NULL |
| Гордость и предубеждение | Джейн Остин | 1813-01-28 | 4.5 | 9000 | 1200 | 430 |
| Emma | Джейн Остин | 1815-12-23 | 3.9 | 3000 | 150 | NULL |
Есть и четвёртый автор — «Новый автор» без единой книги. В выводе ниже показаны названия найденных книг (или имена авторов) в порядке возврата из базы.
filter() и exclude()
Book.objects.filter(author__country="RU", rating__gte=4.6)
# → ['Война и мир', 'Анна Каренина', 'Преступление и наказание']
Book.objects.exclude(author__country="RU")
# → ['Гордость и предубеждение', 'Emma']
Оба метода возвращают новый QuerySet и не обращаются к базе, пока вы не начнёте перебирать результат. Поэтому их можно сколько угодно комбинировать: qs.filter(...).exclude(...).order_by(...). Исходный QuerySet при этом не меняется:
qs = Book.objects.filter(rating__gte=4.5)
qs2 = qs.exclude(author__country="GB")
# qs → ['Война и мир', 'Анна Каренина', 'Преступление и наказание', 'Гордость и предубеждение']
# qs2 → ['Война и мир', 'Анна Каренина', 'Преступление и наказание']
Чтобы увидеть SQL, выведите qs.query:
print(Book.objects.filter(published__year=1869).query)
# SELECT ... FROM "library_book"
# WHERE "library_book"."published" BETWEEN 1869-01-01 AND 1869-12-31
Учтите: print(qs.query) подставляет параметры без кавычек, поэтому такой текст не всегда можно выполнить в консоли базы как есть. Он нужен, чтобы понять логику запроса.
Таблица lookups
Lookup — это суффикс после двойного подчёркивания. Без суффикса используется exact.
| Lookup | Что проверяет | Пример |
|---|---|---|
exact | равенство; =None превращается в IS NULL | title="Идиот", pages=None |
iexact | равенство без учёта регистра | title__iexact="emma" |
contains, icontains | подстрока (LIKE '%…%') | title__icontains="мир" |
startswith, istartswith | начало строки | title__startswith="Идиот" |
endswith, iendswith | конец строки | title__endswith="мир" |
in | значение из списка или подзапроса | pk__in=[1, 3] |
gt, gte, lt, lte | больше, больше или равно, меньше, меньше или равно | rating__gte=4.5 |
range | диапазон включительно (BETWEEN) | published__range=(d1, d2) |
isnull | IS NULL / IS NOT NULL | pages__isnull=True |
regex, iregex | регулярное выражение | title__regex=r"^[А-Я]" |
date | дата из DateTimeField | created_at__date=date(2026, 5, 6) |
year, iso_year, quarter, month | год, ISO-год, квартал, месяц | published__year=1869 |
week, week_day, iso_week_day | неделя года, день недели | created_at__week_day=2 |
day | день месяца | published__day=1 |
time, hour, minute, second | части времени из DateTimeField | created_at__hour__gte=18 |
Части даты можно комбинировать с операторами сравнения: published__year__lt=1850, created_at__hour__gte=18. Список полный для обычных полей. JSONField добавляет свои lookups — обращение по ключу и has_key, а поля PostgreSQL вроде ArrayField — contains, overlap и другие.
Несколько результатов на наших данных:
Book.objects.filter(title__startswith="Идиот") # → ['Идиот']
Book.objects.filter(pk__in=[1, 3]) # → ['Война и мир', 'Преступление и наказание']
Book.objects.filter(pages__isnull=True) # → ['Идиот', 'Emma']
Book.objects.filter(pages=None) # → ['Идиот', 'Emma'] — то же самое
Book.objects.filter(pk__in=[]) # → [] — база даже не запрашивается
Важно: в SQLite
icontains,iexactи другиеi-lookups не учитывают регистр только для латиницы.Book.objects.filter(title__icontains="война")вернёт пустой список, хотя"Война"найдёт «Войну и мир», аtitle__iexact="emma"сработает. В PostgreSQL регистронезависимый поиск по кириллице работает корректно — ещё одна причина разрабатывать на той же СУБД, что и в продакшне (см. Django и PostgreSQL).
Фильтр по связям
Двойное подчёркивание позволяет пройти по ForeignKey к полям связанной модели — Django сам добавит JOIN:
Book.objects.filter(author__name__startswith="Лев")
# → ['Война и мир', 'Анна Каренина']
Если нужен конкретный объект, передайте его или его ID — это эквивалентные запросы:
tolstoy = Author.objects.get(name="Лев Толстой")
Book.objects.filter(author=tolstoy)
Book.objects.filter(author_id=tolstoy.pk)
В __in можно передать другой QuerySet — он станет подзапросом, а не отдельным обращением к базе:
Book.objects.filter(author__in=Author.objects.filter(country="GB"))
# WHERE "library_book"."author_id" IN (SELECT "U0"."id" FROM "library_author" "U0" WHERE "U0"."country" = GB)
# → ['Гордость и предубеждение', 'Emma']
Обратные связи и дубликаты
В обратную сторону фильтр пишется через related_name (здесь books; без него — имя модели в нижнем регистре, book):
Author.objects.filter(books__title__startswith="Пре") # → ['Фёдор Достоевский']
Author.objects.filter(books__isnull=True) # → ['Новый автор']
У автора может быть несколько подходящих книг, и тогда он попадёт в результат несколько раз — JOIN возвращает строку на каждую пару «автор–книга»:
Author.objects.filter(books__rating__gte=4.5)
# → ['Лев Толстой', 'Лев Толстой', 'Фёдор Достоевский', 'Джейн Остин']
Author.objects.filter(books__rating__gte=4.5).distinct()
# → ['Лев Толстой', 'Фёдор Достоевский', 'Джейн Остин']
distinct() убирает повторы, но добавляет к запросу SELECT DISTINCT, что на больших таблицах стоит ресурсов. Альтернатива без дубликатов — подзапрос: Author.objects.filter(pk__in=Book.objects.filter(rating__gte=4.5).values("author")). В PostgreSQL есть ещё distinct("поле") (DISTINCT ON), но он требует, чтобы order_by() начинался с тех же полей.
Фильтр по дате и времени
Для DateField и DateTimeField работают обычные сравнения и lookups частей даты:
from datetime import date
Book.objects.filter(published__year=1869)
# → ['Война и мир', 'Идиот']
Book.objects.filter(published__range=(date(1860, 1, 1), date(1870, 12, 31)))
# → ['Война и мир', 'Преступление и наказание', 'Идиот']
published__year=1869 Django превращает в BETWEEN 1869-01-01 AND 1869-12-31 (это видно в SQL выше), поэтому индекс по полю продолжает работать.
Для DateTimeField есть __date, __hour и другие части времени. «Записи за последние 7 дней» выбираются сравнением с текущим моментом:
from datetime import timedelta
from django.utils import timezone
Book.objects.filter(created_at__gte=timezone.now() - timedelta(days=7))
Book.objects.filter(created_at__date=date(2026, 5, 6)) # → ['Emma']
Book.objects.filter(created_at__hour__gte=18) # → ['Анна Каренина', 'Идиот']
Book.objects.filter(created_at__week_day=2) # → ['Война и мир', 'Идиот']
Две особенности:
- Часовой пояс. При
USE_TZ = Truelookups__date,__year,__hourи т. д. считаются в текущем часовом поясе (по умолчаниюTIME_ZONEиз настроек). Книга «Emma» создана 6 мая в 01:30 по Москве — это 5 мая 22:30 по UTC. СTIME_ZONE = "Europe/Moscow"её находитcreated_at__date=date(2026, 5, 6), а внутриtimezone.override("UTC")— ужеdate(2026, 5, 5). Подробнее о датах и часовых поясах в Python — в статье про datetime. - Нумерация дней недели.
week_dayсчитает от воскресенья: 1 — воскресенье, 2 — понедельник, …, 7 — суббота. Если удобнее ISO-нумерация (1 — понедельник), используйтеiso_week_day.
Q-объекты: OR, NOT и вложенные условия
Q упаковывает условие в объект, который можно комбинировать операторами | (OR), & (AND) и ~ (NOT):
from django.db.models import Q
Book.objects.filter(Q(rating__gte=4.7) | Q(views__gte=12000))
# → ['Война и мир', 'Анна Каренина', 'Преступление и наказание']
Book.objects.filter(~Q(author__country="RU"))
# → ['Гордость и предубеждение', 'Emma']
Скобки задают приоритет, как в обычной логике:
Book.objects.filter(
Q(author__country="RU") & (Q(rating__gte=4.7) | Q(pages__isnull=True))
)
# WHERE ("library_author"."country" = RU AND ("library_book"."rating" >= 4.70 OR "library_book"."pages" IS NULL))
# → ['Война и мир', 'Преступление и наказание', 'Идиот']
Q и обычные именованные аргументы можно смешивать, но Q должен идти первым — так требует синтаксис Python (позиционные аргументы раньше именованных):
Book.objects.filter(Q(author__country="GB"), rating__gte=4) # → ['Гордость и предубеждение']
Book.objects.filter(rating__gte=4, Q(author__country="GB")) # SyntaxError
Условие из списка значений удобно собрать через functools.reduce, например поиск по нескольким словам:
import operator
from functools import reduce
words = ["мир", "Идиот", "Emma"]
cond = reduce(operator.or_, (Q(title__contains=w) for w in words))
print(cond)
# (OR: ('title__contains', 'мир'), ('title__contains', 'Идиот'), ('title__contains', 'Emma'))
Book.objects.filter(cond) # → ['Война и мир', 'Идиот', 'Emma']
F-выражения: сравнение полей между собой
Справа от lookup обычно стоит константа. Чтобы сравнить одно поле с другим, используйте F() — значение возьмётся из той же строки в базе:
from django.db.models import F
# Лайков больше, чем 10 % просмотров
Book.objects.filter(likes__gt=F("views") / 10)
# → ['Война и мир', 'Преступление и наказание', 'Гордость и предубеждение']
# Просмотров хотя бы в 10 раз больше, чем лайков
Book.objects.filter(views__gte=F("likes") * 10)
# → ['Анна Каренина', 'Идиот', 'Emma']
F тоже ходит по связям: F("author__name"). Деление целого поля на целое число в SQLite и PostgreSQL отбрасывает дробную часть: при views = 7 выражение F("views") / 2 даст 3. В MySQL результат будет дробным. Чтобы получить дробь в любой СУБД, делите на число с точкой (F("views") / 2.0) или приведите поле через Cast("views", FloatField()). Про F в update() для атомарного увеличения счётчиков — в шпаргалке по QuerySet.
Цепочка filter() на многозначных связях
Для обычных полей filter(a).filter(b) и filter(a, b) дают одинаковый результат. На обратных ForeignKey и ManyToMany — нет, и это частый источник ошибок.
Задача: найти авторов, у которых есть книга 1869 года с рейтингом от 4.7.
# Оба условия к ОДНОЙ и той же книге
Author.objects.filter(books__published__year=1869, books__rating__gte=4.7)
# → ['Лев Толстой']
# Условия могут выполниться на РАЗНЫХ книгах
Author.objects.filter(books__published__year=1869).filter(books__rating__gte=4.7)
# → ['Лев Толстой', 'Фёдор Достоевский']
У Достоевского есть «Идиот» 1869 года (4.4) и «Преступление и наказание» с рейтингом 4.7 — это разные книги. Второй filter() создаёт отдельный JOIN, поэтому каждое условие проверяется независимо. Если условия относятся к одному связанному объекту, пишите их в одном вызове filter().
exclude() с несколькими условиями на многозначной связи ведёт себя как цепочка, а не как один filter():
Author.objects.exclude(books__published__year=1869, books__rating__gte=4.7)
# → ['Джейн Остин', 'Новый автор'] — Достоевский тоже исключён
Чтобы исключить только авторов, у которых одна и та же книга подходит под оба условия, используйте подзапрос:
Author.objects.exclude(
books__in=Book.objects.filter(published__year=1869, rating__gte=4.7)
)
# → ['Фёдор Достоевский', 'Джейн Остин', 'Новый автор']
exclude() и NULL
exclude() — не точная инверсия filter(), если в поле бывает NULL:
Book.objects.filter(pages__gte=500)
# → ['Война и мир', 'Анна Каренина', 'Преступление и наказание']
Book.objects.exclude(pages__lt=500)
# → ['Война и мир', 'Анна Каренина', 'Преступление и наказание', 'Идиот', 'Emma']
Django строит условие WHERE NOT (pages < 500 AND pages IS NOT NULL), поэтому книги без количества страниц остаются в выборке. Если они не нужны, добавьте .exclude(pages__isnull=True) или сформулируйте условие через filter().
Частые ошибки
FieldError: Cannot resolve keyword into field
Опечатка в имени поля или попытка фильтровать по свойству (@property) либо методу модели — ORM видит только поля базы:
FieldError: Cannot resolve keyword 'name' into field. Choices are: author, author_id, created_at, id, likes, pages, published, rating, title, views
В сообщении перечислены доступные имена. Вычисляемое значение сначала добавьте через annotate(), а потом фильтруйте по нему.
FieldError: Unsupported lookup
Unsupported lookup 'more' for DecimalField or join on the field not permitted — lookup с таким именем не существует (правильно gt/gte) или вы пытаетесь пройти через поле, которое не является связью.
Дубликаты в результате
Фильтр по обратной связи или ManyToMany вернул один объект несколько раз. Добавьте distinct() или перепишите условие через pk__in с подзапросом.
Поиск не находит кириллицу
icontains на SQLite регистронезависим только для ASCII. Для кириллицы используйте PostgreSQL или храните отдельное поле в нижнем регистре.
Фильтр в цикле
for author in authors: Book.objects.filter(author=author) — отдельный запрос на каждого автора. Используйте author__in=authors или prefetch_related — подробно в статье о проблеме N+1.
Частые вопросы
Как передать имя поля в filter() из переменной?
Соберите условия в словарь и распакуйте его в именованные аргументы:
field = "title"
Book.objects.filter(**{f"{field}__icontains": "мир"})
Если имя поля приходит от пользователя, сверяйте его со списком разрешённых: иначе можно получить FieldError или фильтр по полю, которое не должно быть доступно.
Как найти записи с пустым полем: NULL или пустой строкой?
isnull проверяет только NULL, пустую строку ищут через exact. Если в колонке встречается и то и другое, объедините условия через Q:
Author.objects.filter(Q(country="") | Q(country__isnull=True))
Как отфильтровать по количеству связанных объектов?
Сначала посчитайте связи через annotate(), затем фильтруйте по аннотации как по обычному полю:
from django.db.models import Count
Author.objects.annotate(books_count=Count("books")).filter(books_count__gte=2)
# → ['Лев Толстой', 'Фёдор Достоевский', 'Джейн Остин']
Условие на аннотацию Django переносит в SQL HAVING.
Как фильтровать по ключу в JSONField?
Ключи JSON указывают через двойное подчёркивание, как поля связанной модели. Пример для модели с полем attributes = models.JSONField(default=dict); запросы работают во всех поддерживаемых СУБД, включая SQLite:
Product.objects.filter(attributes__cover="hard")
Product.objects.filter(attributes__has_key="lang")
Lookup contains для JSONField в SQLite и Oracle не поддерживается.
Как фильтровать по параметрам из URL?
Читайте значения из request.GET и добавляйте условие, только если параметр передан:
def book_list(request):
books = Book.objects.select_related("author")
if q := request.GET.get("q"):
books = books.filter(title__icontains=q)
if country := request.GET.get("country"):
books = books.filter(author__country=country)
...
Когда параметров много, удобнее библиотека django-filter: она строит фильтры и форму по описанию полей. Для API на DRF она подключается как бэкенд фильтрации — пример в руководстве по Django REST Framework.
Итог
filter() и exclude() с lookups закрывают большинство задач выборки, Q добавляет OR и NOT, а F позволяет сравнивать поля между собой. Главные подводные камни — дубликаты при фильтре по обратным связям, разная семантика filter(a, b) и filter(a).filter(b) на многозначных связях и NULL в exclude().
Дальше пригодятся шпаргалка по QuerySet с сортировкой, агрегацией и обновлением и разбор оптимизации запросов N+1. Полный список lookups — в документации Django.
- #Django ORM
- #QuerySet
- #filter
- #Q-объекты
- #F-выражения