Лучшая база данных для Django: PostgreSQL, MySQL или SQLite

Обновлено:

Короткий ответ: PostgreSQL для продакшена, SQLite для локальной разработки и тестирования. MySQL или MariaDB — если под них уже есть инфраструктура и опыт. Ниже — почему так и как настроить каждый вариант.


Какие базы данных поддерживает Django

Django «из коробки» поддерживает пять СУБД:

  • PostgreSQL — рекомендуется как основной выбор
  • MySQL и MariaDB — два отдельных встроенных бэкенда, широко используются
  • SQLite — default при startproject, для разработки и тестирования
  • Oracle — для корпоративных legacy-систем

Через сторонние пакеты:

  • Microsoft SQL Server — через mssql-django
  • MongoDB — через djongo или mongoengine (неофициально, лучше не надо)
  • CockroachDB — совместим с PostgreSQL API

SQLite: с чего начинает каждый Django-проект

Когда вы создаёте Django-проект через django-admin startproject, в settings.py уже настроен SQLite:

# settings.py — Django default
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

Никаких зависимостей, никакой установки — просто файл db.sqlite3 в корне проекта.

Когда SQLite — правильный выбор

  • Локальная разработка: быстрый старт, не нужен Docker или отдельный сервис
  • Юнит-тесты: SQLite в памяти быстро создаётся и не требует сервера (но тесты на другой СУБД, чем в продакшене, могут пропустить ошибки)
  • Маленькие проекты: личный блог, портфолио, внутренний инструмент с 1-2 пользователями
  • CI/CD pipeline: тесты без внешних зависимостей
# Быстрый in-memory SQLite для тестов
# settings_test.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': ':memory:',
    }
}

Ограничения SQLite в Django

С SQLite недоступно:

  • всё из django.contrib.postgres: ArrayField, HStoreField, range-поля, PostgreSQL-поиск (JSONField при этом работает на всех бэкендах начиная с Django 3.1);
  • параллельная запись: в каждый момент пишет только один процесс, WAL-режим лишь позволяет читать во время записи;
  • часть ALTER TABLE — Django обходит это пересозданием таблицы при миграции, что на больших таблицах долго.

Для небольших проектов SQLite в продакшене — рабочий вариант. С Django 5.1 PRAGMA и режим транзакций можно задать прямо в настройках:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
        'OPTIONS': {
            'init_command': 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA busy_timeout=5000;',
            'transaction_mode': 'IMMEDIATE',
        },
    }
}

Не забудьте про бэкап: файл db.sqlite3 нельзя просто копировать во время работы приложения — см. как сделать бэкап SQLite без повреждения.


PostgreSQL: стандарт для Django в продакшене

PostgreSQL — самый полно поддерживаемый Django бэкенд: модуль django.contrib.postgres добавляет поля, индексы, ограничения и поиск, которых нет для других СУБД.

Установка и настройка

# Django 4.2+: psycopg 3 — рекомендуемый драйвер для новых проектов
pip install "psycopg[binary]"

# или классический psycopg2
pip install psycopg2-binary  # бинарная сборка, удобно для разработки
pip install psycopg2         # сборка из исходников, нужны libpq-dev и компилятор
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'myapp_db',
        'USER': 'myapp_user',
        'PASSWORD': 'secure_password',
        'HOST': 'localhost',
        'PORT': '5432',
        'CONN_MAX_AGE': 60,  # держать соединение открытым до 60 с (это не пул)
        'OPTIONS': {
            'connect_timeout': 5,
        }
    }
}

Через переменную окружения (рекомендуется):

pip install dj-database-url
import dj_database_url

DATABASES = {
    'default': dj_database_url.config(
        default='postgresql://user:password@localhost:5432/myapp_db',
        conn_max_age=600
    )
}

PostgreSQL-специфичные возможности Django ORM

Именно ради этих фич выбирают PostgreSQL:

JSONField (нативный, с индексами)

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    attributes = models.JSONField(default=dict)  # Нативный JSONB в PostgreSQL

# Поиск по JSON-полю
Product.objects.filter(attributes__brand='Apple')
Product.objects.filter(attributes__ram__gte=16)
Product.objects.filter(attributes__tags__contains=['M3'])

ArrayField

from django.contrib.postgres.fields import ArrayField

class Article(models.Model):
    title = models.CharField(max_length=200)
    tags = ArrayField(models.CharField(max_length=50), default=list)

# Запросы
Article.objects.filter(tags__contains=['python'])
Article.objects.filter(tags__overlap=['django', 'postgresql'])
Article.objects.filter(tags__len__gte=3)

Full-text Search

from django.contrib.postgres.search import SearchVector, SearchQuery, SearchRank

# Полнотекстовый поиск с рангом
vector = SearchVector('title', weight='A') + SearchVector('content', weight='B')
query = SearchQuery('база данных')
results = Article.objects.annotate(
    rank=SearchRank(vector, query)
).filter(rank__gte=0.1).order_by('-rank')

