Django Model Definitions: How They Differ From the Generated Database Schema

Models are the way we tell Django what we want our database schema to look like, and are the things through which we interact with rows, or instances, in our database.

One of the benefits of using a framework like Django is that it provides functionality making it easier to create and interact with our data than if we had to do so directly with the database and generate and validate our own SQL statements. This means that there are things that will look different in the database than they do in our code — this is okay! But it’s also interesting to look at some of them to better understand where Postgres (in this example, you may be using a different database engine) and Django diverge, and which is driving what behavior.

Let’s build an example model as we go through this, so that at the end we can take a comprehensive look at the differences between our model definition, the migration file, and the database schema.

Primary Keys

By default, models are given an field as the primary key without needing to define it at all (so nothing to add to our example model definition we’ll be using throughout this post!) — documented here.

Foreign Keys

To our example class, we’ll add a foreign key to the :

However, what’s actually stored in the database for foreign keys is the value of the related object’s primary key (typically the field, as mentioned above). For this reason, the name of the field in the database will be slightly different than our model definition: the field will be . Both and are available as properties on a model instance.

Optimization note: there’s a cool implication of this! Because of Django magic related to defining something as a (instead of just an integer, which is what’s actually stored as the database), when we call on an instance of a blog post, it will retrieve that instance from the database as well. If all we need is the field, calling can improve performance over calling

The database field will be of type and will by default have an index, even though we did not define the way we would for non-foreign key fields on which we wanted an index.

The bit of a is actually handled at the database level, not Django - even though this is just an integer field, trying to save an integer in it for which there is no corresponding row in the related table, will fail.


A is Django’s way of creating the many-to-many relationship without you needing to define a through table (though you can provide a argument if you have a table defined yourself). If you inspect your database schema after creating one of these, you will see that a join table is created for which you have no corresponding model.

To demonstrate this, we’ll add a table and create a field on our table so we can see what this looks like at the end:


I wrote more about these fields and their use for tracking when model instances were created and updated here, but it’s worth noting that they’re used strictly by Django. Django ensures that the fields are added or updated on each save, but in the database they’re just fields. Let’s add them to our model definition so we can see what things look like at the end.



Both of these are options you can pass into a field as you’re initializing them, and neither has any impact on the schema definition of the field. Django uses them both when validating form submissions, and when trying to save data to the database via the ORM, and will only allow you to save values that comply.

However, because these are not enforced at the database level, you could definitely write an SQL statement that saves invalid data. This is one of the (many!) reasons always using the ORM to access data is advisable.

Our above is already defined with , so we’ll just add a new field here to demonstrate :

Custom field types

Examples of custom field types include , , and , though it’s definitely worth perusing all the different field types Django offers ( documentation here or source code here — looking at the source code can help clarify which base field type each of these inherits from, a hint at how it will actually be defined in the database).

All of these fields inherit from one of the basic field types (usually a ) but have a default validator on them, giving you one less thing to worry about when validating form submissions or API serializers, and which handle the work for you of deciding how to define the validator as you’re adding the field. Let’s add a to our example model:


Uniqueness is interesting: when a single field on a model needs to be unique, we define that by just applying to the field definition, but at the database level, it doesn’t touch the field — instead Postgres creates an index to enforce uniqueness. The Django docs mention this in reference to the fact that adding means it’s unnecessary to add , since we definitionally get an index with the uniqueness constraint.

This means that unlike with things like and , even if you’re accessing the database directly with SQL, this constraint will be enforced.

Let’s add a unique field to our model:

Help Text

Adding to a field definition doesn’t have any impact on the database at all. Django uses it when when drawing model forms. Perhaps unexpectedly though, if you change it, running will generate a migration file that will be a no-op when it’s run. Let’s add help text to one of the fields we added above.

Bringing it all Together

Model Definition

Ok, pulling together each of the fields we discussed above, this is what our model definitions look like now:

Migration File

Given the model definitions above, when we run , here’s are the operations that are added to the migration file:

You can see here some of the things we talked about, such as how both of our new models have an field, which is shown here as an . For the most part though, things look an awful lot like what we defined in our file. That’s because migration files are solidly in the Django realm, instead of in the database realm — Django translates them into SQL under the hood.

Note: if you want to see the SQL that will be run when you run your migration file, you can use for that.

Things get more interesting when we look at what happens in the database itself. Let’s do that now.

Database Schema

First, let’s look at the tables we have. When we go into our Postgres shell and run , here’s what we see:

One of the primary things that you’ll notice here is there’s a table in the database that we didn’t define at all in our models! That’s the table, which is the join table between and . It has its own primary key, in addition to foreign keys to those two tables.

Ok, let’s look at the database schema for the table, which is where most of the interesting things are. We can do that by running . Here’s what we see when we do that:

Let’s do a quick rundown of the things we notice here:

  • We have an field, even though we didn’t define one
  • Our fields are just fields, but they have on them, which is how cascade deletion is handled
  • No sign of , , or
  • Also no sign of a specific field type, though that field was given a of , even though we didn’t define that
  • Our field (which had the choices) is just an field, since that’s the type of data all of the options were
  • Our and fields are just your typical fields, with no references to being populated automatically
  • I’m not going to talk about all of the indices that are defined there, as that’s out of scope for this post, but the notable one is that our field has a index on it.

There are plenty more interesting bits to the way Django and databases communicate with each other, and how things are translated from one to the other, but hopefully this was a useful primer. ✨

Senior Software Engineer |

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store