QuerySets & Managers

Difficulty: Beginner

QuerySets are the primary way you retrieve data from the database in Django. A QuerySet represents a collection of objects from the database and can be filtered, sliced, and chained. The most important thing to understand about QuerySets is that they are lazy - they do not hit the database until you actually evaluate them (by iterating, slicing, calling len(), etc.).

Every model has at least one Manager, accessible via the 'objects' attribute. The default manager provides the initial QuerySet for database operations. Model.objects.all() returns a QuerySet of all objects. Model.objects.filter() returns objects matching the given criteria. Model.objects.exclude() returns objects NOT matching the criteria. Model.objects.get() returns a single object (raises DoesNotExist if not found, MultipleObjectsReturned if multiple match).

Field lookups are the heart of filtering. You pass them as keyword arguments using double-underscore syntax. 'name__exact' does an exact match. 'name__iexact' does case-insensitive matching. 'name__contains' checks if the field contains the value. 'price__gt', 'price__gte', 'price__lt', 'price__lte' do numeric comparisons. 'date__year', 'date__month' extract date parts. 'status__in' checks membership in a list. 'bio__isnull' checks for NULL values.

QuerySets are chainable - each filter/exclude/order_by call returns a new QuerySet, allowing you to build complex queries incrementally. Because they are lazy, chaining does not execute multiple queries. Django builds the complete SQL query and executes it only when the QuerySet is evaluated.

Q objects enable complex lookups with OR, AND, and NOT operations. By default, filter() arguments are ANDed together. To use OR logic, wrap conditions in Q() objects and combine them with the | operator. The ~ operator negates a Q object (NOT). Q objects can be combined with & (AND) and | (OR) to build arbitrarily complex queries.

F expressions reference model field values in queries without loading them into Python. They enable database-level operations like incrementing a counter, comparing two fields on the same model, or performing arithmetic between fields. F expressions are efficient because the operations happen entirely in the database.

Aggregation functions (Count, Sum, Avg, Max, Min) compute summary values across QuerySets. Use aggregate() for a single summary value (returns a dictionary) or annotate() to add computed values to each object in the QuerySet. Annotations are powerful for adding computed fields like comment counts, average ratings, or total sales to query results.

Custom managers let you encapsulate common queries. You create a custom manager by subclassing models.Manager and adding methods that return filtered QuerySets. This keeps view code clean and business logic centralized in the model layer.

Code examples

Basic QuerySet Operations

from myapp.models import Product

# Retrieve all products
all_products = Product.objects.all()

# Filter products
cheap = Product.objects.filter(price__lt=10.00)
active = Product.objects.filter(is_active=True)
electronics = Product.objects.filter(category__name='Electronics')

# Chain filters (AND)
cheap_active = Product.objects.filter(price__lt=10).filter(is_active=True)
# Same as:
cheap_active = Product.objects.filter(price__lt=10, is_active=True)

# Exclude
not_cheap = Product.objects.exclude(price__lt=10)

# Get single object (raises exception if 0 or 2+ results)
try:
    product = Product.objects.get(slug='django-book')
except Product.DoesNotExist:
    print('Product not found')
except Product.MultipleObjectsReturned:
    print('Multiple products found')

# Ordering
by_price = Product.objects.order_by('price')         # ascending
by_price_desc = Product.objects.order_by('-price')   # descending

# Slicing (LIMIT/OFFSET)
first_five = Product.objects.all()[:5]
page_two = Product.objects.all()[5:10]

QuerySets are lazy - none of these lines execute SQL until the QuerySet is evaluated (iterated, converted to list, etc.). Chaining filters creates AND conditions. get() should only be used when you expect exactly one result.

Field Lookups

from myapp.models import Article

# Text lookups
Article.objects.filter(title__exact='Django Guide')      # exact match
Article.objects.filter(title__iexact='django guide')     # case-insensitive
Article.objects.filter(title__contains='Django')         # SQL LIKE %Django%
Article.objects.filter(title__icontains='django')        # case-insensitive contains
Article.objects.filter(title__startswith='How')          # starts with
Article.objects.filter(title__endswith='?')              # ends with

# Numeric lookups
Article.objects.filter(views__gt=1000)                   # greater than
Article.objects.filter(views__gte=1000)                  # greater than or equal
Article.objects.filter(views__lt=100)                    # less than
Article.objects.filter(views__range=(100, 1000))         # between 100 and 1000

# Date lookups
Article.objects.filter(published__year=2024)
Article.objects.filter(published__month=6)
Article.objects.filter(published__date__gt='2024-01-01')

# Membership & null checks
Article.objects.filter(status__in=['draft', 'review'])
Article.objects.filter(category__isnull=True)

# Related model lookups (spanning relationships)
Article.objects.filter(author__username='admin')
Article.objects.filter(tags__name='python')

Field lookups use double-underscore syntax. They translate to SQL WHERE clauses. You can span relationships with double underscores too - author__username traverses the ForeignKey to the User model.

Q Objects and F Expressions

from django.db.models import Q, F
from myapp.models import Product

# Q objects for complex lookups
# OR: products that are cheap OR on sale
Product.objects.filter(
    Q(price__lt=10) | Q(on_sale=True)
)

# AND with OR: active AND (cheap OR on sale)
Product.objects.filter(
    Q(is_active=True) & (Q(price__lt=10) | Q(on_sale=True))
)

# NOT: products that are NOT in electronics
Product.objects.filter(~Q(category__name='Electronics'))

# F expressions for field references
# Products where sale_price < original_price
Product.objects.filter(sale_price__lt=F('original_price'))

# Increment a field without loading the object
Product.objects.filter(pk=1).update(stock=F('stock') - 1)

# Arithmetic between fields
Product.objects.annotate(
    discount=F('original_price') - F('sale_price')
)

Q objects enable OR logic that is not possible with keyword arguments alone. F expressions reference field values at the database level, avoiding race conditions and enabling efficient field comparisons and updates.

Aggregation and Custom Managers

from django.db.models import Count, Avg, Sum, Max, Min
from myapp.models import Order, Product

# aggregate() returns a dictionary
stats = Order.objects.aggregate(
    total_revenue=Sum('total'),
    avg_order=Avg('total'),
    max_order=Max('total'),
    order_count=Count('id'),
)
# {'total_revenue': Decimal('50000'), 'avg_order': Decimal('125'), ...}

# annotate() adds computed fields to each object
popular_products = Product.objects.annotate(
    order_count=Count('orderitem'),
    avg_rating=Avg('review__rating'),
).order_by('-order_count')

for product in popular_products:
    print(f'{product.name}: {product.order_count} orders, {product.avg_rating} avg rating')

# Custom Manager
class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='published')

class Article(models.Model):
    title = models.CharField(max_length=200)
    status = models.CharField(max_length=20)

    objects = models.Manager()           # default manager
    published = PublishedManager()       # custom manager

# Usage: Article.published.all()  -> only published articles

aggregate() returns a single dictionary of summary values. annotate() adds computed fields to each object in the QuerySet. Custom managers encapsulate common filters so you can write Article.published.all() instead of Article.objects.filter(status='published') everywhere.

Key points

Concepts covered

QuerySet, Manager, filter, exclude, Q Objects, F Expressions, Aggregation