N+1 xảy ra khi bạn chạy 1 query lấy N object, rồi với mỗi object lại chạy thêm 1 query để lấy related → tổng 1 + N query.
posts = Post.objects.all() # 1 query
for p in posts:
print(p.author.full_name) # +1 query mỗi vòng — N query!Fix bằng select_related (FK/OneToOne) hoặc prefetch_related (M2M, reverse FK) — xem [[#9207]]:
posts = Post.objects.select_related('author') # 1 query JOINBắt sớm trong dev có 3 cách: django-debug-toolbar (panel "SQL" hiện tất cả query của 1 request, có cảnh báo duplicate), django.db.connection.queries trong shell sau khi DEBUG=True, và viết test self.assertNumQueries(2): quanh code path quan trọng — fail ngay khi N+1 sinh thêm query.
N+1 trong template rất dễ lọt qua review — {% for p in posts %}{{ p.author.name }} cũng tính. Khi serializer DRF nested mà queryset gốc không prefetch, mỗi item trong list lại đẻ ra 2-3 query. Luôn test với dataset ≥ 50 row, không phải 2-3 row — bug N+1 chỉ "nhìn thấy" được khi data đủ lớn.
N+1 happens when you run one query to get N objects, then for each object run another query to fetch a related row → 1 + N total queries.
posts = Post.objects.all() # 1 query
for p in posts:
print(p.author.full_name) # +1 query per iteration — N queries!Fix with select_related (FK/OneToOne) or prefetch_related (M2M, reverse FK) — see [[#9207]]:
posts = Post.objects.select_related('author') # 1 JOIN queryCatch it early in dev:
- django-debug-toolbar — its "SQL" panel lists every query per request, with duplicate warnings.
- django.db.connection.queries in shell after DEBUG=True.
- Tests with self.assertNumQueries(2): around important code paths — fails when N+1 leaks extra queries.
Pitfall: N+1 leaking through templates is sneaky — {% for p in posts %}{{ p.author.name }} counts too. When a nested DRF serializer's base queryset is not prefetched, every item adds 2–3 queries. Always test with ≥ 50 rows, not 2.