DjangoORMQuery counts

Django Interview Questions

Django interviews concentrate on the ORM, because that is where Django's convenience becomes a performance and correctness liability. Querysets are lazy and evaluate at moments that are not always obvious; relationships load per row unless you say otherwise; and the same template that renders beautifully in development issues four hundred queries against production data.

The second theme is implicit control flow. Signals, middleware and model `save` overrides all move behaviour away from the call site, and the interview question is usually about something happening that the code you are reading does not show. Candidates who can reason about where Django is doing something on their behalf do well; candidates who know only the explicit API struggle.

Written and reviewed by Sahil Srivastav

What the bar actually is

You should know when a queryset actually hits the database — iteration, slicing with a step, `len`, `bool`, `list` — and that chaining filters does not. That knowledge is what lets you reason about query counts, which is the thing Django interviews measure most consistently.

On transactions you are expected to use `atomic` deliberately and understand that `ATOMIC_REQUESTS` wrapping every request is a trade with real costs: long transactions, held connections, and external calls accidentally inside the boundary. You should also know that `select_for_update` requires an atomic block and will fail loudly outside one.

How the rounds are structured

Machine coding with Django

Models, views and validation for a small feature. Expect questions about query counts and where validation lives — model, serializer or view.

ORM behaviour and query counts

Laziness, `select_related` versus `prefetch_related`, `only` and `defer`, annotations. The deciding round for Django roles.

Transactions and concurrency

`atomic` blocks, `select_for_update`, `F` expressions for atomic updates, and what `get_or_create` actually does under concurrency.

Implicit behaviour

Signals, middleware ordering, `save` overrides, and migrations that lock tables in production.

What this interview bar tests

When a queryset evaluates

Filters chain without a query; iteration, slicing with a step, `len` and `bool` execute. Reasoning about query counts depends entirely on knowing this boundary.

select_related versus prefetch_related

The first is a SQL join for forward single-valued relations; the second issues a second query and joins in Python for many-valued relations. Using the wrong one produces either no improvement or a cartesian blow-up.

Atomic updates with F expressions

`F("count") + 1` updates in the database and avoids the read-modify-write race that Python-side arithmetic creates. The Django-specific version of the most-asked concurrency question.

Implicit behaviour located

Signals and `save` overrides run work the call site does not show. Knowing to look there is frequently the whole answer to a debugging question.

A preparation plan that works

  1. 1Install `django-debug-toolbar` or enable query logging on a small project and count queries for one list view. Then fix the N+1 with the correct prefetch strategy and watch the count collapse. This is the single most valuable Django preparation exercise.
  2. 2Write the `get_or_create` race: two concurrent calls creating the same object, with and without a unique constraint. Understanding why the constraint is what actually enforces it generalises well beyond Django.
  3. 3Replace a Python-side increment with an `F` expression and reason about why the first is unsafe under concurrency.
  4. 4Read one of your own apps for implicit behaviour: every signal, every `save` override, every middleware. Being able to say where Django acts on your behalf is what the debugging questions test.

Questions you should expect

When does a queryset hit the database?

On iteration, on `len`, on `bool`, on `list`, on slicing with a step, on pickling, and on `repr` in a shell. Chaining `filter`, `exclude` and `order_by` does not — it builds the query. This is the boundary that makes query counts predictable, and it is asked in almost every Django loop.

select_related or prefetch_related here?

`select_related` for forward `ForeignKey` and `OneToOne`, because it can be expressed as a join in one query. `prefetch_related` for reverse relations and `ManyToMany`, because joining them would multiply rows — it runs a second query and stitches in Python instead. Using `select_related` on a many-valued relation does nothing; using `prefetch_related` on a simple foreign key costs an extra query unnecessarily.

Two requests call `get_or_create` with the same key simultaneously. What happens?

Both can miss the `get` and both attempt the create, so one raises an `IntegrityError` — assuming a unique constraint exists. If it does not, you get duplicate rows and no error at all, which is worse. The constraint is the enforcement; `get_or_create` is a convenience that races.

Why is `counter += 1` then `save()` unsafe?

Because it is a read-modify-write across two statements, so two concurrent requests both read the same value and one update is lost. `F("counter") + 1` performs the arithmetic in the database as a single atomic statement, which removes the race entirely rather than narrowing it.

What gets candidates rejected

  • Not knowing when a queryset evaluates, which makes query-count reasoning impossible
  • Using `select_related` for many-valued relations, or `prefetch_related` where a join would do
  • Python-side increments instead of `F` expressions
  • Relying on `get_or_create` without a unique constraint behind it
  • Enabling `ATOMIC_REQUESTS` globally without accounting for long transactions and external calls inside them
  • Business logic in signals, which hides control flow and makes testing awkward
  • Migrations applied without considering the lock they take on a large table

What to practise, in order

Database overload under a traffic spike

N+1 queries, a missing index and an unbounded fetch compounding. Tests assert statements executed and rows visited per request.

Inventory overselling under concurrency

The read-modify-write race that `F` expressions and conditional updates exist to prevent.

Large table pagination failure

Why offset pagination degrades with depth and drops rows under concurrent inserts.

Database connection pool exhaustion

What long transactions and leaked sessions do to a pool, with the database-side evidence.

Practise in a real repository

Gronex ships broken backend repositories with failing test suites that encode the production invariant. You read the evidence, find the defect, and make the tests pass — which is what the round actually measures, rather than whether you can recite a definition.

FAQ

How much Django REST Framework is expected?

For API roles, a fair amount — serializer validation, viewsets, and where permission checks belong. The questions that matter are about validation placement and query efficiency in list endpoints, not about the configuration surface.

Is raw SQL acceptable in Django interviews?

Yes, where justified. Being able to say "the ORM cannot express this window function efficiently so I would drop to raw SQL here" is a strong signal, provided you also know what you give up — parameter safety needs attention and the result bypasses the ORM entirely.

Are signals considered good practice?

Treat them carefully. Interviewers often probe this, and the credible answer is that signals are appropriate for decoupled side effects like cache invalidation and poor for business logic, because they hide control flow and make the flow untestable in isolation.

What about async Django?

Worth knowing it exists and that the ORM support is partial. Most production Django remains synchronous, so claiming async expertise invites questions about which parts are actually async-safe — a specific question that is easy to get wrong.

Related

Other preparation tracks