HStoreField (ключ-значение)

from django.contrib.postgres.fields import HStoreField

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    settings = HStoreField(default=dict)

profile.settings['theme'] = 'dark'
profile.save()

Window Functions

from django.db.models import Window, F
from django.db.models.functions import Rank

# Ранжирование пользователей по баллам
User.objects.annotate(
    rank=Window(
        expression=Rank(),
        order_by=F('score').desc()
    )
)

Constraint с исключениями (ExclusionConstraint)

from django.contrib.postgres.constraints import ExclusionConstraint
from django.contrib.postgres.fields import DateTimeRangeField, RangeOperators

# Требует расширения btree_gist (BtreeGistExtension в миграции) для сравнения room через GiST

# Запрет пересечения временных слотов (для бронирования)
class Booking(models.Model):
    room = models.ForeignKey(Room, on_delete=models.CASCADE)
    time_range = DateTimeRangeField()

    class Meta:
        constraints = [
            ExclusionConstraint(
                name='no_overlapping_bookings',
                expressions=[
                    ('room', RangeOperators.EQUAL),
                    ('time_range', RangeOperators.OVERLAPS),
                ]
            )
        ]

MySQL / MariaDB: для legacy и специфических требований

Настройка

pip install mysqlclient
# settings.py
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'myapp_db',
        'USER': 'myapp_user',
        'PASSWORD': 'password',
        'HOST': 'localhost',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8mb4',
            'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
        }
    }
}

Важные нюансы MySQL с Django

Всегда используйте utf8mb4, не utf8 — иначе нельзя хранить эмодзи:

CREATE DATABASE myapp_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

Включайте строгий режим (STRICT_TRANS_TABLES): без него MySQL молча обрезает слишком длинные значения, и Django выдаёт предупреждение mysql.W002.

Что не работает в MySQL (по сравнению с PostgreSQL)

  • ArrayField, HStoreField — нет поддержки
  • django.contrib.postgres.search — нет; полнотекстовый поиск MySQL доступен только через raw SQL
  • Оконные функции есть с MySQL 8.0 и MariaDB 10.2, в старых версиях — нет
  • JSONField поддерживается, но без JSONB-индексов, как в PostgreSQL
  • Ограничение длины ключа индекса: 3072 байта в InnoDB с форматом строк DYNAMIC, 767 байт в старых конфигурациях — длинные CharField(unique=True) в utf8mb4 могут не влезть

Сравнительная таблица

| Критерий | PostgreSQL | MySQL/MariaDB | SQLite | |---|---|---|---| | Продакшен | Да | Да | Только малые проекты | | Django-фичи | Полный набор | Частичный | Минимальный | | JSONField | JSONB с GIN-индексами | JSON | Да (через JSON1, хранится как текст) | | Full-text Search в ORM | Да (contrib.postgres.search) | Нет, только raw SQL | Нет (FTS5 — только raw SQL) | | Параллельные записи | MVCC (отлично) | Хорошо | Ограниченно | | Транзакции | ACID | ACID | ACID | | Масштабирование чтения | Реплики, расширения | Реплики, кластеры | Нет | | Лицензия | PostgreSQL (свободная) | GPL | Public Domain | | Российский хостинг | Везде | Везде | Везде | | Сложность настройки | Средняя | Средняя | Нет |


Workflow: SQLite локально, PostgreSQL в продакшене

Стандартная практика — разные настройки для разных окружений:

# settings/base.py
import os
import dj_database_url

# settings/development.py
from .base import *

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}

# settings/production.py
from .base import *

DATABASES = {
    'default': dj_database_url.config(
        default=os.environ.get('DATABASE_URL'),
        conn_max_age=600,
        ssl_require=True
    )
}

Но Django ORM гарантирует: тот же Python-код работает с обеими базами. Разница только в настройках.


Пул соединений: важно для продакшена

По умолчанию Django открывает новое соединение при каждом запросе. В продакшене используйте пул:

pgBouncer (внешний пул для PostgreSQL)

apt-get install pgbouncer
; /etc/pgbouncer/pgbouncer.ini
[databases]
myapp_db = host=127.0.0.1 port=5432 dbname=myapp_db

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
# Подключение через pgBouncer
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'HOST': 'localhost',
        'PORT': '6432',  # Порт pgBouncer, не PostgreSQL
        'NAME': 'myapp_db',
        # В режиме transaction серверные курсоры нужно отключить
        'DISABLE_SERVER_SIDE_CURSORS': True,
        # ...
    }
}

Встроенный пул Django 5.1+ (psycopg 3)

pip install "psycopg[binary,pool]"
# settings.py — пул через psycopg 3 (Django 5.1+); CONN_MAX_AGE при этом должен быть 0
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        # NAME, USER, PASSWORD, HOST, PORT — как выше
        'OPTIONS': {
            'pool': {
                'min_size': 2,
                'max_size': 20,
                'reconnect_timeout': 5,
            }
        }
    }
}

