Django database configuration
last modified November 13, 2026 Python 3.15
In this article we configure the database of a Django project. We start with the default SQLite database and its options, and then connect the same project to PostgreSQL and MariaDB running in containers. We look at persistent connections, health checks, and connection pooling, and finally split a project into two databases with a database router. The examples use Django 6.1 and Python 3.15, PostgreSQL 18, and MariaDB 12.
The DATABASES setting
Django reads the database connections from the DATABASES setting, a
dictionary that maps an alias to a configuration. Every project needs the alias
default; other aliases are used when a project talks to several databases. A
configuration has an ENGINE, which is the Python path of the backend, a
NAME, the connection parameters such as USER, PASSWORD,
HOST and PORT, and optional keys such as OPTIONS,
CONN_MAX_AGE and TEST. Django includes backends for SQLite, PostgreSQL, MySQL
(which also serves MariaDB), and Oracle. Except for SQLite, whose driver is part of Python, a backend needs
a driver package.
django.db.backends.sqlite3, no driver to installdjango.db.backends.postgresql, driverpsycopg(version 3) orpsycopg2django.db.backends.mysql, drivermysqlclient, for MariaDB and MySQLdjango.db.backends.oracle, driveroracledb
Databases other than these are available through third-party backends, which we do not cover.
The example project
The project is created with uv; the steps are explained in
Django introduction. The commands below create a project
directory, install Django, generate the project with the configuration package
config, start the app shop, and create the folder demos for demo scripts.
$ mkdir database-configuration $ cd database-configuration $ uv init --bare $ uv add django $ uv run django-admin startproject config . $ uv run python manage.py startapp shop $ mkdir demos
The trailing dot in startproject config . creates manage.py and the
config package in the current directory instead of one level deeper. A new app is
ignored by Django until it is listed in INSTALLED_APPS, so we add
the app to the list in config/settings.py; the other
entries are generated by startproject.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'shop',
]
The listings in this article are of two kinds. A listing with a solid blue border and the label
Project file is a file of the application: models, views, templates, tests, and
settings. A listing with a dashed amber border and the label Demo script is a small
helper in the demos folder; it sets up Django, runs a few lines of code, and prints
the results, so that we can watch the behaviour. Demo scripts are not part of the application;
we run one from the project directory with uv run python -m demos.name.
The package config holds the settings and the app shop has one
model. To compare the backends, we keep the settings of each in a separate module,
such as config/settings_postgres.py, that imports the base settings and replaces
DATABASES. We select a module with the DJANGO_SETTINGS_MODULE environment variable or the
--settings option; in a real project, a single settings file is enough.
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=8, decimal_places=2)
def __str__(self):
return self.name
A product has a name and a price. We create the migration with makemigrations; the migration file does not
depend on the database, and the same file is applied to every backend (see
Django migrations).
$ uv run python manage.py makemigrations shop
Migrations for 'shop':
shop/migrations/0001_initial.py
+ Create model Product
The default SQLite database
The settings file generated by startproject configures SQLite. The database is a
single file in the project directory, and the value of NAME is its path. SQLite
needs no server and no setup, which makes it ideal for learning, development, tests, and small
sites with few concurrent writers.
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
The migrate command creates the file and the tables. The script
demos/info.py, listed next, prints the vendor of the backend, the name of the database, and the
version of the server, using the object django.db.connection, which represents the connection of
the default database.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.db import connection
settings = connection.settings_dict
print('vendor:', connection.vendor)
print('engine:', settings['ENGINE'])
print('name: ', settings['NAME'])
sql = 'SELECT sqlite_version()' if connection.vendor == 'sqlite' else 'SELECT version()'
with connection.cursor() as cursor:
cursor.execute(sql)
print('server:', cursor.fetchone()[0])
The vendor attribute is a short name of the backend (sqlite,
postgresql, mysql, oracle), which is useful for code that must behave differently
for each database. The script chooses the SQL that returns the version, because SQLite has
sqlite_version() and the other databases have version().
$ uv run python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, sessions, shop Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying sessions.0001_initial... OK Applying shop.0001_initial... OK $ uv run python -m demos.info vendor: sqlite engine: django.db.backends.sqlite3 name: ./db.sqlite3 server: 3.53.1
SQLite options
SQLite is configured with the keys of the OPTIONS dictionary. Since Django 5.1,
the init_command option runs SQL statements for each new connection, and the
transaction_mode option selects the type of transaction that atomic() begins; the
timeout option is the number of seconds that a connection waits for a lock held by another
connection. A write-heavy site can improve concurrency with the write-ahead log mode and relaxed
synchronization, and a transaction mode of IMMEDIATE makes a transaction take the write lock at
the beginning, avoiding the error "database is locked" in the middle of a transaction.
from .settings import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'OPTIONS': {
'timeout': 20,
'transaction_mode': 'IMMEDIATE',
'init_command': 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;',
},
}
}
The script reads the options back and asks SQLite for the three values with PRAGMA
statements.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.db import connection
print('OPTIONS:', connection.settings_dict['OPTIONS'])
with connection.cursor() as cursor:
for pragma in ('journal_mode', 'synchronous', 'busy_timeout'):
cursor.execute(f'PRAGMA {pragma}')
print(pragma, '=', cursor.fetchone()[0])
We run the script twice: with the default settings, and with the settings module that sets the options.
By default, the journal mode is delete, synchronous is 2 (full), and the busy timeout is 5000
milliseconds, which is the default 5 seconds of the timeout option. The second run shows the write-ahead log
mode, synchronous 1 (normal), and the timeout of 20 seconds.
$ uv run python -m demos.sqlite
OPTIONS: {}
journal_mode = delete
synchronous = 2
busy_timeout = 5000
$ DJANGO_SETTINGS_MODULE=config.settings_sqlite uv run python -m demos.sqlite
OPTIONS: {'timeout': 20, 'transaction_mode': 'IMMEDIATE', 'init_command': 'PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;'}
journal_mode = wal
synchronous = 1
busy_timeout = 20000
Running PostgreSQL and MariaDB in containers
For the next sections we need a PostgreSQL and a MariaDB server. The easiest way is to run
them as containers. The commands create a database named shop and a user with the
password secret in each of them; the ports are mapped to 5433 and 3307 so
that they do not collide with servers installed on the machine. Any other installation of the databases will do as well.
$ docker run -d --rm --name djpg -e POSTGRES_USER=shop -e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=shop -p 127.0.0.1:5433:5432 postgres:18
$ docker run -d --rm --name djmaria -e MARIADB_ROOT_PASSWORD=rootsecret -e MARIADB_USER=shop \
-e MARIADB_PASSWORD=secret -e MARIADB_DATABASE=shop -p 127.0.0.1:3307:3306 mariadb:12
$ docker exec djmaria mariadb -uroot -prootsecret \
-e "GRANT ALL ON \`test\_%\`.* TO 'shop'@'%'"
The third command lets the MariaDB user create the test databases, which we need at the end of
the article. The containers are removed when they stop (--rm), and so is their data;
this is what we want for experiments. The passwords are throwaway values; a real project must never keep
real credentials in the settings that go to version control.
PostgreSQL
PostgreSQL is the database that most Django projects use in production. The backend
needs the psycopg package. The extra binary installs a precompiled
libpq, so no compiler or PostgreSQL headers are needed, and the extra pool
installs the connection pool that we use later.
$ uv add "psycopg[binary,pool]"
Next comes the configuration. The values come from environment variables, with defaults for our local container, so that the same settings work on a developer machine and on a server where the variables are set by the deployment.
import os
from .settings import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME', 'shop'),
'USER': os.environ.get('DB_USER', 'shop'),
'PASSWORD': os.environ.get('DB_PASSWORD', 'secret'),
'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
'PORT': os.environ.get('DB_PORT', '5433'),
}
}
The engine is django.db.backends.postgresql. The NAME is the
database, and the database and the user must already exist; Django creates tables, not
databases. The first run prints the information about the connection. Because the container
contains an empty database, the migrate shop command applies only the migrations
of our app and, to keep the output short, we do not run the full migrate. The
last command lists the tables in PostgreSQL itself.
$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.info
vendor: postgresql
engine: django.db.backends.postgresql
name: shop
server: PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
$ uv run python manage.py migrate shop --settings=config.settings_postgres
Operations to perform:
Apply all migrations: shop
Running migrations:
Applying shop.0001_initial... OK
$ docker exec djpg psql -U shop -c "\dt"
List of tables
Schema | Name | Type | Owner
--------+-------------------+-------+-------
public | django_migrations | table | shop
public | shop_product | table | shop
(2 rows)
MariaDB and MySQL
The mysql backend works with MariaDB and MySQL, and the driver is
mysqlclient. It is a C extension, which is built when it is installed, so it needs a compiler
and the client library with the headers; on Debian-based systems they come in the
libmariadb-dev package. If the build cannot find the library, set the
MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS environment variables.
$ uv add mysqlclient
The configuration has the same shape as that for PostgreSQL. The charset option sets the connection
character set; with utf8mb4 the full Unicode, including emoji, can be stored.
import os
from .settings import *
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DB_NAME', 'shop'),
'USER': os.environ.get('DB_USER', 'shop'),
'PASSWORD': os.environ.get('DB_PASSWORD', 'secret'),
'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
'PORT': os.environ.get('DB_PORT', '3307'),
'OPTIONS': {'charset': 'utf8mb4'},
}
}
The script demos/info.py prints the same information. Django reports the vendor as
mysql for both servers; the version string shows that the server is MariaDB. We apply the migrations and list the
tables with the mariadb client inside the container.
$ DJANGO_SETTINGS_MODULE=config.settings_mariadb uv run python -m demos.info vendor: mysql engine: django.db.backends.mysql name: shop server: 12.3.3-MariaDB-ubu2404 $ uv run python manage.py migrate shop --settings=config.settings_mariadb Operations to perform: Apply all migrations: shop Running migrations: Applying shop.0001_initial... OK $ docker exec djmaria mariadb -ushop -psecret shop -e "SHOW TABLES" Tables_in_shop django_migrations shop_product
Persistent connections
By default, Django opens a database connection at the beginning of a request and closes it at the end,
which is the setting CONN_MAX_AGE = 0. Opening a connection costs time, mainly with a remote
server and TLS. With CONN_MAX_AGE set to a number of seconds, a worker thread keeps the
connection and reuses it for the following requests until it is older than that value;
None means that the connection lives forever. At the beginning and at the end of each request,
Django calls close_old_connections(), which closes the connections that are too old or broken. Each
thread has its own connection, so the number of open connections can reach the number of the worker threads
of all the server processes, which must stay below the limit of the server (max_connections in PostgreSQL).
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.db import close_old_connections, connection
def backend_pid():
with connection.cursor() as cursor:
cursor.execute('SELECT pg_backend_pid()')
return cursor.fetchone()[0]
for age in (0, 60):
connection.close()
connection.settings_dict['CONN_MAX_AGE'] = age
first = backend_pid()
close_old_connections()
second = backend_pid()
print(f'CONN_MAX_AGE={age}: same connection after the "request": {first == second}')
The script simulates two requests by calling close_old_connections() between two
queries, as Django does when a request finishes. The function pg_backend_pid() returns the id of the server
process that serves the connection, which tells us whether it is the same connection. With the age 0 the
second query runs on a new connection; with the age 60 it reuses the first one.
$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.conn CONN_MAX_AGE=0: same connection after the "request": False CONN_MAX_AGE=60: same connection after the "request": True
Connection health checks
A persistent connection can be closed by the server, by a restart, a failover, or a
firewall that drops idle connections. The next request that uses it fails with an
OperationalError. The setting CONN_HEALTH_CHECKS = True makes Django check
a reused connection before the first query of a request and open a new one if the check fails.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
import psycopg
from django.db import OperationalError, close_old_connections, connection
def backend_pid():
with connection.cursor() as cursor:
cursor.execute('SELECT pg_backend_pid()')
return cursor.fetchone()[0]
def terminate(pid):
s = connection.settings_dict
with psycopg.connect(dbname=s['NAME'], user=s['USER'], password=s['PASSWORD'],
host=s['HOST'], port=s['PORT'], autocommit=True) as other:
other.execute('SELECT pg_terminate_backend(%s)', [pid])
for checks in (False, True):
connection.close()
connection.settings_dict.update(CONN_MAX_AGE=60, CONN_HEALTH_CHECKS=checks)
pid = backend_pid()
terminate(pid)
close_old_connections()
try:
print(f'CONN_HEALTH_CHECKS={checks}: new connection, query ok:', backend_pid() != pid)
except OperationalError as error:
print(f'CONN_HEALTH_CHECKS={checks}: {type(error).__name__}:', str(error).strip())
The script opens a persistent connection, terminates its server process from a second
connection with pg_terminate_backend(), and then runs a query, once with the health checks off
and once with them on. Without the check the query fails on the dead connection; with the check Django
notices that the connection is unusable, opens a new one, and the query succeeds.
$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.health CONN_HEALTH_CHECKS=False: OperationalError: terminating connection due to administrator command CONN_HEALTH_CHECKS=True: new connection, query ok: True
Connection pooling
Since Django 5.1 the PostgreSQL backend can use a connection pool, which is available
with psycopg 3 and the pool extra. The pool is enabled with the pool key of
OPTIONS; True accepts the defaults, and a dictionary passes the arguments to the
ConnectionPool of psycopg_pool. A connection that a thread closes returns to the pool and is reused by another thread,
so the number of connections to the server is limited by max_size, no matter how many threads
there are. A pool does not work with persistent connections: CONN_MAX_AGE must be left at 0, otherwise
Django raises ImproperlyConfigured.
from .settings_postgres import *
DATABASES['default']['OPTIONS'] = {
'pool': {'min_size': 2, 'max_size': 4, 'timeout': 10},
}
The pool keeps at least two connections and opens at most four; a thread that needs a connection when all four are in use waits up to ten seconds. The next script runs forty queries in eight threads, and each thread closes its connection after the query, as the worker threads of a server do at the end of a request.
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from concurrent.futures import ThreadPoolExecutor
from django.db import connection
def work(_):
with connection.cursor() as cursor:
cursor.execute('SELECT pg_backend_pid()')
pid = cursor.fetchone()[0]
connection.close()
return pid
with ThreadPoolExecutor(max_workers=8) as executor:
pids = list(executor.map(work, range(40)))
print('queries:', len(pids))
print('server connections used:', len(set(pids)))
print('pool:', connection.settings_dict['OPTIONS'].get('pool'))
We run the script with the plain PostgreSQL settings and with the pool. Without the pool every query opens a new connection and the server sees forty of them. With the pool, the same work goes through a few connections, never more than four; the exact number depends on the timing.
$ DJANGO_SETTINGS_MODULE=config.settings_postgres uv run python -m demos.threads
queries: 40
server connections used: 40
pool: None
$ DJANGO_SETTINGS_MODULE=config.settings_pool uv run python -m demos.threads
queries: 40
server connections used: 3
pool: {'min_size': 2, 'max_size': 4, 'timeout': 10}
Multiple databases
A project can use several databases. We add another alias to DATABASES; here
the second database is named analytics and lives on the same PostgreSQL server. We create a second app,
analytics, with a model PageView that records page visits; we want its table in the analytics
database, and the rest of the project in shop. The first command creates the app, the second creates the
database on the server with createdb.
$ uv run python manage.py startapp analytics $ docker exec djpg createdb -U shop analytics
The model stores the visited path and the time of the visit; the field auto_now_add fills the time when
the row is created.
from django.db import models
class PageView(models.Model):
path = models.CharField(max_length=200)
viewed_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.path
The settings module reuses the PostgreSQL connection parameters with a different name, installs the app, and registers a router.
from .settings_postgres import *
INSTALLED_APPS = INSTALLED_APPS + ['analytics']
DATABASES['analytics'] = {**DATABASES['default'], 'NAME': 'analytics'}
DATABASE_ROUTERS = ['config.routers.AnalyticsRouter']
A database router is a class that tells Django which database to use. The methods
db_for_read() and db_for_write() return an alias for a model, or
None to let the next router or the default decide. The method
allow_migrate() decides whether the migrations of an app run on a given database, and
allow_relation() whether a relation between two objects is permitted.
class AnalyticsRouter:
"""Send the analytics app to the analytics database."""
def db_for_read(self, model, **hints):
if model._meta.app_label == 'analytics':
return 'analytics'
return None
def db_for_write(self, model, **hints):
if model._meta.app_label == 'analytics':
return 'analytics'
return None
def allow_relation(self, obj1, obj2, **hints):
return None
def allow_migrate(self, db, app_label, model_name=None, **hints):
if app_label == 'analytics':
return db == 'analytics'
return db == 'default'
Our router sends the models of the analytics app to the analytics
database. It allows the migrations of that app only there, and everything else only on default.
allow_relation() returns None, which means "no opinion": by default Django permits relations
only between objects from the same database. Foreign keys between models in different databases are not
supported, because a database cannot enforce a constraint across servers, so we keep the models
that refer to each other together.
Migrating multiple databases
The makemigrations command creates the migration of the new app, which does not depend on
the database. The migrate command works on one database at a time and applies to the
default database unless we pass the --database option. The router
controls which operations run. The first command applies the app analytics on the
default database: the router forbids it, so Django records the migration as applied but creates no table. The second command applies it
on the analytics database, where the table is created. A real project runs plain
migrate once for each alias; we name the app to keep the output short. The last two commands list
the tables of both databases.
$ uv run python manage.py makemigrations analytics
Migrations for 'analytics':
analytics/migrations/0001_initial.py
+ Create model PageView
$ uv run python manage.py migrate analytics
Operations to perform:
Apply all migrations: analytics
Running migrations:
Applying analytics.0001_initial... OK
$ uv run python manage.py migrate analytics --database=analytics
Operations to perform:
Apply all migrations: analytics
Running migrations:
Applying analytics.0001_initial... OK
$ docker exec djpg psql -U shop -d shop -c "\dt analytics*"
Did not find any tables named "analytics*".
$ docker exec djpg psql -U shop -d analytics -c "\dt"
List of tables
Schema | Name | Type | Owner
--------+--------------------+-------+-------
public | analytics_pageview | table | shop
public | django_migrations | table | shop
(2 rows)
Using multiple databases
With the router in place, the code does not change: the QuerySet manager of a model has an
attribute db with the alias that the router chose. To override the router, we call
using() on a QuerySet or save(using=...) on an instance. The
connection of an alias is available as connections['alias'].
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')
django.setup()
from django.db import connections
from django.db.utils import DatabaseError
from analytics.models import PageView
from shop.models import Product
Product.objects.all().delete()
PageView.objects.all().delete()
Product.objects.create(name='Lamp', price='19.90')
for path in ('/', '/lamp/', '/'):
PageView.objects.create(path=path)
print('products:', Product.objects.count(), 'from', Product.objects.db)
print('page views:', PageView.objects.count(), 'from', PageView.objects.db)
for alias in ('default', 'analytics'):
connection = connections[alias]
tables = [t for t in connection.introspection.table_names()
if t.startswith(('shop_', 'analytics_'))]
print(f'{alias} ({connection.settings_dict["NAME"]}):', tables)
try:
PageView.objects.using('default').count()
except DatabaseError as error:
print(type(error).__name__, str(error).splitlines()[0])
The script saves a product and three page views with the ordinary calls. The router sends the products
to default and the page views to analytics, which the db attribute confirms.
The introspection lists the application tables in each database: each of them holds only its own table. Forcing
the model PageView to the default database with using() fails, because the table
does not exist there. Transactions are per database as well: transaction.atomic(using='analytics')
covers only that database, and there is no transaction that spans both.
$ DJANGO_SETTINGS_MODULE=config.settings_multi uv run python -m demos.multi products: 1 from default page views: 3 from analytics default (shop): ['shop_product'] analytics (analytics): ['analytics_pageview'] ProgrammingError relation "analytics_pageview" does not exist
Test databases
The test runner never touches the databases that we configured. For each database it creates a
new one with the name prefixed with test_, runs the migrations in it, and destroys it at the end.
The TEST dictionary of a configuration can set another name, a character set, or a mirror
of another alias. The user must have the privilege to create databases; in the MariaDB container the user
shop owns only the database shop, so we granted it the privileges on the
databases whose names begin with test_. A test case may use only the databases that it lists in
the databases attribute; by default, that is default, which keeps the tests fast.
from django.test import TestCase
from .models import Product
class ProductTests(TestCase):
def test_create_product(self):
Product.objects.create(name='Lamp', price='19.90')
self.assertEqual(Product.objects.count(), 1)
The test of the shop app creates a product and needs only the default database.
from django.test import TestCase
from .models import PageView
class RoutingTests(TestCase):
databases = {'default', 'analytics'}
def test_page_views_use_the_analytics_database(self):
PageView.objects.create(path='/')
self.assertEqual(PageView.objects.db, 'analytics')
self.assertEqual(PageView.objects.using('analytics').count(), 1)
The test RoutingTests declares both aliases, creates a page view, and checks that it went
to the analytics database. We run the tests with the multi-database settings, which creates two test databases, and then
the tests of the shop app on MariaDB.
$ uv run python manage.py test --settings=config.settings_multi Found 2 test(s). Creating test database for alias 'default'... Creating test database for alias 'analytics'... System check identified no issues (0 silenced). .. ---------------------------------------------------------------------- Ran 2 tests in 0.013s OK Destroying test database for alias 'default'... Destroying test database for alias 'analytics'... $ uv run python manage.py test shop --settings=config.settings_mariadb Found 1 test(s). Creating test database for alias 'default'... System check identified no issues (0 silenced). . ---------------------------------------------------------------------- Ran 1 test in 0.002s OK Destroying test database for alias 'default'...
Source
Django documentation: Databases
Django documentation: Multiple databases
Django documentation: DATABASES setting
In this article we have configured Django for SQLite, PostgreSQL, and MariaDB, tuned
SQLite with OPTIONS, used persistent connections, health checks, and a connection pool, and split a project
into two databases with a router. Related articles are Django models,
Django migrations, and
Django raw SQL and transactions.
Author
List all Django tutorials.