Django ORM

Bulk в Django ORM | Курс Django ORM урок 5.4

Bulk в Django ORM | Курс Django ORM урок 5.4
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 2

Цель урока

Замерить реальную производительность save() в цикле против bulk_create() и bulk_update(). Разобрать паттерн "накопление и сброс" для обработки больших объемов данных, и понять как транзакции влияют на скорость батчевых операций.

Необходимые знания

  • Урок 2.5: update(), bulk_create(), bulk_update(), delete()
  • Урок 5.3: iterator(), only()
  • Базовое понимание INSERT и UPDATE в SQL

В уроке 2.5 разобрали как работают bulk_create() и bulk_update(), какой SQL они генерируют и какие у них ограничения. Здесь сосредоточимся на производительности: реальные цифры, оптимальный batch_size и паттерны для обработки больших датасетов.


Реальные замеры производительности

Замеры на PostgreSQL в Docker, вставка 10 000 объектов. Функция для замера:

import time
from django.db import connection, reset_queries
from decimal import Decimal

def benchmark(label, func):
    reset_queries()
    start = time.perf_counter()
    func()
    elapsed = time.perf_counter() - start
    queries = len(connection.queries)
    print(f"{label}: {elapsed:.3f}s, запросов: {queries}")


category = Category.objects.first()

# Вариант 1: save() в цикле
def create_one_by_one():
    for i in range(10_000):
        Product.objects.create(
            name=f"P{i}", slug=f"p-{i}", price=Decimal("100"), stock=10, category=category
        )

benchmark("save() в цикле", create_one_by_one)
# save() в цикле: 8.423s, запросов: 10000
# Вариант 2: bulk_create без batch_size
def create_bulk():
    products = [
        Product(name=f"P{i}", slug=f"p-{i}", price=Decimal("100"), stock=10, category=category)
        for i in range(10_000)
    ]
    Product.objects.bulk_create(products)

benchmark("bulk_create()", create_bulk)
# bulk_create(): 0.187s, запросов: 1


# Вариант 3: bulk_create с batch_size=500
def create_bulk_batched():
    products = [
        Product(name=f"P{i}", slug=f"p-{i}", price=Decimal("100"), stock=10, category=category)
        for i in range(10_000)
    ]
    Product.objects.bulk_create(products, batch_size=500)

benchmark("bulk_create(batch_size=500)", create_bulk_batched)
# bulk_create(batch_size=500): 0.312s, запросов: 20

Аналогичная картина для обновления 10 000 строк:

products = list(Product.objects.all()[:10_000])

# save() в цикле: 12.1s, запросов: 10000
# bulk_update(): 0.89s, запросов: 1
# bulk_update(batch_size=500): 1.1s, запросов: 20

Разница в 45-60 раз на localhost. На продакшне с сетевой задержкой 1-5 мс разрыв будет ещё больше.


Выбор batch_size

batch_size влияет на баланс между памятью, количеством запросов и скоростью:

batch_size=None  → 1 запрос,  максимальная память, риск превысить лимит ~65k параметров PostgreSQL
batch_size=1000  → 10 запросов, хороший баланс для большинства случаев
batch_size=100   → 100 запросов, меньше памяти на батч, больше round-trip

Правило: batch_size=500-1000 для строк весом до 1 кБ. Если строки большие (много текстовых полей), уменьшайте до 200-500.


QuerySet.update() vs bulk_update()

Оба метода обновляют несколько строк, но для разных задач:

QuerySet.update() - когда значение одинаково для всех строк:

# Один запрос, вычисление на уровне SQL
Product.objects.filter(category=category).update(
    price=F("price") * Decimal("1.1")
)


UPDATE shop_product SET price = price * 1.1 WHERE category_id = 1;

bulk_update() - когда у каждой строки своё значение:

# Разные цены для каждого продукта из внешнего источника
for product in products:
    product.price = external_prices[product.id]

Product.objects.bulk_update(products, ["price"])