Индексы и оптимизация Django ORM

class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)  # Автоматический индекс
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    status = models.CharField(max_length=20, db_index=True)

    class Meta:
        indexes = [
            # Составной индекс
            models.Index(fields=['author', 'status'], name='author_status_idx'),
            # Частичный индекс (только опубликованные)
            models.Index(
                fields=['created_at'],
                name='published_articles_idx',
                condition=models.Q(status='published')
            ),
        ]

Проблема N+1 запросов

# ПЛОХО: N+1 запрос для каждой статьи
articles = Article.objects.all()
for article in articles:
    print(article.author.name)  # Отдельный SELECT для каждой статьи!

# ХОРОШО: select_related для ForeignKey
articles = Article.objects.select_related('author').all()

# ХОРОШО: prefetch_related для ManyToMany
articles = Article.objects.prefetch_related('tags').all()

Бэкапы базы для Django-проектов

Перед каждым деплоем с миграциями делайте дамп — откатить migrate с удалением столбца без копии нельзя.

# Дамп в custom-формате перед деплоем
pg_dump -h localhost -U myapp_user -Fc -f backup_before_deploy.dump myapp_db

# Восстановление в существующую базу с удалением текущих объектов
pg_restore -h localhost -U myapp_user -d myapp_db --clean --if-exists backup_before_deploy.dump

pg_restore читает только custom, directory и tar-форматы. Plain-дамп (.sql) восстанавливается через psql -v ON_ERROR_STOP=1 -f backup.sql. Все варианты восстановления — в статье как восстановить базу из бэкапа, а регулярные дампы по cron с выгрузкой в S3 — в статье бэкап PostgreSQL через pg_dump в S3.

Если хотите хранить версии вне сервера и видеть, что поменялось между ними, plain-дамп можно загружать в dbsend. Сервис не подключается к вашей базе — дамп делаете вы, CLI его загружает:

pg_dump --format=plain --no-owner "$DATABASE_URL" > /tmp/myapp.sql
dbsend backup /tmp/myapp.sql -d "$DBSEND_DATABASE_ID" -e postgresql_dump -l "deploy-$GIT_SHA"

Для db.sqlite3 достаточно dbsend backup db.sqlite3 -d "$DBSEND_DATABASE_ID" -e sqlite — CLI сам сделает консистентную копию работающей базы. Подробнее — на странице бэкапы PostgreSQL.


Рекомендации по типу проекта

Для прототипа и MVP:

Для SaaS-приложения:

  • PostgreSQL с pgBouncer, JSONB для гибких данных, pg_trgm для поиска

Для e-commerce:

  • PostgreSQL, обязательно транзакции, возможно TimescaleDB для аналитики

Для контентного сайта (много чтения, мало записи):

  • PostgreSQL + Redis для кэша, полнотекстовый поиск через PostgreSQL FTS или Elasticsearch

Для инди-проекта одного разработчика:

  • SQLite + Litestream или периодические версии в S3 — дёшево и достаточно для начала (сравнение — в статье Litestream vs dbsend)

Частые вопросы

Почему Django по умолчанию использует SQLite, если PostgreSQL лучше?

SQLite не требует настройки: python manage.py migrate работает сразу после startproject. PostgreSQL нужно установить, создать базу и пользователя. Django выбирает простоту старта.

Можно ли мигрировать с SQLite на PostgreSQL?

Да. Сделайте python manage.py dumpdata --natural-foreign --natural-primary -e contenttypes -e auth.Permission --indent 2 > data.json, переключите настройки на PostgreSQL, выполните migrate и затем loaddata data.json. На больших базах dumpdata медленный — тогда используют специализированные инструменты переноса. Перед переносом сохраните копию db.sqlite3.

MySQL или PostgreSQL для Django?

PostgreSQL, если нет конкретной причины выбрать MySQL (существующая инфраструктура, требования IT, опыт команды). С PostgreSQL доступен весь django.contrib.postgres.

Как ускорить тесты в Django?

Используйте --keepdb, чтобы не пересоздавать тестовую базу, и --parallel для параллельного запуска. SQLite в памяти даёт быстрые тесты, но если в продакшене PostgreSQL, запускайте тесты и на нём — хотя бы в CI.

Django работает с несколькими базами данных?

Да. В DATABASES можно описать несколько подключений, а маршрутизацию запросов задать через database routers. Это используют для реплик на чтение и разделения данных.

Как настроить read replica в Django?

Через DATABASE_ROUTERS: router-класс направляет чтение на реплику, а запись — на основной сервер. Учитывайте задержку репликации: сразу после записи читайте с основного сервера. Подробнее — в документации Django.

Нужен ли Redis вместе с PostgreSQL для Django?

Redis — это кэш, брокер для Celery и хранилище сессий; PostgreSQL — основное хранилище данных. Они не конкурируют. Типичный продакшен-стек: Django + PostgreSQL + Redis + Celery.