Migrate a Django project from MySQL to PostgreSQL
PostgreSQL is Django's reference database — features like JSONField,
ArrayField and full-text search are best supported there. Switching is a few
lines of config plus a careful data move. Here is the order that avoids surprises.
1. Install the PostgreSQL adapter
Django talks to PostgreSQL through psycopg (version 3). Install it into your
virtualenv:
pip install "psycopg[binary]"
The [binary] extra pulls a prebuilt wheel so you do not need PostgreSQL's
development headers locally. On older projects you may still see psycopg2-binary;
both work with Django 4.2+, but new code should prefer psycopg 3.
2. Change DATABASES in settings.py
Swap the MySQL engine for the PostgreSQL backend:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "myapp",
"USER": "myapp",
"PASSWORD": "secret",
"HOST": "127.0.0.1",
"PORT": "5432",
}
}
The MySQL backend (django.db.backends.mysql) often carried an
OPTIONS entry forcing utf8mb4 and STRICT_TRANS_TABLES.
Delete it — PostgreSQL is UTF-8 throughout and strict by default, so those options have no
equivalent and will error if left in.
3. Know what changes under the hood
Django abstracts most differences, but a few leak through:
- BooleanField. MySQL stored it as
tinyint(1); PostgreSQL uses a realboolean. Django handles this for you, but raw SQL or.extra()/RawSQLfilters comparing the column to1will break. - Case-sensitive lookups. MySQL's default collation makes
filter(name="foo")case-insensitive; PostgreSQL is case-sensitive. Use__iexactand__icontainswhere you relied on the old behaviour. - AutoField sequences. PostgreSQL backs auto IDs with sequences. If you import rows with explicit primary keys, the sequence is not advanced and the next insert collides — you must reset it (covered in step 5).
- Stricter SQL. PostgreSQL rejects ambiguous
GROUP BYand invalid date literals (e.g.0000-00-00) that MySQL silently accepted. Clean these before importing.
4. Move the data
Small / fixture-friendly projects. Django's own serializer is database-agnostic and the simplest path. Dump from MySQL, point settings at the empty PostgreSQL database, migrate, then load:
python manage.py dumpdata --natural-foreign --natural-primary \
-e contenttypes -e auth.Permission > data.json
# switch settings.py to PostgreSQL, then:
python manage.py migrate
python manage.py loaddata data.json
Excluding contenttypes and auth.Permission avoids the classic
"duplicate key" error, because migrate recreates those rows itself.
Large / production databases. dumpdata loads every row into memory
and is slow at scale. Instead, take a real SQL dump and convert the dialect — type mappings,
AUTO_INCREMENT → sequences, backtick quoting and all:
mysqldump --single-transaction --no-tablespaces \
-u root -p myapp > myapp.sql
Convert myapp.sql to PostgreSQL, then import with
psql -d myapp -f converted.sql. Run python manage.py migrate --fake
afterward so Django records the schema as already applied.
5. Reset the sequences
After importing rows with explicit IDs, realign every table's sequence so new inserts get fresh IDs. Django prints the exact SQL for you:
python manage.py sqlsequencereset myapp | python manage.py dbshell 6. Verify
Run python manage.py check, then your test suite against PostgreSQL. Compare row
counts on key models (Model.objects.count()) with the MySQL originals before pointing
production at the new database.
Convert your dump in seconds — turn your MySQL export into PostgreSQL now →