High Risk
Dropping columns, dropping tables, changing data types without conversion.
Database setup and migrations are the backbone of a stable Django project. If these two parts are done correctly, your app grows safely. If done carelessly, you can face downtime, broken deployments, or data loss.
This chapter gives you a practical and beginner-friendly path:
Before migration work, your database configuration must be correct.
Django reads DATABASES from settings.py and uses it for all schema and query operations.
Use environment variables so the same code works for local, staging, and production. This avoids hardcoding secrets and makes deployments safer.
from pathlib import Pathimport os
BASE_DIR = Path(__file__).resolve().parent.parent
DB_ENGINE = os.getenv("DB_ENGINE", "sqlite")SQLite is perfect for learning, prototypes, and small apps. It is file-based, so setup is very fast and requires no DB server.
Install dependency
# No extra driver needed. SQLite support is built into Python.Django config
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parentDATA_DIR = BASE_DIR / "_data"DATA_DIR.mkdir(exist_ok=True)
DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": DATA_DIR / "db.sqlite3", }}MySQL is common in production projects and managed hosting platforms.
Install dependencies
uv add mysqlclientIf mysqlclient build fails on your machine, use:
uv add pymysqlThen activate pymysql in your project __init__.py:
import pymysql
pymysql.install_as_MySQLdb()Django config
import os
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": os.getenv("DB_NAME"), "USER": os.getenv("DB_USER"), "PASSWORD": os.getenv("DB_PASSWORD"), "HOST": os.getenv("DB_HOST", "127.0.0.1"), "PORT": os.getenv("DB_PORT", "3306"), "OPTIONS": { "charset": "utf8mb4", }, }}PostgreSQL is a strong choice for production because of reliability, indexing power, and advanced SQL features.
Install dependencies
uv add psycopg[binary]Alternative classic package:
uv add psycopg2-binaryDjango config
import os
DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.getenv("DB_NAME"), "USER": os.getenv("DB_USER"), "PASSWORD": os.getenv("DB_PASSWORD"), "HOST": os.getenv("DB_HOST", "127.0.0.1"), "PORT": os.getenv("DB_PORT", "5432"), }}DB_ENGINE = os.getenv("DB_ENGINE", "sqlite").lower()
if DB_ENGINE == "mysql": DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": os.getenv("DB_NAME"), "USER": os.getenv("DB_USER"), "PASSWORD": os.getenv("DB_PASSWORD"), "HOST": os.getenv("DB_HOST"), "PORT": os.getenv("DB_PORT", "3306"), "OPTIONS": {"charset": "utf8mb4"}, } }elif DB_ENGINE in ["postgres", "postgresql", "pgsql"]: DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "NAME": os.getenv("DB_NAME"), "USER": os.getenv("DB_USER"), "PASSWORD": os.getenv("DB_PASSWORD"), "HOST": os.getenv("DB_HOST"), "PORT": os.getenv("DB_PORT", "5432"), } }else: DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "_data" / "db.sqlite3", } }A Django migration is a Python file that records schema changes.
When you change models.py, Django can generate migration files and apply them in order.
python manage.py makemigrationspython manage.py migratepython manage.py showmigrationspython manage.py sqlmigrate app_name 0001| Command | What it does |
|---|---|
makemigrations | Creates migration files from model changes |
migrate | Applies unapplied migrations to DB |
showmigrations | Shows which migrations are applied/not applied |
sqlmigrate | Displays raw SQL for one migration |
Django creates and updates tables in dependency order.
If one model depends on another (for example through ForeignKey), parent migration must run first.
class Category(models.Model): name = models.CharField(max_length=120)
class Product(models.Model): name = models.CharField(max_length=120) category = models.ForeignKey(Category, on_delete=models.PROTECT)In this case, Category table must exist before Product table.
class Migration(migrations.Migration): dependencies = [ ("products", "0001_initial"), ]Migration applying is simple in command form, but production-safe applying needs process discipline.
python manage.py makemigrations --check --dry-run to verify no missing migration file.python manage.py showmigrations to inspect pending items.python manage.py migrate in staging first.python manage.py migratepython manage.py migrate app_namepython manage.py migrate app_name 0003python manage.py migrate app_name zero| Action | Command |
|---|---|
| Apply all pending | python manage.py migrate |
| Apply one app only | python manage.py migrate app_name |
| Roll back to specific migration | python manage.py migrate app_name 0003 |
| Unapply all app migrations | python manage.py migrate app_name zero |
Not all migrations are equally safe. Some operations are low-risk; some can destroy data if done without backup.
High Risk
Dropping columns, dropping tables, changing data types without conversion.
Medium Risk
Renaming fields incorrectly, changing null/unique rules on dirty data.
Low Risk
Adding nullable columns, adding new tables, adding indexes.
CharField to IntegerField when old values are non-numericnull=False on a column that already has null rows| Error | Why it happens | Typical fix |
|---|---|---|
No migrations to apply but model changed | You forgot makemigrations | Run makemigrations and commit file |
relation already exists | Manual SQL changed DB outside migration history | Align state using --fake carefully |
column does not exist | Migration history out of sync between environments | Compare applied migration list and fix order |
IntegrityError on migrate | Existing rows violate new constraints | Clean/fill data before constraint migration |
Conflicting migrations detected | Parallel branches created different migration heads | Create merge migration |
When your app already has users and data, schema changes should be incremental. Avoid big destructive jumps.
Instead of adding non-null field directly:
null=True and optional default.null=False in next migration.This two-step approach prevents downtime and integrity failures.
If rename detection fails:
RunPython migration.Conflicts usually happen when two branches create migrations from the same parent migration.
python manage.py showmigrationspython manage.py makemigrations --mergepython manage.py migrateAfter creating a merge migration:
RunPythonSchema migrations change structure; data migrations change existing row values.
from django.db import migrations
def fill_order_status(apps, schema_editor): Order = apps.get_model("orders", "Order") Order.objects.filter(status__isnull=True).update(status="pending")
def reverse_fill_order_status(apps, schema_editor): Order = apps.get_model("orders", "Order") Order.objects.filter(status="pending").update(status=None)
class Migration(migrations.Migration): dependencies = [ ("orders", "0007_add_status"), ]
operations = [ migrations.RunPython(fill_order_status, reverse_fill_order_status), ]Use custom SQL only when Django migration operations cannot express what you need. Examples: advanced indexes, triggers, and vendor-specific DB features.
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [ ("store", "0004_auto_20240601_1234"), ]
operations = [ migrations.RunSQL( """ INSERT INTO store_collection (title) VALUES ('collection1') """, """ DELETE FROM store_collection WHERE title = 'collection1' """, ) ]Rollback is part of production safety. You should know exactly how to move backward when a release fails.
python manage.py migrate app_name 0003python manage.py migrate app_name zerosqlmigrate.Golden Rule
Small, reversible, well-tested migrations are better than one large risky migration.
Team Rule
Migration files are source code. Review them in pull requests like any critical code.
Beginner Rule
If unsure, test on a copy of real data first. Never guess in production.