skip to content

In Django, why does an order list template printing order.customer.name run one query per order, and how does select_related() fix it?

level: juniorimportance: must knowfreq 74%

answer

  1. related row not in the list query
  2. loaded on first attribute access
  3. one statement per touched instance
  4. join the single-valued link up front

basics

~20 s

By default Django loads a ForeignKey's target only when it is first read, so each order.customer access in the loop runs its own SELECT. select_related('customer') joins the customer table into the list query, so every customer arrives with its order.

solid answer

~40 s

`Order.objects.all()` selects only the order table's columns, including the key `customer_id` but not the customer's row. The first read of `order.customer` on each instance runs a `SELECT` on the customer table for that one order (Django 6.1's default `FETCH_ONE` fetch mode), so 50 orders cost 1 + 50 queries. Changing the view to `Order.objects.select_related('customer')` makes the ORM add a SQL `JOIN` and build each `Customer` from the joined columns, caching it on its order, so the template's `order.customer.name` reads memory and the page runs one query. `select_related()` follows forward `ForeignKey` and `OneToOneField` links (and a reverse one-to-one); name the relations explicitly, because calling it with no arguments is deprecated in 6.1.

code

python · 9 lines
python
from django.shortcuts import render

from .models import Order


def order_list(request):
    # One query: orders JOIN customers.
    orders = Order.objects.select_related('customer').order_by('-placed_at')[:50]
    return render(request, 'shop/order_list.html', {'orders': orders})

go deeper

for a junior

Recall that a ForeignKey target is loaded on first access, count the queries a loop causes, and show select_related('customer') in the view as the fix.

for a middle

Explain the join Django builds, INNER versus LEFT OUTER for nullable keys, which relation types are allowed, and why customer_id is free while customer is not.

for a senior

Show you keep joins to what the page renders, name relations explicitly given the 6.1 deprecation, and confirm the fix by counting queries rather than trusting it.

for a principal

Frame select_related as a per-read decision owned by the view that renders the data, and weigh wide joined rows against the round trips they save.

## The setup: an order list page Picture a small shop app. An `Order` belongs to one `Customer` through a `ForeignKey`, and the order list page shows each order's date and customer name. ```python from django.db import models class Customer(models.Model): name = models.CharField(max_length=200) class Order(models.Model): customer = models.ForeignKey( Customer, on_delete=models.PROTECT, related_name='orders' ) placed_at = models.DateTimeField() ``` The view passes the 50 newest orders to a template that loops with `{% for order in orders %}` and prints `{{ order.customer.name }}` on every row. ## Why the loop runs one query per order A Django **model instance** holds the columns its query selected. For `Order`, that includes the foreign key column `customer_id`, but not the customer's own row. The related `Customer` object is loaded **on demand** the first time code reads the `order.customer` attribute. In Django 6.1 this on-demand behaviour is the default **fetch mode**, `FETCH_ONE`, which fetches the missing value for the one instance that was touched. So rendering the page goes like this: 1. The template's `for` loop evaluates the QuerySet: one `SELECT` on the order table. 2. Row 1 reads `order.customer.name`; no customer is cached on that instance, so Django runs a `SELECT` on the customer table filtered by that order's `customer_id`. 3. Row 2 does the same for its own customer, and so on for all 50 rows. 4. The page finishes after **1 + 50 queries**, even if only three distinct customers placed those orders. A few details matter in interviews: - Reading `order.customer_id` never queries: the key is a local column already on the order row. - Once `order.customer` has been loaded, it is cached on that instance, so a second read on the same row is free. - The template engine is not the culprit; the same loop in a view, a management command or a serializer produces the same queries. ## What select_related() changes `select_related('customer')` tells the ORM to **JOIN** the customer table into the list query and to build a `Customer` instance from the joined columns for every order row. The view becomes: ```python from django.shortcuts import render from .models import Order def order_list(request): orders = Order.objects.select_related('customer').order_by('-placed_at')[:50] return render(request, 'shop/order_list.html', {'orders': orders}) ``` Now the page runs **one query**. Each order arrives with its customer already cached, so `{{ order.customer.name }}` reads memory instead of the database. Django uses an `INNER JOIN` when the foreign key is non-nullable and a `LEFT OUTER JOIN` when it is `null=True`, so rows whose key is `NULL` are not dropped from the list. The method can sit anywhere in the chain: `Order.objects.filter(...).select_related('customer')` and `Order.objects.select_related('customer').filter(...)` are equivalent, because the QuerySet compiles one statement when it is evaluated. You can follow several hops with the double-underscore syntax, for example `select_related('customer__account_manager')`, and several relations at once with `select_related('customer', 'shipping_address')`. ## Which relations it can follow | Relation from `Order`'s point of view | Works with `select_related()`? | |---|---| | Forward `ForeignKey` (`order.customer`) | Yes | | Forward `OneToOneField` | Yes | | Reverse `OneToOneField`, named by its `related_name` | Yes | | Reverse `ForeignKey` (`order.items`) | No: raises `FieldError` | | `ManyToManyField` | No: raises `FieldError` | The rule is that `select_related()` handles **single-valued** links, where the join adds at most one related row per order. Many-valued links need `prefetch_related()`, which runs a separate query instead of a join. ## Fixes that do not fix it Interviewers often probe with tempting alternatives: - **Adding a database index** on `customer_id` makes each lookup faster but leaves all 51 round trips in place. - **Building a customer dictionary by hand** (collect the ids, query the customers once, look them up in the loop) does cut the count, but it re-implements what the ORM already offers and drifts out of date as the page grows. - **Caching the rendered page** hides the queries on a cache hit and brings every one of them back on a miss. The ORM-level fix is to load the related rows together with the orders, which is exactly what `select_related()` does. ## Habits worth keeping - **Name the relations.** Calling `select_related()` with no arguments follows every non-null foreign key it can reach; Django 6.1 deprecates that form and plans to remove it in 7.0. - **Join only what the page renders.** Every joined table widens every row, so a join nobody reads is pure cost. - **Clear it when reusing a QuerySet.** `select_related(None)` removes previously requested relations. - **Verify the count.** After the change, count the queries the page emits and confirm it dropped from 51 to 1. The interview-ready summary: the extra queries come from on-demand foreign-key access on each row, and `select_related()` removes them by moving the related row into the original query with a join.

  • Does reading order.customer_id in the loop also trigger the extra query?
    No. `customer_id` is the foreign key column stored on the order row itself, so the list query already loaded it. Only following the link to the `Customer` object needs the customer's row. Printing the id instead of the name is sometimes a legitimate way to avoid the lookup entirely.
  • What happens if you pass select_related() a many-valued relation such as items?
    Django raises `FieldError` with "Invalid field name(s) given in select_related" and lists the valid choices. Reverse foreign keys and many-to-many fields are not joined this way, because each order would be repeated once per item; `prefetch_related()` loads those with a separate batched query.
  • Does it matter whether select_related() is chained before or after filter()?
    No. Chaining order does not change the SQL: the QuerySet records both calls and compiles a single statement when it is evaluated, so `filter(...).select_related('customer')` and `select_related('customer').filter(...)` produce the same join and the same query count.

saying these in an interview costs you the question

  • select_related() runs a second query to load all the customers
  • Django loads every ForeignKey target automatically together with the parent row
  • Calling select_related() with no arguments is the recommended way to catch every relation
  • select_related('items') will load each order's line items in the same query
  • The extra queries come from the template engine, so moving the loop into the view fixes them