only(), defer() и iterator() | Курс Django ORM урок 5.3
Цель урока
Научиться контролировать какие поля загружаются из БД и как читать большие датасеты без переполнения памяти. Разобрать only(), defer() и iterator(), инструменты, которые становятся критически важными при работе с таблицами на миллионы строк.
Необходимые знания
- Урок 2.2: QuerySet API, only(), defer()
- Урок 5.2: EXPLAIN и анализ запросов
- Базовое понимание работы памяти Python
Проблема: Django загружает все поля
По умолчанию Django загружает все поля модели:
products = Product.objects.filter(is_active=True)
for p in products:
print(p.name) # нужно только name
SELECT id, name, slug, description, price, stock, is_active, created_at, updated_at, category_id
FROM shop_product
WHERE is_active = true
ORDER BY name;
Если у Product есть поле description TEXT с длинными текстами по 5-10 кБ на строку, и нам нужно только name, мы всё равно тащим все данные. На 100 000 строк это десятки мегабайт лишних данных.
Вторая проблема, Django загружает весь QuerySet в память перед итерацией. 100 000 объектов Python в памяти одновременно могут занять сотни мегабайт.
only() - загрузить только указанные поля
only() говорит Django загрузить только перечисленные поля. Остальные поля становятся "отложенными" (deferred).
# Загрузить только name и price
products = Product.objects.only("name", "price")
SELECT id, name, price
FROM shop_product
ORDER BY name;
id всегда загружается, это первичный ключ, он нужен Django для идентификации объекта.
Доступ к отложенному полю
Если обратиться к полю, которое не было загружено, Django выполнит дополнительный SELECT:
products = Product.objects.only("name", "price")
for p in products:
print(p.name) # OK - загружено
print(p.price) # OK - загружено
print(p.stock) # дополнительный SELECT для каждого объекта!
-- Основной запрос
SELECT id, name, price FROM shop_product ORDER BY name;
-- Для каждого объекта при обращении к stock:
SELECT stock FROM shop_product WHERE id = 1;
SELECT stock FROM shop_product WHERE id = 2;
-- ... N запросов
Это N+1 в чистом виде. only() без контроля за доступными полями может ухудшить ситуацию, а не улучшить.
Правило: если вы не уверены, что код никогда не обратится к другим полям, используйте values() или values_list(), а не only().
only() с select_related
# Только name продукта и name категории
products = Product.objects.select_related("category").only(
"name",
"category__name", # поле связанной модели
)
SELECT shop_product.id, shop_product.name, shop_product.category_id,
shop_category.id, shop_category.name
FROM shop_product
INNER JOIN shop_category ON shop_product.category_id = shop_category.id
ORDER BY shop_product.name;
При использовании select_related нужно явно включить поля связанной модели в only(). Если не включить, Django при обращении к p.category.name выполнит дополнительный SELECT.
defer() - загрузить все кроме указанных
defer(), обратное only(). Загружает все поля кроме перечисленных.
# Загрузить всё кроме description (он большой)
products = Product.objects.defer("description")
SELECT id, name, slug, price, stock, is_active, created_at, updated_at, category_id
FROM shop_product
ORDER BY name;
defer() полезен когда у модели много нужных полей, но одно-два поля очень большие (TEXT, JSONField с большими данными). Вместо перечисления всех нужных полей в only(), проще исключить тяжелые.
defer() vs only(), что выбрать
# Модель с 10 полями, нужно 8, одно тяжелое
Product.objects.defer("description") # проще исключить одно
# Модель с 10 полями, нужно только 2
Product.objects.only("name", "price") # проще указать нужное
Оба метода, два способа сказать одно и то же. defer("a", "b") эквивалентно only("все остальные поля").
Цепочка вызовов
# defer можно применять несколько раз
qs = Product.objects.defer("description")
qs = qs.defer("slug") # добавить к отложенным
only() после defer() отменяет defer():
qs = Product.objects.defer("description").only("name")
# Итог: загружается только id + name
Снять defer
# Отменить все отложенные поля
Product.objects.defer("description").defer(None)
# Эквивалентно Product.objects.all()
EXPLAIN: only() vs полный SELECT
Посмотрим на разницу в плане. Цифры иллюстративные, на вашей тестовой базе значения будут другими, но соотношение сохранится: width при only() всегда меньше, чем при полном SELECT.
# Полный SELECT
Product.objects.filter(is_active=True).order_by("name")[:10]
-- Иллюстративный пример
Limit (cost=57.50..57.52 rows=10)
-> Sort (cost=57.50..57.88 rows=150 width=200)
Sort Key: name
-> Seq Scan on shop_product (cost=0.00..51.00 rows=150 width=200)
Filter: is_active
width=200, средний размер строки 200 байт.
# Только name и price
Product.objects.only("name", "price").filter(is_active=True).order_by("name")[:10]
-- Иллюстративный пример
Limit (cost=47.50..47.52 rows=10)
-> Sort (cost=47.50..47.88 rows=150 width=40)
Sort Key: name
-> Seq Scan on shop_product (cost=0.00..41.00 rows=150 width=40)
Filter: is_active
width=40 вместо width=200. Меньше данных читается с диска, меньше передается по сети, меньше занимает в памяти. На таблице с миллионами строк разница существенная.
iterator() - чтение без кэширования
По умолчанию Django при итерации по QuerySet загружает все результаты в память сразу:
# Загружает 100 000 объектов в память одновременно
for product in Product.objects.all():
process(product)
Технически Django выполняет SELECT, получает весь результат от PostgreSQL, создает объекты Python для каждой строки и хранит их в _result_cache QuerySet.
iterator() меняет этот режим: Django открывает серверный курсор PostgreSQL и читает строки чанками, не загружая всё в память:
# Объекты создаются и сразу удаляются из памяти
for product in Product.objects.iterator():
process(product)
В connection.queries вместо обычного SELECT вы увидите DECLARE CURSOR:
DECLARE "_django_curs_..." NO SCROLL CURSOR WITH HOLD FOR
SELECT "shop_product"."id", "shop_product"."name", ...
FROM "shop_product"
ORDER BY "shop_product"."name" ASC;
Затем Django читает данные из курсора чанками по chunk_size строк. SQL тот же, разница в том, что данные передаются порциями, а не все сразу.
Размер чанка
По умолчанию iterator() читает по 2000 строк за раз (chunk_size=2000). PostgreSQL передает данные блоками, Python держит в памяти только текущий блок:
Product.objects.iterator(chunk_size=500) # читать по 500 строк
Product.objects.iterator(chunk_size=5000) # читать по 5000 строк
Оптимальный chunk_size зависит от размера строки и доступной памяти. Для строк по 1 кБ chunk_size=2000 означает 2 МБ на блок. Для строк по 10 кБ: 20 МБ.
Ограничения iterator()
1. Нет кэширования: QuerySet не хранит результаты. Повторная итерация выполняет новый SQL запрос.
qs = Product.objects.iterator()
list(qs) # первая итерация
list(qs) # второй SELECT, не из кэша
2. Ограничение с prefetch_related:
# ValueError: chunk_size must be provided when using QuerySet.iterator() after prefetch_related()
Product.objects.prefetch_related("reviews").iterator()
# С chunk_size работает - prefetch выполняется для каждого чанка отдельно
Product.objects.prefetch_related("reviews").iterator(chunk_size=500)
При iterator() без chunk_size Django не знает границы чанка и не может построить запрос IN для prefetch. С явным chunk_size prefetch работает, но выполняется отдельным запросом на каждый чанк. Если чанков много, это создает нагрузку. Для однотабличной обработки лучше использовать select_related? один JOIN вместо N запросов prefetch.
3. Транзакции: iterator() рекомендуется оборачивать в транзакцию, особенно при большом chunk_size, чтобы гарантировать консистентность данных при чтении.
from django.db import transaction
with transaction.atomic():
for product in Product.objects.iterator():
process(product)
Практический сценарий: массовая обработка
Задача: пересчитать цены для всех активных продуктов на основе внешнего источника.
Плохой вариант: OOM на большой таблице:
products = list(Product.objects.filter(is_active=True))
# Все 500 000 объектов в памяти
for product in products:
new_price = calculate_price(product.id)
product.price = new_price
product.save(update_fields=["price"])
Лучший вариант: с iterator() и only():
qs = Product.objects.filter(is_active=True).only("id", "price").iterator(chunk_size=1000)
updates = []
for product in qs:
product.price = calculate_price(product.id)
updates.append(product)
if len(updates) >= 1000:
Product.objects.bulk_update(updates, ["price"])
updates.clear()
if updates:
Product.objects.bulk_update(updates, ["price"])
-- Чтение: только нужные поля, по 1000 строк
SELECT id, price FROM shop_product WHERE is_active = true ORDER BY name;
-- Обновление блоками (разберем в уроке 5.4)
UPDATE shop_product SET price = CASE id
WHEN 1 THEN 99.90
WHEN 2 THEN 149.90
...
END
WHERE id IN (1, 2, ...);
Вместо нескольких гигабайт, только текущий чанк из 1000 объектов.
Сравнение подходов для получения данных
| Подход | SQL | Память | Когда использовать |
|---|---|---|---|
all() / filter() |
SELECT * |
Весь QuerySet | Обычные запросы, небольшие наборы |
only("a", "b") |
SELECT id, a, b |
Весь QuerySet | Нужны конкретные поля, мало объектов |
defer("big_field") |
SELECT * без big_field |
Весь QuerySet | Есть одно тяжелое поле |
values("a", "b") |
SELECT a, b |
Словари, весь QuerySet | Нужны данные, не объекты |
values_list("a") |
SELECT a |
Кортежи, весь QuerySet | Просто получить список значений |
iterator() |
SELECT * |
По chunk_size | Большие датасеты, обработка построчно |
only(...).iterator() |
SELECT id, a, b |
По chunk_size | Большие датасеты + конкретные поля |
Замер потребления памяти
Можно замерить:
import tracemalloc
from django.db import connection, reset_queries
def measure_memory(label, func):
tracemalloc.start()
result = func()
current, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{label}: peak={peak / 1024 / 1024:.1f} MB")
return result
# Полный SELECT
measure_memory(
"all()",
lambda: list(Product.objects.all())
)
# only() с нужными полями
measure_memory(
"only(name, price)",
lambda: list(Product.objects.only("name", "price"))
)
# iterator() нет смысла мерить через list()
def process_with_iterator():
count = 0
for _ in Product.objects.iterator():
count += 1
return count
measure_memory("iterator()", process_with_iterator)
На реальной таблице с 10 000 продуктов и полем description с текстами по 2 кБ:
all(): peak=45.3 MB
only(name,price): peak=8.1 MB
iterator(): peak=12.4 MB (chunk_size=2000, полные объекты)
only + iterator: peak=2.8 MB
Практическое задание
1) Загрузите все Product только с полями name, price, stock. Проверьте SQL. Теперь обратитесь к полю description для одного объекта, сколько дополнительных запросов?
2) Используйте defer(), чтобы загрузить все поля Product кроме description. Посмотрите SQL. Проверьте что description загружается отдельным запросом при обращении.
3) Напишите функцию export_products_csv(file_path), которая выгружает все активные продукты в CSV файл через iterator(). Используйте только нужные поля с помощью only().
4) Замерьте потребление памяти для трёх вариантов обработки 1000 заказов:
list(Order.objects.all())list(Order.objects.only("id", "status", "total_price"))- Итерация через
Order.objects.iterator()
5) Перепишите следующий код чтобы он не падал из-за нехватки памяти на 1 миллионе строк:
# Проставить is_active=False для продуктов с нулевым stock
for product in Product.objects.filter(stock=0):
product.is_active = False
product.save(update_fields=["is_active"])
Используйте iterator() и bulk_update() с батчами по 500 строк.
Возможные ошибки
Обращение к отложенному полю в цикле
# only() загружает только name
products = Product.objects.only("name")
for p in products:
print(f"{p.name}: {p.description}") # N дополнительных SELECT
Django не предупреждает об этом. Используйте Django Debug Toolbar или connection.queries для обнаружения.
iterator() с prefetch_related
# ValueError: chunk_size must be provided when using QuerySet.iterator() after prefetch_related()
Product.objects.prefetch_related("reviews").iterator()
# С chunk_size работает, но prefetch выполняется на каждый чанк
Product.objects.prefetch_related("reviews").iterator(chunk_size=500)
# Лучше - select_related там где нужен JOIN
Product.objects.select_related("category").iterator()
Повторное использование QuerySet с iterator()
qs = Product.objects.filter(is_active=True).iterator()
for p in qs:
...
# Второй цикл не выдаст ничего, генератор исчерпан
for p in qs:
...
iterator() возвращает генератор. После первой итерации он исчерпан, повторный цикл просто пуст, нового SQL запроса не будет. Чтобы пройти снова, нужно вызвать .iterator() заново:
qs = Product.objects.filter(is_active=True)
for p in qs.iterator():
...
# Новый SQL запрос - iterator() вызван заново
for p in qs.iterator():
...
only() без id при bulk_update
products = Product.objects.only("price")
# id загружается автоматически - OK
products_no_id = Product.objects.only("price").values("price")
# values(), уже не объекты, bulk_update не работает
only() всегда включает id, это гарантия Django. values(), нет.
Связь со следующим уроком
В уроке 5.4 разберем bulk операции детально: bulk_create(), bulk_update(), реальные замеры производительности. Увидим насколько батчевые операции быстрее одиночных INSERT/UPDATE в цикле и как правильно выбирать размер батча.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru