Django 5.0 Database Computed Columns

django python

Django 5.0 introduced two powerful features: GeneratedField for computed columns and db_default for database-level defaults. Here’s how to use them effectively.

db_default: Database-Level Defaults

Basic Usage

from django.db import models
from django.db.models.functions import Now, Random

class Order(models.Model):
    # Database generates timestamp
    created_at = models.DateTimeField(db_default=Now())
    
    # Database generates random float
    sort_key = models.FloatField(db_default=Random())
    
    # Static default at database level
    status = models.CharField(max_length=20, db_default="pending")

Why db_default Over default?

# Python default (runs in application)
created_at = models.DateTimeField(default=timezone.now)

# Database default (runs in database)
created_at = models.DateTimeField(db_default=Now())
AspectPython defaultdb_default
Works with bulk_createNoYes
Works with raw SQLNoYes
Timezone handlingPythonDatabase
ConsistencyPer-processDatabase-wide

Database Functions

from django.db.models import F, Value
from django.db.models.functions import Now, Concat, Lower

class Article(models.Model):
    title = models.CharField(max_length=200)
    
    # Slug from title (simplified)
    slug = models.CharField(
        max_length=200,
        db_default=Lower(F("title"))  # Won't work—F() at creation time
    )
    
    # Timestamp
    published_at = models.DateTimeField(db_default=Now())

UUID Generation

# PostgreSQL
class Document(models.Model):
    uuid = models.UUIDField(
        db_default=models.Func(function="gen_random_uuid")
    )

GeneratedField: Computed Columns

What Is It

GeneratedField creates columns computed from other columns:

from django.db.models import F, GeneratedField

class Product(models.Model):
    price = models.DecimalField(max_digits=10, decimal_places=2)
    tax_rate = models.DecimalField(max_digits=5, decimal_places=4)
    
    # Computed by database, stored in database
    price_with_tax = GeneratedField(
        expression=F("price") * (1 + F("tax_rate")),
        output_field=models.DecimalField(max_digits=10, decimal_places=2),
        db_persist=True  # Stored, not computed on every read
    )

db_persist Options

# STORED: Computed on INSERT/UPDATE, stored physically
price_with_tax = GeneratedField(
    expression=...,
    output_field=...,
    db_persist=True
)

# VIRTUAL: Computed on every read (PostgreSQL, MySQL)
price_with_tax = GeneratedField(
    expression=...,
    output_field=...,
    db_persist=False
)
db_persistStorageComputationIndexable
TruePhysicalOn writeYes
FalseNoneOn readNo*

*Virtual columns can be indexed in some databases.

Use Cases

Full Name

class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    
    full_name = GeneratedField(
        expression=Concat(
            F("first_name"),
            Value(" "),
            F("last_name")
        ),
        output_field=models.CharField(max_length=201),
        db_persist=True
    )

Search Vector (PostgreSQL)

from django.contrib.postgres.search import SearchVector

class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    
    search_vector = GeneratedField(
        expression=SearchVector("title", weight="A") + 
                   SearchVector("body", weight="B"),
        output_field=models.GeneratedField(),
        db_persist=True
    )
    
    class Meta:
        indexes = [
            GinIndex(fields=["search_vector"])
        ]

Derived Status

class Subscription(models.Model):
    expires_at = models.DateTimeField()
    created_at = models.DateTimeField(db_default=Now())
    
    is_active = GeneratedField(
        expression=Q(expires_at__gt=Now()),
        output_field=models.BooleanField(),
        db_persist=False  # Virtual—always current
    )

Indexing Generated Fields

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    quantity = models.IntegerField()
    
    total_value = GeneratedField(
        expression=F("price") * F("quantity"),
        output_field=models.DecimalField(max_digits=15, decimal_places=2),
        db_persist=True
    )
    
    class Meta:
        indexes = [
            models.Index(fields=["total_value"]),
        ]

Queries filtering on total_value now use the index.

Migration Example

# Generated migration
class Migration(migrations.Migration):
    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='product',
            name='price_with_tax',
            field=models.GeneratedField(
                db_persist=True,
                expression=django.db.models.expressions.CombinedExpression(
                    django.db.models.expressions.F('price'),
                    '*',
                    django.db.models.expressions.CombinedExpression(
                        django.db.models.expressions.Value(1),
                        '+',
                        django.db.models.expressions.F('tax_rate')
                    )
                ),
                output_field=models.DecimalField(decimal_places=2, max_digits=10)
            ),
        ),
    ]

Database Support

Databasedb_defaultGeneratedField (Stored)GeneratedField (Virtual)
PostgreSQL
MySQL
SQLite
Oracle

Best Practices

1. Use for Truly Derived Data

# Good: Derived from other fields
total = GeneratedField(
    expression=F("quantity") * F("unit_price"),
    ...
)

# Bad: Could just be a property
@property
def total(self):
    return self.quantity * self.unit_price

Use GeneratedField when you need to query/index the value.

2. Consider Update Cost

# Stored: Updates on every write
# If price or tax_rate changes frequently, consider virtual
price_with_tax = GeneratedField(..., db_persist=False)

3. Combine with Indexes

class Meta:
    indexes = [
        models.Index(fields=["generated_field_name"])
    ]

If you’re querying on generated fields, index them.

Final Thoughts

db_default and GeneratedField push logic to the database where it belongs for certain use cases. They enable:

Use them when your computed data needs to be queried, not just displayed.


Let the database do the work it’s good at.

All posts