Challenge 3: Fix an N+1 Query — Possible Solution ==================================================================== # BEFORE — N+1 for BOTH relationships def book_report_view(request): books = Book.objects.all() # 1 query for book in books: print(book.title) print(book.author.name) # +1 query PER book (ForeignKey) print([t.name for t in book.tags.all()]) # +1 query PER book (ManyToMany) # Total for N books: 1 + N + N = 1 + 2N queries # AFTER — fixed with select_related + prefetch_related together def book_report_view(request): books = Book.objects.select_related("author").prefetch_related("tags") for book in books: print(book.title) print(book.author.name) # already loaded — no extra query print([t.name for t in book.tags.all()]) # already loaded — no extra query # Total, regardless of N: exactly 2 queries WHY THIS WORKS -------------- - .select_related("author") is correct for the author relationship because Book -> Author is a ForeignKey (each book has exactly one author) — Django issues a single SQL query with a JOIN, so book.author.name for every book comes from data already loaded in that one query, with zero additional queries as the loop runs. - .prefetch_related("tags") is correct for the tags relationship because Book <-> Tag is a ManyToManyField — a JOIN here would multiply rows (a book with 3 tags would appear 3 times), so Django instead runs ONE separate query fetching all tags for all books in the QuerySet, then matches them up to the right book in Python as the loop runs. - Both optimizations are combined on the SAME QuerySet (.select_related("author").prefetch_related("tags")) — they aren't mutually exclusive, and combining them is the standard way to fix N+1 problems that involve more than one relationship type on the same query. - The total query count for the fixed version is a CONSTANT 2 queries no matter how many books or tags exist — this is the actual signal that the N+1 problem has been solved: query count stops scaling with the number of rows returned.