Django ORM Queries: The 80-20 Guide#
Complete guide to Django ORM covering the 20% of queries you'll use 80% of the time.
🎯 Basic Queries#
Get All Objects#
from myapp.models import Post
# Get all posts
posts = Post.objects.all()
# Iterate
for post in posts:
print(post.title)
Get Single Object#
# Get by primary key
post = Post.objects.get(pk=1)
post = Post.objects.get(id=1)
# Get by field
post = Post.objects.get(title='My Post')
# Raises DoesNotExist if not found
# Raises MultipleObjectsReturned if multiple found
# Safe get (returns None)
post = Post.objects.filter(pk=1).first()
Filter (WHERE)#
# Basic filter
published_posts = Post.objects.filter(is_published=True)
# Multiple conditions (AND)
posts = Post.objects.filter(is_published=True, author_id=1)
# Chaining filters (AND)
posts = Post.objects.filter(is_published=True).filter(author_id=1)
# Exclude
draft_posts = Post.objects.exclude(is_published=True)
# Complex exclude
posts = Post.objects.exclude(is_published=True, author_id=1)
🔍 Field Lookups#
Exact Match#
# Exact (default)
posts = Post.objects.filter(title='My Post')
posts = Post.objects.filter(title__exact='My Post')
# Case-insensitive
posts = Post.objects.filter(title__iexact='my post')
Contains#
# Contains (case-sensitive)
posts = Post.objects.filter(title__contains='Django')
# Case-insensitive contains
posts = Post.objects.filter(title__icontains='django')
# Starts with
posts = Post.objects.filter(title__startswith='How')
# Case-insensitive starts with
posts = Post.objects.filter(title__istartswith='how')
# Ends with
posts = Post.objects.filter(title__endswith='Guide')
# Case-insensitive ends with
posts = Post.objects.filter(title__iendswith='guide')
Comparison#
# Greater than
posts = Post.objects.filter(views__gt=100)
# Greater than or equal
posts = Post.objects.filter(views__gte=100)
# Less than
posts = Post.objects.filter(views__lt=100)
# Less than or equal
posts = Post.objects.filter(views__lte=100)
# Range
from datetime import datetime, timedelta
week_ago = datetime.now() - timedelta(days=7)
posts = Post.objects.filter(created_at__gte=week_ago)
Date/Time Lookups#
from django.utils import timezone
from datetime import date, timedelta
# Year
posts = Post.objects.filter(created_at__year=2024)
# Month
posts = Post.objects.filter(created_at__month=1)
# Day
posts = Post.objects.filter(created_at__day=15)
# Week day (1=Sunday, 7=Saturday)
posts = Post.objects.filter(created_at__week_day=1)
# Date range
today = date.today()
posts = Post.objects.filter(created_at__date=today)
# Today
posts = Post.objects.filter(created_at__date=timezone.now().date())
# This week
week_start = timezone.now() - timedelta(days=7)
posts = Post.objects.filter(created_at__gte=week_start)
Null/Empty#
# Is null
posts = Post.objects.filter(published_at__isnull=True)
# Is not null
posts = Post.objects.filter(published_at__isnull=False)
# Empty string
posts = Post.objects.filter(excerpt='')
# Not empty
posts = Post.objects.exclude(excerpt='')
In#
# In list
posts = Post.objects.filter(id__in=[1, 2, 3, 5, 8])
# In queryset
author_ids = User.objects.filter(is_staff=True).values_list('id', flat=True)
posts = Post.objects.filter(author_id__in=author_ids)
Regex#
# Case-sensitive regex
posts = Post.objects.filter(title__regex=r'^[A-Z]')
# Case-insensitive regex
posts = Post.objects.filter(title__iregex=r'^[a-z]')
🔗 Related Object Lookups#
ForeignKey Lookups#
# Get posts by author
posts = Post.objects.filter(author__username='admin')
# Get posts by author's email
posts = Post.objects.filter(author__email='admin@example.com')
# Reverse lookup (from User to Post)
from django.contrib.auth.models import User
user = User.objects.get(username='admin')
posts = user.post_set.all() # Or user.posts.all() if related_name='posts'
# Multiple levels
posts = Post.objects.filter(author__profile__bio__contains='developer')
ManyToMany Lookups#
# Posts with specific tag
posts = Post.objects.filter(tags__name='Django')
# Posts with any of these tags
posts = Post.objects.filter(tags__name__in=['Django', 'Python'])
# Posts with all these tags
from django.db.models import Q
posts = Post.objects.filter(
Q(tags__name='Django') & Q(tags__name='Python')
).distinct()
# Posts without tag
posts = Post.objects.exclude(tags__name='Django')
🔀 Q Objects (Complex Queries)#
OR Conditions#
from django.db.models import Q
# OR
posts = Post.objects.filter(
Q(title__contains='Django') | Q(title__contains='Python')
)
# AND
posts = Post.objects.filter(
Q(is_published=True) & Q(views__gt=100)
)
# NOT
posts = Post.objects.filter(
~Q(is_published=True)
)
# Complex
posts = Post.objects.filter(
Q(is_published=True) &
(Q(title__contains='Django') | Q(content__contains='Django'))
)
Combining Q with filter#
# Q objects can be combined with regular filters
posts = Post.objects.filter(
is_published=True
).filter(
Q(title__contains='Django') | Q(content__contains='Django')
)
📊 Sorting#
Order By#
# Ascending
posts = Post.objects.all().order_by('created_at')
# Descending
posts = Post.objects.all().order_by('-created_at')
# Multiple fields
posts = Post.objects.all().order_by('is_published', '-created_at')
# Random
posts = Post.objects.all().order_by('?')
# Reverse existing order
posts = Post.objects.all().order_by('created_at').reverse()
Model Meta Ordering#
# In model
class Post(models.Model):
class Meta:
ordering = ['-created_at'] # Default ordering
# Override in query
posts = Post.objects.all().order_by('title') # Overrides model ordering
🔢 Limiting & Slicing#
Limit Results#
# First 10
posts = Post.objects.all()[:10]
# Skip first 5, get next 10
posts = Post.objects.all()[5:15]
# Last 10 (inefficient, use order_by)
posts = Post.objects.all().order_by('-id')[:10]
# Get single object
post = Post.objects.all()[0] # First
post = Post.objects.first() # Better
post = Post.objects.last() # Last (requires ordering)
Exists & Count#
# Check if exists
if Post.objects.filter(title='My Post').exists():
print('Post exists')
# Count
count = Post.objects.filter(is_published=True).count()
# Check count
if Post.objects.filter(is_published=True).count() > 10:
print('More than 10 published posts')
🎯 Select Specific Fields#
Only (Defer Other Fields)#
# Only load specific fields
posts = Post.objects.only('id', 'title', 'created_at')
# Other fields accessed will trigger additional query
# Usage
for post in posts:
print(post.title) # No extra query
print(post.content) # Extra query to fetch content
Defer (Exclude Fields)#
# Exclude heavy fields
posts = Post.objects.defer('content', 'excerpt')
# Content and excerpt not loaded initially
# Usage
for post in posts:
print(post.title) # No extra query
print(post.content) # Extra query to fetch content
Values (Dictionary)#
# Get dictionaries instead of model instances
posts = Post.objects.values('id', 'title', 'created_at')
# Returns: [{'id': 1, 'title': 'Post 1', 'created_at': ...}, ...]
# Values with related fields
posts = Post.objects.values('id', 'title', 'author__username')
# Flat values (single field)
titles = Post.objects.values_list('title', flat=True)
# Returns: ['Post 1', 'Post 2', ...]
# Named tuples
posts = Post.objects.values_list('id', 'title', named=True)
# Returns: [Row(id=1, title='Post 1'), ...]
🔗 Select Related (JOIN)#
select_related (ForeignKey, OneToOne)#
# Reduces queries for ForeignKey
posts = Post.objects.select_related('author', 'category')
# Without select_related (N+1 problem)
for post in Post.objects.all():
print(post.author.username) # Query for each post
# With select_related (1 query)
for post in Post.objects.select_related('author').all():
print(post.author.username) # No extra query
# Multiple levels
posts = Post.objects.select_related('author__profile')
prefetch_related (ManyToMany, Reverse ForeignKey)#
# Reduces queries for ManyToMany and reverse ForeignKey
posts = Post.objects.prefetch_related('tags', 'comments')
# Without prefetch_related (N+1 problem)
for post in Post.objects.all():
for tag in post.tags.all(): # Query for each post
print(tag.name)
# With prefetch_related (2 queries total)
for post in Post.objects.prefetch_related('tags').all():
for tag in post.tags.all(): # No extra query
print(tag.name)
# Prefetch with filtering
from django.db.models import Prefetch
posts = Post.objects.prefetch_related(
Prefetch('tags', queryset=Tag.objects.filter(is_active=True))
)
Combined#
# Use both for optimal performance
posts = Post.objects.select_related('author').prefetch_related('tags')
📈 Aggregate Functions#
Basic Aggregates#
from django.db.models import Count, Sum, Avg, Max, Min
# Count
total = Post.objects.count()
published_count = Post.objects.filter(is_published=True).count()
# Using aggregate
from django.db.models import Count
stats = Post.objects.aggregate(
total=Count('id'),
published=Count('id', filter=Q(is_published=True))
)
# Sum
total_views = Post.objects.aggregate(total_views=Sum('views'))
# Average
avg_views = Post.objects.aggregate(avg_views=Avg('views'))
# Max/Min
max_views = Post.objects.aggregate(max_views=Max('views'))
min_views = Post.objects.aggregate(min_views=Min('views'))
Group By (annotate)#
from django.db.models import Count, Avg
# Count posts per author
authors = User.objects.annotate(post_count=Count('posts'))
for author in authors:
print(f"{author.username}: {author.post_count} posts")
# Average views per author
authors = User.objects.annotate(avg_views=Avg('posts__views'))
# Multiple annotations
authors = User.objects.annotate(
post_count=Count('posts'),
avg_views=Avg('posts__views'),
total_views=Sum('posts__views')
)
🏷️ Annotate (Add Computed Fields)#
from django.db.models import Count, F, Value, Case, When
# Add computed field
posts = Post.objects.annotate(
title_length=Length('title')
)
# Use F() to reference other fields
posts = Post.objects.annotate(
views_per_day=F('views') / F('days_since_created')
)
# Conditional annotation
posts = Post.objects.annotate(
status_label=Case(
When(is_published=True, then=Value('Published')),
default=Value('Draft'),
output_field=CharField()
)
)
# Count related objects
posts = Post.objects.annotate(
comment_count=Count('comments'),
tag_count=Count('tags')
)
🔍 Distinct#
# Remove duplicates
posts = Post.objects.filter(tags__name='Django').distinct()
# Important for ManyToMany and reverse ForeignKey
# Without distinct, you get duplicates
🎯 Common Query Patterns (80-20)#
Recent Published Posts#
from django.utils import timezone
from datetime import timedelta
week_ago = timezone.now() - timedelta(days=7)
posts = Post.objects.filter(
is_published=True,
created_at__gte=week_ago
).order_by('-created_at')[:10]
Search#
from django.db.models import Q
def search_posts(query):
return Post.objects.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(excerpt__icontains=query)
).distinct()
Popular Posts#
posts = Post.objects.filter(
is_published=True
).order_by('-views', '-created_at')[:10]
Posts by Category with Tags#
posts = Post.objects.filter(
category__slug='django',
tags__name__in=['tutorial', 'guide']
).select_related('category', 'author').prefetch_related('tags').distinct()
⚡ Performance Tips#
- ✅ Use select_related for ForeignKey/OneToOne
- ✅ Use prefetch_related for ManyToMany/reverse ForeignKey
- ✅ Use only()/defer() for large text fields
- ✅ Use values()/values_list() when you don't need model instances
- ✅ Add .distinct() after ManyToMany filters
- ✅ Use .exists() instead of .count() > 0
- ✅ Use .first() instead of [0]
- ✅ Index frequently queried fields
✅ Next Steps#
- Learn CRUD Operations for creating/updating data
- Learn Advanced Topics for transactions and caching
Pro Tip: Use django-debug-toolbar to see all SQL queries. Most performance issues come from N+1 queries - use select_related and prefetch_related!