Django ORM cheatsheet.
Basics
Post.objects.all()
Post.objects.first()
Post.objects.last()
Post.objects.count()
Post.objects.get(id=1) # raises if 0 or multiple
Post.objects.filter(published=True)
Post.objects.exclude(deleted=True)
Post.objects.exists() # True if any
Field lookups
Post.objects.filter(title__exact="x")
Post.objects.filter(title__iexact="x") # case-insensitive
Post.objects.filter(title__contains="x")
Post.objects.filter(title__icontains="x")
Post.objects.filter(title__startswith="x")
Post.objects.filter(title__endswith="x")
Post.objects.filter(title__regex=r"^[A-Z]")
Post.objects.filter(views__gt=100)
Post.objects.filter(views__gte=100)
Post.objects.filter(views__lt=100)
Post.objects.filter(views__lte=100)
Post.objects.filter(views__range=(10, 100))
Post.objects.filter(id__in=[1, 2, 3])
Post.objects.filter(slug__isnull=False)
Post.objects.filter(published_at__year=2026)
Post.objects.filter(published_at__date=date(2026, 1, 1))
Chaining
Post.objects.filter(published=True).exclude(views=0).order_by("-views")[:10]
QuerySets are lazy — query runs on iterate/index/list.
Order / slice
Post.objects.order_by("title")
Post.objects.order_by("-views")
Post.objects.order_by("title", "-views")
Post.objects.order_by("?") # random (slow)
Post.objects.all()[:10]
Post.objects.all()[10:20]
Q objects (OR / NOT)
from django.db.models import Q
Post.objects.filter(Q(views__gt=100) | Q(comments__gt=10))
Post.objects.filter(Q(title__icontains="x") & ~Q(deleted=True))
F objects (column refs)
from django.db.models import F
# Compare two columns
Post.objects.filter(views__gt=F("comments") * 10)
# Atomic update
Post.objects.filter(id=1).update(views=F("views") + 1)
Avoid race conditions on counters.
Aggregation
from django.db.models import Count, Sum, Avg, Min, Max
stats = Post.objects.aggregate(
total=Count("id"),
avg_views=Avg("views"),
max_views=Max("views"),
)
# {"total": 100, "avg_views": 50.0, "max_views": 1000}
annotate (per-row)
authors = User.objects.annotate(post_count=Count("post"))
for a in authors:
print(a.username, a.post_count)
posts = Post.objects.annotate(comment_count=Count("comments"))
Relationships
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
tags = models.ManyToManyField(Tag)
# Forward
post.author # one query
post.tags.all() # one query
# Reverse
user.post_set.all()
# Or with related_name:
class Post(models.Model):
author = models.ForeignKey(User, related_name="posts", on_delete=models.CASCADE)
user.posts.all()
N+1 problem
# BAD: 1 + N queries
for post in Post.objects.all():
print(post.author.name) # extra query per post
# GOOD: 1 query with JOIN
for post in Post.objects.select_related("author"):
print(post.author.name)
select_related (FK, OneToOne)
Post.objects.select_related("author")
Post.objects.select_related("author", "category")
Post.objects.select_related("author__profile") # nested
prefetch_related (M2M, reverse FK)
# BAD
for post in Post.objects.all():
for tag in post.tags.all(): # extra query per post
...
# GOOD
Post.objects.prefetch_related("tags")
Prefetch (filtered prefetch)
from django.db.models import Prefetch
Post.objects.prefetch_related(
Prefetch("comments", queryset=Comment.objects.filter(approved=True))
)
Subqueries
from django.db.models import Subquery, OuterRef
latest = Comment.objects.filter(post=OuterRef("pk")).order_by("-created_at")
posts = Post.objects.annotate(latest_comment=Subquery(latest.values("id")[:1]))
values / values_list
Post.objects.values("id", "title") # dicts
Post.objects.values_list("id", "title") # tuples
Post.objects.values_list("id", flat=True) # flat list of ids
bulk_create / bulk_update
Post.objects.bulk_create([
Post(title="a"),
Post(title="b"),
])
Post.objects.bulk_update(posts, ["title"])
Much faster than save() in loop.
update / delete
Post.objects.filter(deleted=True).update(archived=True)
Post.objects.filter(views=0).delete()
Bulk operations don’t call save() or signals.
Raw SQL
Post.objects.raw("SELECT id, title FROM blog_post WHERE views > %s", [100])
from django.db import connection
with connection.cursor() as c:
c.execute("SELECT ...", [...])
rows = c.fetchall()
Transactions
from django.db import transaction
with transaction.atomic():
a.save()
b.save()
# All-or-nothing
Decorator form:
@transaction.atomic
def update_user(...):
...
select_for_update
with transaction.atomic():
user = User.objects.select_for_update().get(id=1)
user.balance -= 10
user.save()
Row-locks until transaction ends.
explain
print(Post.objects.filter(views__gt=100).explain())
print(Post.objects.filter(views__gt=100).query)
Django Debug Toolbar
uv add --dev django-debug-toolbar
Shows queries per request. Essential for finding N+1.
Common mistakes
- N+1 without select_related/prefetch_related.
len(qs)to count — usescount(*)query. Use.count().if qs:evaluates the whole queryset. Useqs.exists().update()on chainedselect_related— unsupported.- Forgetting
index=Trueon FK / filter fields → slow queries.
Read this next
If you want my ORM optimization recipes, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .