Skip to content

Django Queries

Django ORM is one of the most important skills for any backend developer to learn. As your project grows bigger, how well you write your queries will decide how fast your app runs, how correct your data stays, and how easy your code is to maintain later.

Django ORM is:

  • a tool that connects your Python objects to database tables (this is called an Object Relational Mapper)
  • a tool that helps you build queries
  • a system that waits before running a query (this is called lazy execution)
  • a layer that understands transactions, so it can keep multi-step database changes safe

Django ORM is not:

  • something magical
  • a way to skip learning SQL completely

In simple words, ORM lets you write normal Python code, but behind the scenes, the database still runs SQL to actually get the work done. Good Django developers understand both how ORM works and how SQL thinks.

flowchart LR A[Python Object Access] --> B[QuerySet<br/>Lazy, Not Executed Yet] B --> C[SQL Compiler] C --> D[Database Engine] D --> E[Result Rows] E --> F[Model Instances or Dict/Tuple Data]

This simple picture helps you understand:

  • where a query gets created
  • when it actually runs
  • why performance problems show up
ComponentPurpose
ModelConnects to a database table
ManagerThe starting point for queries
QuerySetA query that waits before running
FieldDefines a column
LookupUsed to build WHERE conditions
ExpressionUsed to build SQL expressions
qs = Product.objects.all() # Query is not executed yet

The query only runs when you actually use the result. For example, this happens when you:

  • loop through qs
  • turn it into a list using list(qs)
  • print it in most cases
  • slice it in a way that forces it to run
  • check if it is true or false using if qs:
qs = Product.objects.filter(is_active=True)
print(qs) # often triggers evaluation
products = list(qs) # definitely evaluates

Resetting your database is useful while you are still developing your app, especially when you are changing your database structure a lot.

Terminal window
python manage.py flush

This command removes your data but keeps your tables and migration history safe.

Terminal window
rm db.sqlite3
rm -rf app/migrations
python manage.py makemigrations
python manage.py migrate

A Manager is the starting point for talking to your database.

Product.objects

objects is the default Manager.

A QuerySet is basically a database query, and you can chain many of them together.

qs = Product.objects.filter(price__gt=100)
ManagerQuerySet
The starting pointBuilds the query
Usually one default per modelCan be chained, and stays unchanged once created (immutable)
Example: objectsExample: filter(), exclude()
Product.objects.all()

This gives you a QuerySet with all the rows for that model.

Product.objects.get(id=1)

get() expects to find exactly one row. If it finds none, or finds more than one, it gives an error:

  • DoesNotExist
  • MultipleObjectsReturned
from django.core.exceptions import ObjectDoesNotExist
try:
product = Product.objects.get(id=1)
except Product.DoesNotExist:
product = None
Product.objects.order_by('id').first()
Product.objects.order_by('id').last()

These return one object, or None if nothing is found. This makes them safer to use when the data might not always exist.

Product.objects.filter(price__gt=100)
Product.objects.exclude(is_active=False)

Use this pattern:

field__lookup=value
LookupMeaning
exact=
iexactcase-insensitive exact
containsLIKE %x%
icontainscase-insensitive contains
inIN (...)
gt / gte> / >=
lt / lte< / <=
rangeBETWEEN
isnullIS NULL
startswithLIKE x%
istartswithcase-insensitive startswith
endswithLIKE %x
iendswithcase-insensitive endswith
Product.objects.filter(name__icontains='phone', price__range=(100, 1000))
Product.objects.filter(category_id__in=[1, 2, 3], deleted_at__isnull=True)

When you chain filters one after another, Django uses AND logic between them by default. Q objects let you also use OR, AND, and NOT whenever you need them.

from django.db.models import Q
Product.objects.filter(
Q(price__gt=500) | Q(stock__lt=10)
)
Product.objects.filter(~Q(is_active=True))
from django.db.models import Q
query = Q()
if min_price is not None:
query &= Q(price__gte=min_price)
if max_price is not None:
query &= Q(price__lte=max_price)
if keyword:
query &= Q(name__icontains=keyword) | Q(description__icontains=keyword)
products = Product.objects.filter(query)

