How do you model inheritance/polymorphic relationships in a relational schema?
Quick Answer
There's no native "inheritance" in relational modeling, so it's approximated with one of three patterns: **single table inheritance** (one wide table with a type discriminator column and nullable columns for subtype-specific fields), **class table inheritance** (a base table plus one table per subtype sharing the base's primary key), or **concrete table inheritance** (a fully separate table per subtype, duplicating shared columns). Each trades off query simplicity, storage efficiency, and referential integrity differently.
Detailed Answer
Suppose you have Vehicle as a concept, with subtypes Car (has num_doors) and Truck (has cargo_capacity). Relational databases have no built-in "subtype" concept, so you pick one of three patterns.
1. Single Table Inheritance (STI)
One table for all subtypes, with a discriminator column and nullable subtype-specific columns:
CREATE TABLE vehicles (
id SERIAL PRIMARY KEY,
type VARCHAR(20) NOT NULL, -- discriminator: 'car' or 'truck'
make VARCHAR(50),
model VARCHAR(50),
num_doors INT, -- only meaningful when type = 'car'
cargo_capacity NUMERIC(10,2) -- only meaningful when type = 'truck'
);
Pros: simplest queries (no joins to get a full record), easy to add a new shared field. Cons: many nullable columns that are only meaningful for some rows, no way to enforce "car rows must have num_doors set" with a plain NOT NULL (needs a CHECK tied to type), and the table grows wide as subtypes multiply.
2. Class Table Inheritance (CTI)
A base table plus one table per subtype, sharing the same primary key value:
CREATE TABLE vehicles (id SERIAL PRIMARY KEY, make VARCHAR(50), model VARCHAR(50));
CREATE TABLE cars (
vehicle_id INT PRIMARY KEY REFERENCES vehicles(id),
num_doors INT NOT NULL
);
CREATE TABLE trucks (
vehicle_id INT PRIMARY KEY REFERENCES vehicles(id),
cargo_capacity NUMERIC(10,2) NOT NULL
);
Pros: no wasted nullable columns, NOT NULL works normally within each subtype table, closely mirrors an OOP inheritance hierarchy. Cons: fetching a full car record requires a join across vehicles and cars; nothing in plain SQL stops a vehicle_id from (incorrectly) appearing in both cars and trucks without an extra constraint.
3. Concrete Table Inheritance
A fully separate, self-contained table per subtype, duplicating the shared columns:
CREATE TABLE cars (id SERIAL PRIMARY KEY, make VARCHAR(50), model VARCHAR(50), num_doors INT NOT NULL);
CREATE TABLE trucks (id SERIAL PRIMARY KEY, make VARCHAR(50), model VARCHAR(50), cargo_capacity NUMERIC(10,2) NOT NULL);
Pros: each table is simple and self-contained, no joins needed for a single subtype. Cons: querying "all vehicles regardless of type" needs a UNION, shared columns (make, model) are duplicated in every subtype's schema, and there's no single foreign-key target if some other table needs to reference "any vehicle."
- Few, rarely-changing subtypes with mostly shared columns → STI is often good enough and keeps queries simple.
- Many subtypes with substantially different, non-overlapping columns, and you need
NOT NULLcorrectness per subtype → CTI. - Subtypes that are queried almost entirely independently and never need a unified "any vehicle" view → concrete table inheritance.
This is also a place where document databases (see the NoSQL topic) sometimes fit more naturally than a relational schema, since a document can freely have subtype-specific fields without any of these three tradeoffs — worth mentioning if the interview also touches polyglot persistence.