Ленивость 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(), чтобы не держать всё в памяти.