In Django multi-table inheritance, what does the implicit parent_link OneToOneField do, and what SQL does querying the child model run?
answer
- two tables, one identity
- the _ptr field
- primary key doubles as foreign key
- INNER JOIN on child reads
- reverse accessor raises DoesNotExist
basics
~20 sDjango adds a <parent>_ptr OneToOneField with parent_link=True and primary_key=True to the child, so the child row's primary key is also a foreign key to its parent row, and loading child objects joins the parent table.
solid answer
~40 sFor `class Van(Vehicle)`, Django adds `vehicle_ptr = OneToOneField(Vehicle, on_delete=CASCADE, parent_link=True, primary_key=True)`. The van table holds only van columns plus `vehicle_ptr_id`, which is both its primary key and a foreign key to `vehicle.id`. `Van.objects.filter(...)` selects from `van INNER JOIN vehicle`, so parent fields read as if local, and saving a van writes the vehicle row and then the van row in one transaction. From the parent side, `vehicle.van` follows the reverse one-to-one: another query unless you used `select_related("van")`, and `Van.DoesNotExist` if that vehicle is not a van. `Vehicle.objects.all()` returns plain `Vehicle` objects. You can declare the link yourself with `parent_link=True` to name it.
code
python · 15 linesfrom django.db import models
class Vehicle(models.Model):
plate = models.CharField(max_length=12, unique=True)
daily_rate = models.DecimalField(max_digits=7, decimal_places=2)
class Van(Vehicle):
# Declared explicitly only to rename the link; Django would otherwise
# create vehicle_ptr with the same arguments.
base_vehicle = models.OneToOneField(
Vehicle, on_delete=models.CASCADE, parent_link=True, primary_key=True
)
cargo_volume_m3 = models.DecimalField(max_digits=5, decimal_places=2)go deeper
Remember that multi-table inheritance means two tables and that Django links them with an automatic one-to-one field ending in _ptr.
Draw the van table with vehicle_ptr_id as primary key and foreign key, write the INNER JOIN, and explain vehicle.van versus Van.DoesNotExist.
Connect the link to production behaviour: per-level JOINs, no automatic downcast, related_name clashes on new relations, and when to rename the link explicitly.
Judge whether the identity split across tables is worth its JOIN tax for the team's query patterns, and document the navigation rules so services do not each reinvent downcasting.
## Two tables, one identity In **multi-table inheritance** both the parent and the child are concrete Django models, so each gets its own table. `class Van(Vehicle):` produces a `vehicle` table holding `plate`, `daily_rate` and the other parent columns, and a `van` table holding only the van-specific columns. A van is therefore **two rows** — one in each table — sharing a single identity. ## The implicit link field Django connects the two rows by adding a field to the child automatically: ```python vehicle_ptr = models.OneToOneField( Vehicle, on_delete=models.CASCADE, parent_link=True, primary_key=True, ) ``` Each argument matters: - **`OneToOneField`** — at most one van row per vehicle row; - **`primary_key=True`** — the van table has no `id` of its own; `vehicle_ptr_id` *is* its primary key, with the same value as `vehicle.id`; - **`parent_link=True`** — marks this field as the inheritance link rather than an ordinary relation, so Django uses it to read and save parent fields through the child; - **`on_delete=models.CASCADE`** — deleting the vehicle row deletes the van row. The name is the lowercased parent name plus `_ptr`. To choose another name — or to resolve a diamond in which two parents share a common ancestor — declare your own `OneToOneField(Vehicle, on_delete=models.CASCADE, parent_link=True)` on the child and Django uses it instead of creating one. ## What the ORM emits Because parent columns live only in `vehicle`, a query that loads child objects joins it: ```sql -- Van.objects.filter(cargo_volume_m3__gte=10) SELECT vehicle.id, vehicle.plate, vehicle.daily_rate, van.vehicle_ptr_id, van.cargo_volume_m3 FROM van INNER JOIN vehicle ON (van.vehicle_ptr_id = vehicle.id) WHERE van.cargo_volume_m3 >= 10; ``` A grandchild model joins once per level. Writes go the other way: `Van.save()` saves the parent row first, copies its key into `vehicle_ptr_id`, then saves the van row; because two statements are needed, Django wraps them in a transaction. ## Walking from the parent to the child The parent query does not know about its children: | Expression | Result | |---|---| | `Vehicle.objects.all()` | `Vehicle` instances only — Django does not swap in `Van` objects | | `vehicle.van` | the `Van` row, via the reverse one-to-one; one extra query per vehicle | | `vehicle.van` on a non-van | raises `Van.DoesNotExist` | | `Vehicle.objects.select_related("van")` | joins the van table up front, so `vehicle.van` needs no extra query | | `van.vehicle_ptr` | the parent `Vehicle` object | Code that must act on "whatever this vehicle really is" either tests each accessor or stores a `kind` on the parent. Django has no built-in automatic downcasting. ## Turning an existing parent into a child A frequent follow-up: a plain `Vehicle` already exists, and it must now become a `Van`. Building `Van(vehicle_ptr=vehicle, cargo_volume_m3=12)` and calling `save()` looks right, but saving a child always saves its parent levels too, and the new `Van` instance carries its own values for `plate`, `daily_rate` and every other inherited field. Unless you copy the parent's values onto the new instance first, the save writes empty or default values back over the existing vehicle row, or fails on a `NOT NULL` column. The safe pattern is: - load the `Vehicle`; - construct the `Van` with `vehicle_ptr=vehicle` **and** every inherited field value copied from the vehicle; - save inside a transaction and re-read it to confirm both rows. The reverse conversion, a van that should stop being a van, is `van.delete(keep_parents=True)`, which removes only the van row. ## Reverse-name collisions The implicit link uses up the default reverse name. On `Vehicle`, the accessor `van` and the query name `van` now belong to `vehicle_ptr`. If `Van` then declares another relation to `Vehicle` — say `towable = models.ManyToManyField(Vehicle)` — its default reverse query name is also `van`, and system checks report that the reverse query name for `Van.towable` clashes with the one for `Van.vehicle_ptr`. The fix is an explicit `related_name` on the new field. ## Why it matters in an interview The parent link explains every multi-table cost at once: 1. reads cost a JOIN because the data is split; 2. writes cost a statement per level because each level is a row; 3. navigation from parent to child costs a query because it is a reverse relation; 4. deletes cascade because the link is `CASCADE`. A candidate who can draw the two tables and the `vehicle_ptr_id` column can derive the rest.
- Why does adding towable = ManyToManyField(Vehicle) to Van fail Django's system checks?The implicit `vehicle_ptr` already owns the reverse name `van` on `Vehicle`. The new field's default reverse query name is also `van`, so checks report a clash between `Van.towable` and `Van.vehicle_ptr`. Give the new field an explicit `related_name`, for example `related_name="towed_by"`.
- When would you declare the parent_link field yourself instead of letting Django create it?To control its name, or in multiple inheritance where two parents share a common ancestor: each intermediate parent declares its own `OneToOneField(..., parent_link=True)` to the ancestor so the automatically generated links do not clash in the grandchild.
A van is a registration card plus a separate spec sheet stapled on by the same registration number: reading the van means pulling both, and if you hold only a card you must go and look for a spec sheet that may not exist.
saying these in an interview costs you the question
- The child table repeats every parent column, so no join is needed.
- Vehicle.objects.all() returns Van objects wherever the row is a van.
- vehicle.van returns None when that vehicle is not a van.
- The child table has its own id plus a separate vehicle_id foreign key.
- Deleting a Vehicle leaves its van row behind as an orphan.