Indexes & Database Constraints
Enforce data integrity at the database level with UniqueConstraint and CheckConstraint, and speed reads with indexes.
Validation in a form or a clean() method is a courtesy; a database constraint is a guarantee. Application checks can be bypassed by a shell script, a bulk update, or a race between two requests. Push the rules that must hold down into the schema, where nothing can sneak past them.
Meta.constraints and Meta.indexes
Django 5.x expresses both declaratively in the model's Meta:
UniqueConstraint— uniqueness, optionally conditional (viacondition=) or across expressions. This is how you say ‘only one active subscription per user’.CheckConstraint— a boolean rule the row must satisfy, e.g.end_date > start_date.Index— speeds up the columns you filter/order by most; supports partial and functional indexes.
from django.db import models
class Booking(models.Model):
room = models.ForeignKey('Room', on_delete=models.CASCADE)
start = models.DateField()
end = models.DateField()
class Meta:
constraints = [
models.CheckConstraint(
condition=models.Q(end__gt=models.F('start')),
name='booking_end_after_start',
),
]Note: in Django 5.1+ the argument is condition= (the old check= is deprecated). A conditional UniqueConstraint replaces the blunt unique_together for anything nuanced.
Example
from django.db import models
from django.db.models import Q, F, UniqueConstraint, CheckConstraint, Index
from django.db.models.functions import Lower
class Subscription(models.Model):
class Status(models.TextChoices):
ACTIVE = 'active', 'Active'
CANCELLED = 'cancelled', 'Cancelled'
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
plan = models.CharField(max_length=50)
status = models.CharField(max_length=20, choices=Status.choices)
price = models.DecimalField(max_digits=8, decimal_places=2)
starts = models.DateField()
ends = models.DateField(null=True, blank=True)
class Meta:
constraints = [
# At most ONE active subscription per user (partial unique index).
UniqueConstraint(
fields=['user'],
condition=Q(status='active'),
name='one_active_subscription_per_user',
),
# Price can never be negative.
CheckConstraint(
condition=Q(price__gte=0),
name='subscription_price_non_negative',
),
# If an end date exists it must be after the start.
CheckConstraint(
condition=Q(ends__isnull=True) | Q(ends__gt=F('starts')),
name='subscription_ends_after_starts',
),
]
indexes = [
# Composite index matching the app's hottest query.
Index(fields=['user', 'status'], name='sub_user_status_idx'),
# Functional index for case-insensitive plan lookups.
Index(Lower('plan'), name='sub_plan_lower_idx'),
]
# A violation raises django.db.utils.IntegrityError at write time —
# no application code can slip a second active subscription through.When to use it
- A developer adds a composite index on (author, published_at) to speed up queries that filter by author and sort by date.
- A team uses UniqueConstraint with a condition to enforce uniqueness only on published posts, not drafts.
- A developer uses CheckConstraint to prevent a product's price from being stored as a negative number.
More examples
Add indexes via Meta
Declares composite and single-column indexes so common queries avoid full table scans.
class Post(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE)
published_at = models.DateTimeField(null=True)
class Meta:
indexes = [
models.Index(fields=["author", "-published_at"]),
models.Index(fields=["slug"]),
]Conditional UniqueConstraint
Enforces slug uniqueness only for published posts so multiple drafts can share the same slug.
from django.db.models import UniqueConstraint, Q
class Post(models.Model):
slug = models.SlugField()
draft = models.BooleanField(default=True)
class Meta:
constraints = [
UniqueConstraint(
fields=["slug"],
condition=Q(draft=False),
name="unique_published_slug",
)
]CheckConstraint on a field
Adds a database-level CHECK constraint ensuring the price column can never hold a negative value.
from django.db.models import CheckConstraint, Q
class Product(models.Model):
price = models.DecimalField(max_digits=8, decimal_places=2)
class Meta:
constraints = [
CheckConstraint(
check=Q(price__gte=0),
name="product_price_non_negative",
)
]
Discussion