Фильтрация - lookups | Курс Django ORM урок 2.3
Цель урока
Разобрать все основные lookup'ы Django ORM, виды условий фильтрации, которые транслируются в операторы SQL. Понять как строить условия через связанные модели с помощью двойного подчеркивания.
Необходимые знания
- Урок 2.2: QuerySet API, filter(), exclude()
- Базовое понимание WHERE в SQL
Что такое lookup
Lookup это суффикс после имени поля через двойное подчеркивание, который задает оператор сравнения:
field__lookup=value
Product.objects.filter(price__gt=500)
# ^^^^^ ^^
# поле lookup
Без lookup'а Django использует exact по умолчанию:
Product.objects.filter(is_active=True)
# эквивалентно
Product.objects.filter(is_active__exact=True)
Сравнение значений
exact и iexact
# Точное совпадение (регистрозависимое)
Product.objects.filter(name__exact="iPhone 15")
Product.objects.filter(name="iPhone 15") # то же самое
WHERE name = 'iPhone 15'
# Точное совпадение без учета регистра
Product.objects.filter(name__iexact="iphone 15")
WHERE "shop_product"."name" ILIKE 'iphone 15'
iexact использует оператор ILIKE (PostgreSQL). Это не позволяет использовать обычный B-tree индекс. Для регистронезависимого поиска на больших таблицах лучше хранить данные в нижнем регистре и использовать exact.
Числовые сравнения
Product.objects.filter(price__gt=500) # >
Product.objects.filter(price__gte=500) # >=
Product.objects.filter(price__lt=1000) # <
Product.objects.filter(price__lte=1000) # <=
WHERE price > 500
WHERE price >= 500
WHERE price < 1000
WHERE price <= 1000
Комбинируются в одном filter():
Product.objects.filter(price__gte=500, price__lte=1000)
WHERE price >= 500 AND price <= 1000
range
# Включительно с обеих сторон, эквивалент BETWEEN
Product.objects.filter(price__range=(500, 1000))
WHERE price BETWEEN 500 AND 1000
range работает с числами, датами, строками, любым упорядоченным типом:
from datetime import date
Order.objects.filter(created_at__date__range=(date(2026, 1, 1), date(2026, 3, 31)))
Поиск по строкам
# Содержит подстроку (регистрозависимо)
Product.objects.filter(name__contains="Pro")
WHERE name LIKE '%Pro%'
# Содержит подстроку (регистронезависимо)
Product.objects.filter(name__icontains="pro")
WHERE "shop_product"."name" ILIKE '%pro%'
# Начинается с
Product.objects.filter(name__startswith="iPhone") # регистрозависимо
Product.objects.filter(name__istartswith="iphone") # регистронезависимо
WHERE "shop_product"."name" LIKE 'iPhone%'
WHERE "shop_product"."name" ILIKE 'iphone%'
# Заканчивается на
Product.objects.filter(name__endswith="Pro")
Product.objects.filter(name__iendswith="pro")
WHERE "shop_product"."name" LIKE '%Pro'
WHERE "shop_product"."name" ILIKE '%pro'
Производительность строковых lookups
LIKE '%text%' с ведущим % не использует стандартный B-tree индекс, PostgreSQL вынужден делать seq scan. Это приемлемо для небольших таблиц, но на больших нужно либо:
startswith/istartswith, они используют индекс (нет ведущего%)- Full-text search через
SearchVector(урок 7.2), для полнотекстового поиска pg_trgmиндекс дляcontains/icontains
Списки и множества
in
# Поле входит в список значений
Product.objects.filter(status__in=["active", "featured"])
Product.objects.filter(id__in=[1, 5, 10, 23])
WHERE status IN ('active', 'featured')
WHERE id IN (1, 5, 10, 23)
in принимает любой итерируемый объект, включая QuerySet:
# Subquery, продукты из категорий у которых есть товары
active_categories = Category.objects.filter(products__isnull=False).distinct()
Product.objects.filter(category__in=active_categories)
WHERE category_id IN (SELECT id FROM shop_category WHERE ...)
Django генерирует подзапрос, а не загружает ids в память. Подробнее о подзапросах, в уроке 3.4.
Значения NULL
# IS NULL
Product.objects.filter(description__isnull=True)
# IS NOT NULL
Product.objects.filter(description__isnull=False)
WHERE description IS NULL
WHERE description IS NOT NULL
Для ForeignKey isnull=True находит объекты без связанного объекта:
# Категории без родителя (корневые)
Category.objects.filter(parent__isnull=True)
WHERE parent_id IS NULL
Фильтрация по датам
Django предоставляет lookup'ы для разбивки дат по компонентам:
from datetime import date
# По конкретной дате (без времени)
Order.objects.filter(created_at__date=date(2026, 1, 15))
# По году
Order.objects.filter(created_at__year=2026)
# По месяцу
Order.objects.filter(created_at__month=1)
# По дню
Order.objects.filter(created_at__day=15)
# По дню недели (1=воскресенье, 7=суббота в PostgreSQL)
Order.objects.filter(created_at__week_day=2) # понедельник
# По номеру недели
Order.objects.filter(created_at__week=3)
# По кварталу
Order.objects.filter(created_at__quarter=1)
# По времени
Order.objects.filter(created_at__hour=12)
Order.objects.filter(created_at__minute=30)
-- created_at__date=date(2026, 1, 15)
WHERE DATE(created_at AT TIME ZONE 'UTC') = '2026-01-15'
-- created_at__year=2026
WHERE DATE_PART('year', created_at AT TIME ZONE 'UTC') = 2026
Компоненты можно комбинировать:
# Все заказы января 2026 года
Order.objects.filter(created_at__year=2026, created_at__month=1)
Фильтрация по времени без компонентов
# Заказы за последние 7 дней
from django.utils import timezone
from datetime import timedelta
week_ago = timezone.now() - timedelta(days=7)
Order.objects.filter(created_at__gte=week_ago)
WHERE created_at >= '2026-01-08 10:00:00+00'
Это эффективнее компонентных lookup'ов, позволяет использовать индекс на created_at.
Фильтрация через связанные модели
Двойное подчеркивание выполняет две разные функции в одном синтаксисе:
1) Переход по связи - field__related_field переходит на связанную модель через JOIN
2) Lookup - field__lookup задает оператор сравнения
Django разбирает строку слева направо и сам определяет, что перед ним, имя связи или lookup. Если category это ForeignKey, то category__slug означает JOIN на таблицу категорий и фильтр по полю slug. Если после двойного подчеркивания стоит известный lookup (gt, in, isnull и т.д.) это оператор. Если нет, то это поле связанной модели.
Переходов может быть несколько в одной строке:
Product -> category -> parent -> slug
Product.objects.filter(category__parent__slug="electronics")
# ^ ^ ^
# FK FK поле + lookup exact
Каждый переход через FK или обратную FK добавляет JOIN в SQL.
Прямая FK - фильтруете по полю связанной модели:
# FK: продукты из категории с slug="phones"
Product.objects.filter(category__slug="phones")
SELECT shop_product.*
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
WHERE shop_category.slug = 'phones'
Обратная FK - фильтруете "родителя" через "детей". Django создает обратный менеджер с именем modelname_set, но в filter() используется related_name или имя модели в нижнем регистре:
# Обратная FK: категории у которых есть активные продукты
Category.objects.filter(products__is_active=True)
SELECT shop_category.*
FROM shop_category
INNER JOIN shop_product ON shop_category.id = shop_product.category_id
WHERE shop_product.is_active = true
M2M аналогично, работает в обе стороны через промежуточную таблицу:
# M2M: продукты с тегом "sale"
Product.objects.filter(tags__slug="sale")
# Через M2M обратно: теги продуктов дороже 1000
Tag.objects.filter(products__price__gt=1000)
Многоуровневый переход:
# Двухуровневый переход: продукты из подкатегорий "electronics"
Product.objects.filter(category__parent__slug="electronics")
SELECT shop_product.*
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
INNER JOIN shop_category parent ON shop_category.parent_id = parent.id
WHERE parent.slug = 'electronics'
Django автоматически строит цепочку JOIN'ов.
Фильтрация через обратную связь и дубли
При фильтрации через обратную FK или M2M Django может вернуть дублирующиеся объекты:
# Если у категории несколько активных продуктов, категория вернется несколько раз
Category.objects.filter(products__is_active=True)
SELECT shop_category.*
FROM shop_category
INNER JOIN shop_product ON shop_category.id = shop_product.category_id
WHERE shop_product.is_active = true
-- категория с 5 продуктами появится 5 раз
Можно использовать distinct() для решения этой проблемы, чтобы убрать дубли:
Category.objects.filter(products__is_active=True).distinct()
SELECT DISTINCT shop_category.*
FROM shop_category
INNER JOIN shop_product ON shop_category.id = shop_product.category_id
WHERE shop_product.is_active = true
Составные условия через цепочку filter()
Несколько условий в одном filter() работают как AND:
# AND: активные продукты с ценой > 500
Product.objects.filter(is_active=True, price__gt=500)
Но при фильтрации через обратные связи несколько filter() в цепочке ведут себя иначе, чем один filter() с несколькими условиями:
# Один filter() один JOIN: продукты у которых ОДИН И ТОТ ЖЕ отзыв имеет rating>=4 и user_id=1
Product.objects.filter(reviews__rating__gte=4, reviews__user_id=1)
# Два filter() два JOIN: продукты у которых ЕСТЬ отзыв с rating>=4 И ЕСТЬ отзыв от user_id=1
# (это могут быть разные отзывы!)
Product.objects.filter(reviews__rating__gte=4).filter(reviews__user_id=1)
Это тонкий момент. Для простых полей модели разницы нет, оба варианта дают одинаковый SQL. Но для обратных связей и M2M, разница принципиальна. Более детально разберем в уроке 3.1 при изучении Q objects.
Полный список основных lookups
| Lookup | SQL | Пример |
|---|---|---|
exact |
= value |
name__exact="iPhone" |
iexact |
ILIKE 'value' |
name__iexact="iphone" |
gt |
> value |
price__gt=500 |
gte |
>= value |
price__gte=500 |
lt |
< value |
price__lt=1000 |
lte |
<= value |
price__lte=1000 |
range |
BETWEEN a AND b |
price__range=(500, 1000) |
in |
IN (...) |
id__in=[1, 2, 3] |
contains |
LIKE '%v%' |
name__contains="Pro" |
icontains |
ILIKE '%v%' |
name__icontains="pro" |
startswith |
LIKE 'v%' |
name__startswith="iPhone" |
istartswith |
ILIKE 'v%' |
name__istartswith="iphone" |
endswith |
LIKE '%v' |
name__endswith="Pro" |
iendswith |
ILIKE '%v' |
name__iendswith="pro" |
isnull |
IS NULL / IS NOT NULL |
parent__isnull=True |
date |
DATE(field) |
created_at__date=date.today() |
year |
DATE_PART('year', ...) |
created_at__year=2026 |
month |
DATE_PART('month', ...) |
created_at__month=1 |
day |
DATE_PART('day', ...) |
created_at__day=15 |
week |
DATE_PART('week', ...) |
created_at__week=3 |
quarter |
DATE_PART('quarter', ...) |
created_at__quarter=1 |
hour |
DATE_PART('hour', ...) |
created_at__hour=12 |
Практическое задание
Выполняйте в shell с логированием SQL.
-
Найдите все продукты с ценой от 300 до 800 включительно двумя способами: через
filter(price__gte=..., price__lte=...)и черезfilter(price__range=...). Убедитесь что SQL одинаковый. -
Найдите все заказы, созданные в январе 2026 года. Найдите все заказы, созданные за последние 30 дней.
-
Найдите все категории, у которых есть хотя бы один продукт. Проверьте, возвращает ли запрос дубли. Исправьте с помощью
distinct(). -
Найдите все продукты, у которых в имени есть слово "Pro" (регистронезависимо). Посмотрите на SQL, почему этот запрос не использует индекс?
-
Найдите все продукты из категорий, которые являются дочерними для "Electronics" (то есть
parent__slug="electronics"). Посмотрите на SQL, сколько JOIN'ов?
Возможные ошибки
icontains на большой таблице без специального индекса
# Медленно на больших таблицах, seq scan
Product.objects.filter(name__icontains="phone")
# Для полнотекстового поиска используйте SearchVector (урок 7.2)
# или pg_trgm индекс
Забыть distinct() при фильтрации через обратные связи
# Возвращает дубли если у категории несколько продуктов
Category.objects.filter(products__price__gt=500)
# Правильно
Category.objects.filter(products__price__gt=500).distinct()
Путаница с один filter() vs два filter() при обратных связях
# Ищет продукты с отзывом rating>=4 ОТ пользователя 1
Product.objects.filter(reviews__rating__gte=4, reviews__user_id=1)
# Ищет продукты с отзывом rating>=4 И (возможно другим) отзывом от пользователя 1
Product.objects.filter(reviews__rating__gte=4).filter(reviews__user_id=1)
Оба синтаксически корректны, но возвращают разные результаты.
Использование компонентных date-lookups вместо range
# Применяет функцию к каждой строке
Order.objects.filter(created_at__year=2026, created_at__month=1)
# Лучше, индекс на created_at работает
from datetime import datetime, timezone
Order.objects.filter(
created_at__range=(
datetime(2026, 1, 1, tzinfo=timezone.utc),
datetime(2026, 1, 31, 23, 59, 59, tzinfo=timezone.utc),
)
)
Связь со следующим уроком
В уроке 2.4 разберем order_by(), distinct(), срезы QuerySet и пагинацию. Посмотрим как LIMIT и OFFSET транслируются в SQL и почему пагинация с помощью OFFSET неэффективна на больших таблицах.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru