Django

Lesson 03

QuerySets and lazy evaluation

Compose QuerySets before evaluation and load relationships intentionally to avoid hidden N+1 queries.

Good Code

reviews/selectors.py
# Build the QuerySet before evaluating it.
def recent_reviews():
    return (
        Review.objects.filter(status=Review.Status.PUBLISHED)
        .select_related("author")
        .prefetch_related("comments")
        .order_by("-published_at")[:20]
    )


def review_cards():
    # Related rows are already loaded before this loop runs.
    return [
        {
            "title": review.title,
            "author": review.author.username,
            "comment_count": len(review.comments.all()),
        }
        for review in recent_reviews()
    ]

Bad Code

reviews/selectors.py
def review_cards():
    rows = []
    reviews = Review.objects.filter(status="published").order_by("-published_at")[:20]

    for review in reviews:
        rows.append(
            {
                "title": review.title,
                # Relationship access in the loop can create N+1 queries.
                "author": review.author.username,
                "comment_count": review.comments.count(),
            }
        )

    return rows

Review Notes

What to review

Good Code

The good version composes the query, selects the related author, prefetches comments, and evaluates at the edge where cards are built.

Bad Code

The bad version looks small but can trigger extra queries for every review. The performance cost is hidden inside attribute access.

Takeaways

  • QuerySets are lazy; review where they are built, sliced, and finally evaluated.