Prevent N+1 Queries in Django

Flags Django ORM relations read per row in loops, DRF serializers, and templates without select_related, prefetch_related, or annotate.

What it flags

Flaggedorders/api.py
from rest_framework import serializers, viewsets

from .models import Order


class OrderSerializer(serializers.ModelSerializer):
    customer_name = serializers.CharField(source="customer.name", read_only=True)
    item_count = serializers.SerializerMethodField()

    class Meta:
        model = Order
        fields = ["id", "status", "customer_name", "item_count"]

    def get_item_count(self, order):
        return order.items.count()


class OrderViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = OrderSerializer

    def get_queryset(self):
        # Each order adds two queries: its customer and its item count
        return Order.objects.filter(account=self.request.user.account)
Passesorders/api.py
from django.db.models import Count
from rest_framework import serializers, viewsets

from .models import Order


class OrderSerializer(serializers.ModelSerializer):
    customer_name = serializers.CharField(source="customer.name", read_only=True)
    item_count = serializers.IntegerField(read_only=True)

    class Meta:
        model = Order
        fields = ["id", "status", "customer_name", "item_count"]


class OrderViewSet(viewsets.ReadOnlyModelViewSet):
    serializer_class = OrderSerializer

    def get_queryset(self):
        return (
            Order.objects.filter(account=self.request.user.account)
            .select_related("customer")
            .annotate(item_count=Count("items"))
        )

Why it matters

A list view that runs one query per row is fast against a development database with ten rows and slow in production with five hundred, and the extra queries grow with page size and traffic. The attribute access that triggers each query, such as order.customer.name or {{ order.items.count }}, looks like plain Python or template syntax, so reviewers rarely see the database call. Serializers and templates usually live in different files from the view that builds the queryset, which keeps the missing select_related out of the diff.

Agent instructions

cubic checks every pull request against these instructions. Paste them into AGENTS.md or Cursor rules and your AI coding tool follows them while it writes code.

AGENTS.md

Add this section to AGENTS.md at the repository root. Codex, Cursor, and other AI coding tools read it.

## Prevent N+1 Queries in Django

Applies to `**/*.py`, `**/templates/**/*.html`.

Review Django ORM access in views, Django REST Framework serializers, and templates for queries that run once per row.

Flag changes that:
- Read a `ForeignKey` or `OneToOneField` relation (`order.customer.name`) on each object in a loop, serializer, or template when the queryset has no matching `select_related()`.
- Iterate a reverse `ForeignKey` or a `ManyToManyField` (`order.items.all()`, `book.authors.all()`) per object without `prefetch_related()` on the parent queryset.
- Call `.count()`, `.exists()`, or `.aggregate()` on a related manager per row for a relation that is not prefetched, instead of annotating the parent queryset with `Count`, `Exists`, `Sum`, or `Subquery`.
- Add nested serializers, `source='relation.field'` fields, or `SerializerMethodField` methods that read relations the view's `get_queryset()` does not load.
- Call `.filter()`, `.exclude()`, or `.order_by()` on a prefetched relation inside a loop, which ignores the prefetch cache and queries again; use `Prefetch(queryset=..., to_attr=...)` instead.
- Call `.get()` or `.filter()` inside a loop over IDs instead of one `filter(pk__in=...)` or `in_bulk()` query.

Do not flag:
- Reading the foreign key column (`order.customer_id`), which does not query.
- Code that handles one object or a small fixed number of objects, such as reading `order.customer` once in a detail view.
- Relations already loaded by a custom manager, a shared `get_queryset()`, or the queryset the caller passes in.

Checks files matching **/*.py, **/templates/**/*.html.

More Django agents

See all Django code review standards

Run this agent on every pull request

cubic reviews every pull request against the agents you turn on and comments on the lines that break them.

Catch this in your code