F expressions let the database do the calculation itself, instead of doing it in Python. This helps avoid a problem called a race condition, which can happen when you read a value in Python, change it, and then save it back.

from django.db.models import F
Product.objects.filter(id=1).update(stock=F('stock') - 1)

Why this matters:

  • the update happens safely in one single database step (atomic)
  • it is safer when many users update the same data at the same time
  • it avoids using old, outdated values that were sitting in memory
Product.objects.order_by('price')
Product.objects.order_by('-price')
Product.objects.order_by('?')
Product.objects.all()[:10]

This usually turns into SQL with LIMIT 10.

Paginator uses slicing internally, and it works best when your data has a fixed, stable order.

from django.core.paginator import Paginator
qs = Product.objects.order_by('-created_at')
paginator = Paginator(qs, 20)
page_1 = paginator.get_page(1)
Product.objects.values('id', 'name')
Product.objects.values('id', 'name', 'category__name')

This returns dictionaries.

Product.objects.values_list('name', flat=True)
Product.objects.values_list('id', 'name')

This returns tuples. If you select only one field and use flat=True, it returns a simple flat list instead.

Product.objects.values('category').distinct()

When to use this:

  • report-type API endpoints
  • lightweight API responses
  • improving performance when you do not need the full model object
Product.objects.defer('description')

This skips loading large columns right away, so the query runs faster.

Product.objects.only('name', 'price')

This loads only the fields you choose, first.

Section titled “select_related for ForeignKey and OneToOne”
Order.objects.select_related('user')
Order.objects.select_related('user', 'product')
Order.objects.select_related('user__profile')
# here profile is a OneToOne field on User so we can use double underscore to select it in the same query. We can also select multiple levels of related objects in the same query using double underscores.

select_related uses an SQL JOIN to load related single-value objects in the same query.

Section titled “prefetch_related for ManyToMany and reverse relations”
Category.objects.prefetch_related('products')
Order.objects.prefetch_related('items', 'items__product')
# here order have a reverse relation to items and items have a foreign key relation to product so we can prefetch both of them in the same query using double underscores.

prefetch_related runs separate queries one by one, and then joins the results together in Python.

flowchart TD A[Load categories] --> B[1 query] B --> C[Loop categories] C --> D[Each category loads products separately] D --> E[N extra queries] E --> F[Total: 1 + N queries]

Use select_related or prefetch_related to avoid this N+1 problem.

from django.db.models import Avg, Count, Max, Min, Sum
Product.objects.aggregate(
avg_price=Avg('price'),
total_stock=Sum('stock'),
total_items=Count('id'),
max_price=Max('price'),
min_price=Min('price'),
)

aggregate() gives you back a dictionary, not a QuerySet.

annotate() adds a calculated field to each row.

from django.db.models import Count
Category.objects.annotate(product_count=Count('products'))
Expression
|__ Value
|__ F
|__ Aggregate
|__ Func
aggregateannotate
Gives one summary for everythingGives a calculated value for each row
Returns a single dictionaryReturns a QuerySet
from django.db.models.functions import Length
Product.objects.annotate(name_length=Length('name'))
from django.db.models import F, Func, Value
from django.db.models.functions import Concat
Product.objects.annotate(
full_name=Func(F('first_name'), Value(' '), F('last_name'), function='CONCAT')
)
Product.objects.annotate(
full_name=Concat('first_name', Value(' '), 'last_name')
)

Use Value(' ') when you want a fixed constant value. If you use F(' ') instead, Django will wrongly try to look for a field named space.

  • Length
  • Upper
  • Lower
  • Concat
  • ExtractYear

Use Func when you need a database-specific function that Django does not already provide as a built-in helper.

In Django ORM, grouping data is usually done using values(...).annotate(...) together.

from django.db.models import Count
Product.objects.values('category').annotate(total=Count('id'))

This turns into SQL with GROUP BY category.

Use ExpressionWrapper when Django cannot automatically figure out what type the result of an expression should be.

