Django cookbook · Intermediate
Import and update many rows efficiently
Saving thousands of rows one by one takes minutes and floods the database with queries.
Approach
Build model instances in memory and write them in batches with bulk_create (optionally as an upsert with
update_conflicts) and bulk_update. Wrap the import in a transaction so it's all-or-nothing.
Solution
import csv
from django.db import transaction
from shop.models import Product
@transaction.atomic
def import_prices(path: str) -> int:
with open(path, newline="") as f:
rows = [
Product(sku=row["sku"], name=row["name"], price=row["price"])
for row in csv.DictReader(f)
]
Product.objects.bulk_create(
rows,
batch_size=1000,
update_conflicts=True, # INSERT ... ON CONFLICT (sku) DO UPDATE
unique_fields=["sku"],
update_fields=["name", "price"],
)
return len(rows)
def apply_discount(product_ids: list[int], percent: int) -> None:
products = list(Product.objects.filter(id__in=product_ids))
for product in products:
product.price = product.price * (100 - percent) / 100
Product.objects.bulk_update(products, ["price"], batch_size=500)
Pitfalls
- Expecting
save()overrides andpre_save/post_savesignals to run; bulk operations skip them. - Loading a huge file into memory at once; process it in chunks.
- Forgetting
batch_size, producing one enormous SQL statement.
Variations
- For a single computed change,
queryset.update(price=F("price") * 0.9)is one query. - Use
COPY(psycopg'scopy) for very large PostgreSQL imports.
Further reading: bulk_create
Reference: https://docs.djangoproject.com/en/6.0/ref/models/querysets/#bulk-create
More Django recipes
- Add a required column without downtime
- Add full-text search with PostgreSQL
- Add health checks and structured logging
- Avoid N+1 queries with select_related and prefetch_related
- Cache expensive queries and invalidate on change
- Call external APIs concurrently in async views
- Configure settings from environment variables
- Enforce rules with database constraints