The Problem

When you remove a field from a Django model, there’s a dangerous gap during deployment: the migration runs first (dropping the column), but old code pods are still running that expect the field to exist. This causes ProgrammingError: column X does not exist.

The Solution: Two-Phase SeparateDatabaseAndState

Phase 1: Remove from Django, Keep in Database

Migration:

class Migration(migrations.Migration):
    operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=[
                migrations.RemoveField(
                    model_name='mymodel',
                    name='field_to_remove',
                ),
            ],
            database_operations=[
                # Empty - don't touch database yet
            ],
        ),
    ]

Model:

class MyModel(models.Model):
    # field_to_remove = models.CharField(...) - REMOVED
    other_field = models.CharField(...)

Deploy this. Django stops querying the field, but it still exists in the database (harmless).

Phase 2: Drop the Database Column

Migration:

class Migration(migrations.Migration):
    operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=[
                # Empty - state already correct
            ],
            database_operations=[
                migrations.RunSQL(
                    sql="ALTER TABLE myapp_mymodel DROP COLUMN field_to_remove;",
                    reverse_sql="ALTER TABLE myapp_mymodel ADD COLUMN field_to_remove VARCHAR(100);"
                ),
            ],
        ),
    ]

Deploy this to clean up the unused column.

Why This Works

  • Phase 1: Django’s ORM stops selecting the field, so no code errors when the column eventually disappears
  • Phase 2: Safe to drop the column because no code references it anymore
  • No gap: There’s never a mismatch between what Django expects and what exists in the database