UPDATE shop_product SET price = CASE id
    WHEN 1 THEN 99.50
    WHEN 2 THEN 149.00
    ...
END
WHERE id IN (1, 2, ...);
Подход Когда использовать Запросов
save() в цикле Логика в сигналах, один-два объекта N
QuerySet.update() Одинаковая логика для всех строк 1
bulk_update() Разные значения для разных строк N/batch_size
bulk_create() Вставка множества объектов N/batch_size

Паттерн: накопление и сброс

При обработке большого датасета комбинируем iterator() из урока 5.3 с bulk_update():

BATCH_SIZE = 500

def recalculate_prices():
    updates = []

    for product in Product.objects.only("id", "price", "category_id").iterator(chunk_size=1000):
        new_price = calculate_new_price(product)
        if new_price != product.price:
            product.price = new_price
            updates.append(product)

        if len(updates) >= BATCH_SIZE:
            Product.objects.bulk_update(updates, ["price"])
            updates.clear()

    if updates:
        Product.objects.bulk_update(updates, ["price"])

Этот паттерн:

  • Читает строки чанками по 1000 через серверный курсор (iterator) в памяти только текущий чанк
  • Накапливает только изменившиеся объекты
  • Сбрасывает батчами по 500 через один UPDATE с CASE WHEN

bulk_create в транзакции

При batch_size Django делает N запросов, каждый из которых по умолчанию в autocommit:

# N/batch_size отдельных commit
Product.objects.bulk_create(products, batch_size=500)

# Один commit для всех батчей
from django.db import transaction

with transaction.atomic():
    Product.objects.bulk_create(products, batch_size=500)

Явная транзакция дает два преимущества:

  1. Атомарность: либо все батчи успешно, либо откат. Без транзакции при ошибке на 15-м батче первые 14 уже зафиксированы
  2. Скорость: commit один раз вместо N/batch_size раз. На 10 000 строк с batch_size=500 - 1 commit вместо 20

Практическое задание

1) Напишите функцию import_products(data), которая принимает список словарей и создает продукты через bulk_create() с batch_size=200. Посмотрите SQL через connection.queries.

2) Замерьте время: создание 5000 объектов Review через save() в цикле vs bulk_create(). Выведите разницу в секундах и количество запросов.

3) Напишите функцию apply_category_discount(category_id, percent), которая уменьшает цену всех продуктов категории на заданный процент. Реализуйте через QuerySet.update() и через загрузку + bulk_update(). Замерьте оба варианта.

4) Используйте bulk_create() с update_conflicts=True для синхронизации продуктов: если продукт по slug уже существует, обновить price и stock, если нет, создать.

5) Реализуйте deactivate_out_of_stock() тремя способами и замерьте каждый:

  • save() в цикле
  • QuerySet.update()
  • iterator() + bulk_update()

Возможные ошибки

bulk_update с полями, которых нет в объектах

products = Product.objects.only("name")  # загружены только id + name
for p in products:
    p.price = Decimal("100")  # установили в Python

Product.objects.bulk_update(products, ["price", "stock"])
# stock не был загружен, bulk_update запишет дефолтное значение поля!

bulk_update берет значения из атрибутов объекта. Если поле не было загружено, атрибут содержит дефолтное значение модели, а не реальное значение из БД.
Убедитесь что все поля из update_fields загружены.

bulk_create без транзакции при большом batch_size

# При ошибке на 15-м батче, первые 14 уже в БД, данные в несогласованном состоянии
Product.objects.bulk_create(products, batch_size=500)

# Правильно
with transaction.atomic():
    Product.objects.bulk_create(products, batch_size=500)

Связь со следующим уроком

В уроке 6.1 переходим к транзакциям: transaction.atomic(), savepoints и on_commit(). Увидим как именно работает atomic() который использовали здесь, и как выполнить код после успешного commit. Например, отправить email только если данные действительно сохранились.


<< Урок 5.3

Урок 6.1 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы