Ленивость QuerySet: когда выполняется запрос

QuerySet не ходит в базу, пока вы не попросите данные. Понимание этого момента отделяет код, который делает один запрос, от кода, который делает их десятки.

Ленивый QuerySet — объект, который описывает запрос, но не выполняет его до тех пор, пока результат реально не понадобится (итерация, срез по индексу, len(), приведение к списку и т.п.).

Из базового курса вы помните: User.objects.filter(is_active=True) возвращает QuerySet. Легко решить, что в этот момент Django уже сходил в базу и принёс пользователей. Это не так. QuerySet — это рецепт запроса, а не сами данные. SQL генерируется и отправляется в СУБД только тогда, когда вы заставляете QuerySet «материализоваться». До этого его можно сколько угодно фильтровать, сортировать и срезать — всё это лишь уточняет рецепт.

Зачем это на практике

Ленивость экономит запросы и память. Вы можете собрать запрос по частям в разных функциях, передать QuerySet в шаблон, навесить пагинацию — и всё это не стоит ни одного похода в базу, пока шаблон не начнёт перебирать строки. Обратная сторона: если не понимать, когда именно происходит обращение, легко случайно выполнить один и тот же тяжёлый запрос несколько раз или, наоборот, удивиться, почему изменение в базе «не видно» в уже вычисленном QuerySet.

Что заставляет QuerySet выполниться

Запрос уходит в базу в момент, когда Django нужны конкретные данные. Главные триггеры:

ДействиеВыполняет запрос?
qs = Article.objects.filter(...)нет — только строит рецепт
qs = qs.exclude(...).order_by(...)нет — уточняет рецепт
цикл for a in qs:да — итерация материализует
list(qs)да
len(qs)да (тянет все строки)
qs[0] (индекс)да (с LIMIT 1)
qs[5:10] (срез)нет — добавляет LIMIT/OFFSET в рецепт
if qs: / bool(qs)да
qs.count()да (через SELECT COUNT(*))
qs = Article.objects.filter(published=True)   # запроса ещё НЕТ
qs = qs.exclude(author__is_banned=True)        # запроса всё ещё НЕТ
qs = qs.order_by("-created_at")                # и сейчас НЕТ

for article in qs:                             # ВОТ ЗДЕСЬ уходит один SQL
    print(article.title)

Важная пара методов: count() против len(). Если вам нужно только число строк — берите count(): он превращается в SELECT COUNT(*) и не тащит строки в Python. len(qs) загрузит все объекты в память и посчитает их там — это лишняя работа, если сами объекты не нужны.

Кэш результата: один QuerySet — один запрос

После первого выполнения QuerySet запоминает результат в своём внутреннем кэше. Повторный перебор того же объекта в базу уже не ходит:

qs = Article.objects.filter(published=True)

for a in qs:        # запрос №1 — результат сохранён в кэше qs
    pass
for a in qs:        # запроса НЕТ — берём из кэша
    pass

print(len(qs))      # тоже из кэша, без запроса

А вот это — классическая ловушка двух запросов: каждый новый QuerySet кэшируется отдельно.

articles = Article.objects.filter(published=True)

count = articles.count()              # запрос №1: SELECT COUNT(*)
first_titles = [a.title for a in articles[:5]]  # запрос №2: SELECT ... LIMIT 5

Здесь два разных похода в базу. Если бы нужно было и число, и список объектов, дешевле один раз материализовать: items = list(articles), а потом len(items) и items[:5] уже по списку в памяти. Главное правило: переиспользуйте один и тот же вычисленный QuerySet, а не создавайте похожие заново.

Цепочки фильтров: filter().filter() ≠ filter(a, b)

Каждый вызов filter(), exclude(), order_by() возвращает новый QuerySet, не меняя исходный. Это позволяет строить запрос постепенно. Но для связанных моделей есть тонкость, которая удивляет почти всех.

Article.objects.filter(tags__name="django", tags__name="orm")
# почти всегда вернёт пусто: одна и та же строка-связь не может
# иметь tag=django И tag=orm одновременно

Article.objects.filter(tags__name="django").filter(tags__name="orm")
# вернёт статьи, у которых ЕСТЬ тег django И ЕСТЬ тег orm
# (два разных JOIN — условия применяются к разным строкам связи)

Для отношений «многие» (M2M и обратные FK) два отдельных filter() создают два независимых JOIN, тогда как один filter(a, b) требует, чтобы одна связанная строка удовлетворяла обоим условиям сразу. Для полей самой модели разницы нет — там оба варианта эквивалентны.

