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 (via condition=) 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

Example · python
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.

Example · python
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.

Example · python
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.

Example · python
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

  • Be the first to comment on this lesson.