from django.db.models import DecimalField, ExpressionWrapper, F
Product.objects.annotate(
total_price=ExpressionWrapper(
F('price') * F('quantity'),
output_field=DecimalField(max_digits=12, decimal_places=2),
)
)

When you use GenericForeignKey, you query using ContentType along with the target object’s id.

from django.contrib.contenttypes.models import ContentType
ct = ContentType.objects.get_for_model(Product)
Comment.objects.filter(content_type=ct, object_id=product_id)

Trade-offs:

  • there is no normal foreign key constraint in the database
  • joins become harder to do
  • it can be slower than standard relations

Custom managers let you keep reusable query logic in one place, so you do not repeat the same code again and again.

from django.db import models
class ActiveManager(models.Manager):
def active(self):
return self.filter(is_active=True)
class Product(models.Model):
is_active = models.BooleanField(default=True)
objects = models.Manager()
active_objects = ActiveManager()

Usage:

Product.active_objects.active()

Once a QuerySet runs, its results get saved (cached) for reuse.

qs = Product.objects.all()
list(qs) # query runs
list(qs) # uses cached results in same queryset object

This cache is skipped (not used) when:

  • a new queryset is created
  • the queryset chain changes (filter, exclude, etc.)
  • the data changes outside Django, directly in the database
qs = Product.objects.all()
qs2 = qs.filter(price__gt=100) # new queryset, new SQL
product = Product.objects.create(name='Phone', price=1000)
product = Product()
product.name = 'Phone'
product.price = 1000
product.save()
Product.objects.bulk_create([
Product(name='Phone', price=1000),
Product(name='Laptop', price=2000),
])

bulk_create is fast, but by default:

  • it does not call each object’s save() method
  • it does not fire signals the normal way for each row
Product.objects.filter(id=1).update(price=200)

update() writes directly to the database using SQL, and it does not call the model’s save() method.

product = Product.objects.get(pk=1)
product.price = 200
product.save()

Use the fetch-then-save method when you need validation, custom logic inside save(), or you want signals to fire.

Product.objects.filter(id=1).delete()

Deleting follows the on_delete rules you set on relations (like CASCADE, PROTECT, etc.).

Always check what else will get deleted (the cascade effect) before deleting data in production.

Transactions help keep your data consistent when you are doing multiple related writes together.

from django.db import transaction
with transaction.atomic():
order.save()
payment.save()

If an error happens, all the changes made inside the atomic() block get undone (rolled back).

If you nest transactions inside each other, Django uses savepoints internally to manage them.

flowchart LR A[Begin atomic block] --> B[Write order] B --> C[Write payment] C --> D{Any exception?} D -- No --> E[Commit] D -- Yes --> F[Rollback]

You should normally use ORM by default, but raw SQL is okay to use for complex or database-specific cases.

Product.objects.raw(
'SELECT * FROM product WHERE price > %s',
[100],
)
from django.db import connection
with connection.cursor() as cursor:
cursor.execute('SELECT COUNT(*) FROM product')
row = cursor.fetchone()

Use these in this order:

  • values() or values_list() when you do not need full model instances.
  • select_related() for FK and OneToOne relations.
  • prefetch_related() for ManyToMany and reverse relations.
  • Check Django Debug Toolbar or your query logs to avoid N+1 queries.

Lazy Evaluation

QuerySets are lazy. The SQL only runs when you actually use the result.

Immutability

Every time you chain a QuerySet, Django creates a brand new QuerySet.

Related Loading

Use select_related for joins, and prefetch_related for results that come from separate queries and get merged together.

Q and F

Q is used for OR/NOT logic, and F is used for safe, database-side calculations.

annotate vs aggregate

annotate adds a calculated value to each row, while aggregate gives one summary for the whole query.

Transactions

Use atomic blocks to keep your data consistent and to safely roll back if something goes wrong.

Custom Managers

Keep reusable query logic inside managers and querysets to keep your code clean and organized.

Raw SQL

Use raw SQL only when really needed, and always parameterize your inputs.