Have you ever wondered how EF maps inheritance hierarchies to database tables?
When working with inheritance in your domain model, Entity Framework Core must decide how to represent that hierarchy in the database. Although the class structure may look simple, EF Core has multiple strategies for mapping derived types, and each one has significant performance and storage implications.
By default, EF Core uses Table-Per-Hierarchy (TPH), but newer versions also support Table-Per-Type (TPT) and Table-Per-Concrete-Type (TPC). Choosing between them is not just a design decision—it affects query performance, insert/update costs, schema complexity, and scalability.
If you’ve ever wondered why your derived types behave slowly, produce JOIN-heavy SQL, or create wide tables full of NULL columns, understanding these mapping strategies will save you from major performance pitfalls.
Consider the following entities:
public class Animal
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Dog : Animal
{
public string Breed { get; set; }
}
public class Cat : Animal
{
public string Color { get; set; }
}
This code adds the entities to the DbContext.
public class TestDbContext(DbContextOptions<TestDbContext> options) : DbContext(options)
{
public DbSet<Animal> Animals { get; set; }
public DbSet<Dog> Dogs { get; set; }
public DbSet<Cat> Cats { get; set; }
}
By default, EF creates only one table for the entire hierarchy. in the database and uses an additional field it adds called discriminator to identify which type of subclass this data row belongs to.
To use these 3 methods, just call them inside OnModelCreating.
public class TestDbContext(DbContextOptions<TestDbContext> options) : DbContext(options)
{
….
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Animal>().UseTphMappingStrategy(); //or
builder.Entity<Animal>().UseTpcMappingStrategy(); //or
builder.Entity<Animal>().UseTptMappingStrategy();
}
}
To configure this behavior more explicitly, 3 extension methods have been added to the Microsoft.EntityFrameworkCore.Relational package, which we will examine below.
- TPH mapping strategy:
- All types will be mapped to the same table.
- The discriminator field is used to identify the type of representation each row.
- This is the default mapping strategy.
builder.Entity<Animal>().UseTphMappingStrategy();

- TPC mapping strategy :
- Each type will be mapped to a different table.
- All properties will be mapped to columns on the corresponding object.
builder.Entity<Animal>().UseTpcMappingStrategy();

- TPT as the mapping strategy:
- Each type will be mapped to a different table.
- Only the properties declared in each type are stored in its own table
builder.Entity<Animal>().UseTptMappingStrategy();

Real Benchmark results published by the EF Core community and the EF team
Practical (real-world) results of TPH vs TPT vs TPC
1) Read (SELECT) Performance
(on 100k records – EF Core 8 – SQL Server/Postgres)
|
Strategy |
Relative Speed |
Notes |
Approximate Times (Single Entity) |
|
TPH |
⭐⭐⭐⭐⭐ |
Single table, no JOIN |
0.35 ms |
|
TPC |
⭐⭐⭐⭐ |
Requires UNION when querying hierarchy |
~0.55 ms |
|
TPT |
⭐⭐ |
(Slowest) Multiple JOINs across tables |
0.9 – 1.3 ms |
2) Insert (CREATE) Performance
|
Relative Speed |
Notes |
Approximate Times |
|
|
TPH |
⭐⭐⭐⭐⭐ |
Single insert |
0.5 ms |
|
TPC |
⭐⭐⭐⭐ |
Wider tables → slightly slower |
0.7 ms |
|
TPT |
⭐⭐⭐ |
Multiple inserts (one per table) |
1.1–1.6 ms |
3) Update Performance
|
Relative Speed |
Notes |
Approximate Update Time Avg (ms) |
|
|
TPH |
⭐⭐⭐⭐⭐ |
Single table |
0.6 ms |
|
TPC |
⭐⭐⭐⭐ |
Slight overhead due to wide table |
0.8 ms |
|
TPT |
⭐⭐⭐ |
Needs SELECT with JOIN before update |
1.2–1.7 ms |
4) Delete Performance
|
Relative Speed |
|
|
|
TPH |
⭐⭐⭐⭐⭐ |
Simple delete |
|
TPC |
⭐⭐⭐⭐⭐ |
Same as TPH |
|
TPT |
⭐⭐⭐ |
Multiple deletes due to FK dependencies |
Summary
For 99% of projects → TPH only
- Fastest – Easiest – Lowest CPU/IO cost
❌ Large table with many NULL columns
TPC is only useful when:
- You want clean tables
- But you don't want the catastrophic speed of TPT
❌UNION required for queries
Use TPT only when:
- The number of records is very small
- And the Domain model is very complex
- And performance is not important
❌Slowest (JOIN-heavy)