QuerySet.extra() lets you splice raw SQL fragments — extra SELECT columns, WHERE clauses, bound params, extra tables (joins), and raw ORDER BY — straight into a Django ORM query without leaving the queryset API. It still exists and works in Django 5.x, but it is officially discouraged: the Django documentation describes it as hard to use correctly, database-specific, and an "old API that we aim to deprecate," recommending it only as a last resort. In 2026, almost every legitimate .extra() call has a safer, composable replacement built from annotate(), filter() + Q, expressions such as Func/ExpressionWrapper, Subquery/OuterRef, .alias() (Django 3.2+), or — when you genuinely need raw SQL — RawSQL and Manager.raw().
This guide shows the honest .extra() syntax for each use case, then the modern ORM equivalent side by side, so you can read legacy code and migrate it confidently.
Key takeaways
QuerySet.extra()injects raw SQL (select,where,params,tables,order_by) into an ORM query; it is not removed in Django 5.x but is soft-deprecated and discouraged.- Prefer the modern ORM:
annotate()for computed columns,filter()+Qfor conditions,Subquery/OuterReffor correlated lookups, and.alias()to compute without selecting. - For correlated subqueries that
.extra(select=...)used to handle, useSubquery(... .values('field')[:1])withOuterRef. - When you truly need a raw fragment, use
RawSQLinside the ORM, orManager.raw()for a whole hand-written query — both let you pass boundparamsto avoid SQL injection. .extra()is database-specific and a common SQL-injection footgun; the ORM expressions are portable and parameterised.- The cleanest fix is usually to model relationships as
ForeignKeyand useselect_related()instead of stitching IDs together with raw SQL.
What is QuerySet.extra()?
QuerySet.extra() is an "escape hatch" method on Django querysets that lets you add raw SQL to specific parts of the generated query while keeping the rest of the ORM intact. Its full signature is:
QuerySet.extra(select=None, where=None, params=None, tables=None, order_by=None, select_params=None)
select— a dict of{alias: "raw SQL expression"}added to theSELECTclause; the alias becomes an attribute on each returned object.where— a list of raw SQL stringsAND-ed into theWHEREclause.params— values bound to%splaceholders inwhere(use these, never string-format values yourself).select_params— values bound to placeholders insideselect.tables— extra tables to join into theFROMclause.order_by— raw column names or aliases to order by.
It was historically used to do things the ORM could not express. Today the ORM expression API covers almost all of those cases, which is why .extra() is discouraged.
A concrete example: denormalized IDs
Take a classic case that often reaches for .extra() — a Student whose state and city are stored as plain integer IDs instead of foreign keys:
# myapp/models.py
from django.db import models
class State(models.Model):
name = models.CharField(max_length=150)
class City(models.Model):
name = models.CharField(max_length=150)
class Student(models.Model):
name = models.CharField(max_length=150)
# Denormalized IDs (an anti-pattern — prefer ForeignKey; see below)
state_id = models.PositiveIntegerField()
city_id = models.PositiveIntegerField()
is_active = models.BooleanField(default=False)How do you filter with extra(select / where / params)?
Because state_id and city_id are raw integers (not ForeignKey), fetching the related names normally means an extra query per student (the classic N+1 problem). The legacy trick was to push correlated subqueries into the SELECT with extra(select=...) so each name comes back in a single query:
# Legacy approach with .extra(select=...)
students = Student.objects.filter(is_active=True).extra(
select={
# NOTE: use Django's real table names: <app_label>_<model>.
# The default for these models is myapp_state / myapp_city / myapp_student.
"state": "SELECT name FROM myapp_state WHERE myapp_state.id = myapp_student.state_id",
"city": "SELECT name FROM myapp_city WHERE myapp_city.id = myapp_student.city_id",
},
)
for s in students:
print(s.name, s.state, s.city) # 'state' / 'city' are now attributesThe select aliases (state, city) become attributes on each Student object, populated from one round trip.
extra(where=..., params=...) works the same way for filtering — for example, "students whose name is longer than 10 characters," which older Django could not express without raw SQL:
# Legacy raw filter with bound params (never string-format values in!)
students = Student.objects.extra(
where=["CHAR_LENGTH(name) > %s"],
params=[10],
).extra(
order_by=["name"],
)Why is extra() discouraged?
.extra() still works, but it carries real costs that the modern ORM avoids:
- Database-specific. Raw fragments like
CHAR_LENGTHor quoting rules differ across PostgreSQL, MySQL, and SQLite, so.extra()queries are not portable. - SQL-injection risk. If you interpolate values into the SQL string instead of using
params/select_params, you open an injection hole. Expressions parameterise automatically. - Fragile to schema changes. You hard-code table and column names, so a rename or an app-label change silently breaks the query.
- Doesn't compose.
.extra()is hard to combine, reuse, or layer with the rest of the ORM, and the Django team has long signalled intent to deprecate it.
The Django docs are explicit: use .extra() only when the ORM genuinely cannot express your query — and that situation is rare in 2026.
What should you use instead?
Map each .extra() use case to its modern, portable, parameterised replacement:
.extra() use case |
Modern replacement (Django 5.x, 2026) |
|---|---|
select={...} (extra computed column) |
annotate() with Func / ExpressionWrapper, or Subquery + OuterRef for correlated lookups |
where=[...] + params=[...] (raw filter) |
filter() with field lookups and Q objects (or RawSQL for a true raw fragment) |
tables=[...] (extra joins) |
proper ForeignKey relations + select_related() / FilteredRelation |
order_by=[...] (raw ordering) |
annotate(...).order_by(...) on the annotated alias |
| an alias you only filter or order on | .alias() (Django 3.2+) — computes it without adding it to SELECT |
| a whole hand-written query | Manager.raw() — full raw SQL mapped back to model instances |
The single biggest win is to model relations correctly: if state and city were ForeignKey fields, Student.objects.select_related("state", "city") would solve the original N+1 problem with zero raw SQL.
Replacing extra(select=...) with Subquery + OuterRef
Keeping the denormalized integer columns, the correlated subquery from extra(select=...) becomes a portable, parameterised Subquery:
from django.db.models import OuterRef, Subquery
state_name = State.objects.filter(pk=OuterRef("state_id")).values("name")[:1]
city_name = City.objects.filter(pk=OuterRef("city_id")).values("name")[:1]
students = (
Student.objects
.filter(is_active=True)
.annotate(
state=Subquery(state_name),
city=Subquery(city_name),
)
.order_by("name")
)
for s in students:
print(s.name, s.state, s.city) # one query, no raw SQLThe extra(where=["CHAR_LENGTH(name) > %s"]) filter is now a database function plus a normal lookup. Use annotate() if you want the value in the result, or .alias() if you only filter on it:
from django.db.models import Q
from django.db.models.functions import Length
from django.db.models.expressions import RawSQL
# Portable: Length() emits the right function per backend
Student.objects.annotate(name_len=Length("name")).filter(name_len__gt=10)
# .alias() computes without selecting it (Django 3.2+)
Student.objects.alias(name_len=Length("name")).filter(name_len__gt=10).order_by("name")
# Combine conditions with Q objects instead of raw WHERE strings
Student.objects.filter(Q(is_active=True) & ~Q(name=""))
# Last resort: a raw fragment INSIDE the ORM, still parameterised
Student.objects.annotate(
upper_name=RawSQL("UPPER(name)", ()),
).filter(upper_name__startswith="A")When you need a whole hand-written query (complex CTEs, vendor-specific SQL), drop to Manager.raw() — it maps the result rows back onto model instances and accepts bound params. For deeper patterns, see our guides on running raw SQL queries in Django, querying with Django Q objects, conditional expressions in Django queries, and aggregation in Django. To keep these queries fast at scale, pair them with the techniques in optimizing Django database access.
Migrating a large Django codebase off .extra() and other legacy patterns? Our team has shipped Django for clients across the US, UK, Australia, and Singapore since 2014 — explore our Django development services to modernise your ORM layer safely.
Frequently Asked Questions
Is QuerySet.extra() deprecated in Django 5.x?
Not formally removed, but officially discouraged. .extra() still exists and runs in Django 5.x, yet the documentation labels it an old API the team aims to deprecate and tells you to use it only as a last resort. For new code you should reach for annotate(), filter(), Subquery/OuterRef, .alias(), or RawSQL instead.
What is the difference between extra() and raw()?
QuerySet.extra() modifies parts of an ORM-generated query (it adds raw SELECT, WHERE, tables, or ORDER BY fragments while the ORM builds the rest). Manager.raw() runs an entire SQL statement you write yourself and maps each row back to a model instance. Use raw() when you need a full custom query; prefer pure ORM expressions over both when possible.
How do I replace extra(select=...) with annotate()?
Move the SQL expression into an ORM expression and add it with annotate(). For a simple computed column use a database Func (e.g. Length, Upper, Coalesce); for a correlated subquery use annotate(alias=Subquery(Related.objects.filter(pk=OuterRef("fk_id")).values("name")[:1])). The annotated alias becomes an attribute on each returned object, exactly like the old select alias did.
When is it still OK to use extra()?
Only when the ORM genuinely cannot express the query and RawSQL/Manager.raw() are not a better fit — for example, a vendor-specific clause that has to be stitched into an otherwise ORM-managed query. Even then, always pass values through params/select_params rather than formatting them into the SQL string, and add a comment explaining why the escape hatch was necessary.
What is the difference between annotate() and alias()?
Both attach a computed expression to a queryset, but annotate() adds the value to the SELECT clause so it is returned on each object, while .alias() (added in Django 3.2) computes the expression only for use in later filter() or order_by() calls without selecting it. Use .alias() when you need a value purely to filter or sort on, to avoid fetching data you will not read.
Is extra() vulnerable to SQL injection?
It can be, if you build the SQL string by interpolating user input directly. .extra() is safe only when every value goes through the params and select_params arguments, which Django binds as query parameters. ORM expressions and lookups parameterise automatically, which is one of the main reasons to prefer them over .extra().