Lazy Evaluation
QuerySets are lazy. The SQL only runs when you actually use the result.
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:
Django ORM is not:
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.
This simple picture helps you understand:
| Component | Purpose |
|---|---|
| Model | Connects to a database table |
| Manager | The starting point for queries |
| QuerySet | A query that waits before running |
| Field | Defines a column |
| Lookup | Used to build WHERE conditions |
| Expression | Used to build SQL expressions |
qs = Product.objects.all() # Query is not executed yetThe query only runs when you actually use the result. For example, this happens when you:
qslist(qs)if qs:qs = Product.objects.filter(is_active=True)print(qs) # often triggers evaluationproducts = list(qs) # definitely evaluatesResetting your database is useful while you are still developing your app, especially when you are changing your database structure a lot.
python manage.py flushThis command removes your data but keeps your tables and migration history safe.
rm db.sqlite3rm -rf app/migrationspython manage.py makemigrationspython manage.py migrateA Manager is the starting point for talking to your database.
Product.objectsobjects 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)| Manager | QuerySet |
|---|---|
| The starting point | Builds the query |
| Usually one default per model | Can be chained, and stays unchanged once created (immutable) |
Example: objects | Example: 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:
DoesNotExistMultipleObjectsReturnedfrom django.core.exceptions import ObjectDoesNotExist
try: product = Product.objects.get(id=1)except Product.DoesNotExist: product = NoneProduct.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| Lookup | Meaning |
|---|---|
exact | = |
iexact | case-insensitive exact |
contains | LIKE %x% |
icontains | case-insensitive contains |
in | IN (...) |
gt / gte | > / >= |
lt / lte | < / <= |
range | BETWEEN |
isnull | IS NULL |
startswith | LIKE x% |
istartswith | case-insensitive startswith |
endswith | LIKE %x |
iendswith | case-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:
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:
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.
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.
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.
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| aggregate | annotate |
|---|---|
| Gives one summary for everything | Gives a calculated value for each row |
| Returns a single dictionary | Returns a QuerySet |
from django.db.models.functions import Length
Product.objects.annotate(name_length=Length('name'))from django.db.models import F, Func, Valuefrom 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.
LengthUpperLowerConcatExtractYearUse 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:
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 runslist(qs) # uses cached results in same queryset objectThis cache is skipped (not used) when:
filter, exclude, etc.)qs = Product.objects.all()qs2 = qs.filter(price__gt=100) # new queryset, new SQLproduct = Product.objects.create(name='Phone', price=1000)product = Product()product.name = 'Phone'product.price = 1000product.save()Product.objects.bulk_create([ Product(name='Phone', price=1000), Product(name='Laptop', price=2000),])bulk_create is fast, but by default:
save() methodProduct.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 = 200product.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.
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.Use these rules:
F() for atomic counter and stock updates.transaction.atomic() for multi-step writes.get() then save() when model hooks must run.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.