Сужаем выборку: only, defer, values, values_list

По умолчанию SELECT тянет все колонки модели. Если строк много, а нужны два-три поля, это лишний трафик и память. Инструменты сужения:

МетодЧто делаетЧто возвращает
only("a", "b")грузит только эти поляобъекты модели
defer("big")грузит всё, КРОМЕ указанныхобъекты модели
values("a", "b")выбирает полясловари
values_list("a")выбирает полякортежи
Article.objects.only("id", "title")          # объекты, но в SELECT лишь id, title
Article.objects.defer("body")                # всё, кроме тяжёлого body
Article.objects.values("id", "title")        # [{"id": 1, "title": "..."}, ...]
Article.objects.values_list("title", flat=True)  # ["Заголовок 1", "Заголовок 2", ...]

Ключевое отличие: only/defer возвращают полноценные объекты модели — у них работают методы и свойства. Но осторожно: обращение к не загруженному полю у only-объекта вызовет дополнительный запрос за этим полем (отложенная загрузка). А values/values_list возвращают «сырые» словари и кортежи — без методов модели, зато легче и быстрее, когда нужны только данные для JSON или отчёта.

Так выглядит SQL, который сгенерирует only — для чтения, не для запуска:

SELECT "article"."id", "article"."title"
FROM "article"
WHERE "article"."published" = 1;

Как это работает под капотом

QuerySet хранит внутри объект Query — древовидное описание будущего SQL (какие таблицы, условия WHERE, сортировка, лимиты). Методы вроде filter() копируют QuerySet и дописывают узлы в это дерево, поэтому исходный объект не меняется, а цепочки безопасны. SQL-строка собирается компилятором ровно в момент материализации. Результат после выполнения кладётся в атрибут _result_cache — это и есть тот самый кэш, из-за которого повторный перебор бесплатен. Срез qs[5:10] до выполнения просто меняет LIMIT/OFFSET в дереве; срез после выполнения режет уже закэшированный список в памяти. А для гигантских выборок, где кэшировать всё опасно, есть iterator() — он читает строки порциями и кэш не наполняет.

Частые ошибки

  • Считать строки через len(qs). Это загружает все объекты в память ради числа. Нужен только счёт — берите qs.count().
  • Думать, что filter() меняет QuerySet на месте. Он возвращает новый объект; qs.filter(...) без присваивания результата ничего не «сохранит».
  • Выполнять похожие запросы вместо переиспользования. articles.count() и затем list(articles[:5]) — два запроса; часто дешевле один раз list(articles).
  • Обращаться к полю, исключённому через only/defer. Каждое такое обращение тянет отдельный запрос за полем — в цикле это снова N+1.
  • Ждать, что вычисленный QuerySet «увидит» свежие изменения в базе. После материализации он отдаёт данные из кэша; нужен свежий результат — создайте новый QuerySet.

Итоги

  • QuerySet ленив: SQL уходит в базу только при итерации, list(), len(), индексе, bool() или count() — но не при filter() и срезе.
  • Выполненный QuerySet кэширует результат; повторный перебор того же объекта запросов не делает.
  • Каждый filter() возвращает новый QuerySet; для связей «многие» два отдельных filter() не равны одному filter(a, b).
  • only/defer сужают список колонок, оставляя объекты модели; values/values_list отдают словари и кортежи без накладных расходов на объекты.
  • Для числа строк используйте count(), для огромных выборок — iterator(), чтобы не держать всё в памяти.
Проверьте себя
1. В какой момент QuerySet Article.objects.filter(published=True).order_by("-id") отправит SQL-запрос в базу?
AСразу при вызове filter()
BПри вызове order_by()
CТолько когда результат понадобится — например, при итерации в цикле for или list()
DНикогда, пока не вызвать метод .execute()
2. Чем filter(tags__name="a").filter(tags__name="b") отличается от filter(tags__name="a", tags__name="b") для связи многие-ко-многим?
AНичем, это полностью эквивалентные записи
BЦепочка из двух filter() создаёт два JOIN и требует наличия обоих тегов у объекта, а один filter с двумя условиями ищет одну строку-связь с обоими значениями сразу (почти всегда пусто)
CВторой вариант быстрее, но даёт тот же результат
DПервый вариант вернёт пустой результат всегда
3. Зачем использовать values_list("title", flat=True) вместо only("title")?
Avalues_list возвращает полноценные объекты модели с методами
BОни идентичны по результату
Cvalues_list отдаёт простой список значений без накладных расходов на объекты модели, когда методы модели не нужны
Donly("title") вообще не делает запрос