Chapter 07 · Inheritance, Table Splitting, Entity Splitting, and Advanced Relational Mapping

Choose Mapping Strategies with Measured SQL Plans Instead of Object-Oriented Preference

Compare TPH, TPT, and TPC with identical workloads, SQL, plans, command counts, migration diffs, and repeatable timing so the mapping choice is evidence-backed.

Intermediate120–150 minutesmapping-strategy evidence capstoneEF Core 10.0.11 · .NET 10.0.11SQLite provider 10.0.11 baselineSDK checkpoint: 10.0.400Last reviewed: August 2026

Learning outcomes

TPH, TPT, and TPC all map the same C# hierarchy and can all be “correct.” The engineering question is which mapping fits ServiceHub’s actual query/write/migration workload. This lesson builds an evidence pack: identical data, identical LINQ projections, generated SQL, SQLite plans, command counts, file/schema shape, and repeatable timing. The goal is not to crown a universal winner.

01

Construct comparable TPH/TPT/TPC databases with identical hierarchy data and GUID keys.

02

Capture generated SQL separately from actual database execution plans.

03

Measure latency distributions and command counts without fabricating benchmark numbers.

04

Compare joins, unions, row width, duplicated columns, index needs, write complexity, and migration blast radius.

05

Explain why SQLite lab results cannot be pasted onto SQL Server/PostgreSQL/MySQL/Oracle production decisions.

06

Write an evidence-backed mapping decision and rollback/migration plan.

1. A fair comparison controls variables

Use the same ServiceTarget hierarchy, same deterministic IDs, same row counts, same equipment/facility distribution, same query projections, same tracking mode, and same machine/runtime. Only mapping strategy should change. Record .NET/EF/provider versions and database settings alongside results.

text · measurement manifest
.NET SDK: 10.0.400.NET runtime: 10.0.11EF Core: 10.0.11Microsoft.EntityFrameworkCore.Sqlite: 10.0.11OS/CPU/RAM: <record locally>Databases: servicehub-tph.db / servicehub-tpt.db / servicehub-tpc.dbDataset: <record total + subtype distribution>Tracking: AsNoTracking for read benchmarkLogging during timing: <on/off; disclose>Warmup iterations: <record>Measured iterations: <record>SQLite journal/page/cache settings: <record if changed>

2. Define workloads before measuring

Workload Why it discriminates mappings
W1: equipment by manufacturer + code projection Subtype-only read: TPC may avoid base join; TPH uses discriminator; TPT joins base+derived.
W2: all active targets ordered by code Polymorphic read: TPH one table, TPT joins, TPC union.
W3: insert 1,000 mixed targets Shows command count/table touches, key generation, index maintenance.
W4: change base DisplayName TPH/TPT touch one logical storage location per row; TPC repeats base schema across tables.
W5: migration adds base AssetOwner Shows schema-change blast radius across mapping strategies.

3. Capture LINQ and generated SQL first

csharp · identical workload function
static IQueryable<TargetRow> ActiveTargets(DbContext db) =>    db.Set<ServiceTarget>()      .AsNoTracking()      .Where(x => x.IsActive)      .OrderBy(x => x.TargetCode)      .Select(x => new TargetRow(x.Id, x.TargetCode, x.DisplayName));var query = ActiveTargets(db);Console.WriteLine(query.ToQueryString());var rows = await query.ToListAsync(ct);

ToQueryString() proves translation shape; it is not an execution plan and does not include network, locking, cache state, or actual row counts. Store each strategy’s SQL beside the results.

4. Ask SQLite for the plan—but keep provider scope explicit

sql · representative plan commands
-- TPHEXPLAIN QUERY PLANSELECT Id, target_code, display_nameFROM service_targets_tphWHERE is_active = 1ORDER BY target_code;-- TPT/TPC: paste the actual SELECT emitted/logged by your lab,-- then prefix it with EXPLAIN QUERY PLAN.

SQLite’s plan vocabulary differs from SQL Server actual execution plans, PostgreSQL EXPLAIN (ANALYZE, BUFFERS), MySQL EXPLAIN ANALYZE, and Oracle plans. The local lab teaches evidence collection, not cross-engine plan equivalence.

5. Time repeated end-to-end queries without pretending one stopwatch is science

csharp · simple reproducible timing harness
static async Task<double[]> MeasureAsync(    Func<Task> operation,    int warmups,    int iterations){    for (var i = 0; i < warmups; i++)        await operation();    var samples = new double[iterations];    for (var i = 0; i < iterations; i++)    {        var sw = Stopwatch.StartNew();        await operation();        sw.Stop();        samples[i] = sw.Elapsed.TotalMilliseconds;    }    return samples;}static double Percentile(double[] values, double p){    var ordered = values.OrderBy(x => x).ToArray();    var index = (int)Math.Clamp(        Math.Ceiling(p * ordered.Length) - 1,        0, ordered.Length - 1);    return ordered[index];}

Report median/p95 together with dataset/environment, not a single best run. For serious microbenchmarks, BenchmarkDotNet can add process/warmup rigor, but it cannot replace database plan and topology analysis.

6. Count commands and table touches on writes

csharp · log database commands during a controlled write
optionsBuilder.LogTo(    Console.WriteLine,    new[] { DbLoggerCategory.Database.Command.Name },    LogLevel.Information);// Insert the same deterministic EquipmentTarget in each strategy context.// Record the emitted INSERT commands, affected tables, and transaction boundary.

Expected mechanism: TPH writes one hierarchy row; TPT writes base + derived rows; TPC writes one concrete row. Actual batching/provider behavior is version-specific, so record commands rather than assuming a fixed round-trip count.

7. Compare schema/migration blast radius, not only SELECT latency

shell · generate strategy-specific migrations
dotnet ef migrations add AddTargetOwnerTph   --context TphMappingContext   --output-dir Data/Migrations/Tphdotnet ef migrations add AddTargetOwnerTpt   --context TptMappingContext   --output-dir Data/Migrations/Tptdotnet ef migrations add AddTargetOwnerTpc   --context TpcMappingContext   --output-dir Data/Migrations/Tpc

A new base property typically means one table change in TPH/TPT but a column added to every concrete table in TPC. Conversely, splitting/normalizing can make some constraints easier or harder. Review generated migrations; do not infer operational risk from LOC counts alone.

8. Decision matrix: evidence plus domain constraints

Dimension TPH TPT TPC
Polymorphic read shape One table; discriminator where needed Base + derived joins Union concrete tables
Subtype read shape One table + discriminator Base/derived join Concrete table
Row/schema duplication Subtype columns sparse in one table Low duplication Inherited columns duplicated
Key generation Simple global table Simple via base table Must be global without common table
Simple polymorphic FK Yes Yes Often no single FK target
Base-property migration One table Base table Every concrete table
SQLite lab key choice Guid works; integer also possible Guid works; integer also possible Use global/client Guid for portable lab
Best choice Measure workload Measure workload Measure workload

9. Deliberately wrong benchmark: one query, ten rows, Debug logging

A developer runs each strategy once on ten rows with sensitive/detailed logging enabled and declares the fastest. That result mixes JIT, model initialization, cache state, console I/O, and tiny-data noise. It proves almost nothing about production.

Repair the experiment

  1. Use representative cardinality and subtype distribution.
  2. Separate startup/model cost from steady-state query cost.
  3. Warm up deliberately and record warmup policy.
  4. Disable noisy logging during timed runs, but keep a separate evidence run for SQL/commands.
  5. Run repeated samples and report a distribution.
  6. Inspect the database plan and indexes.
  7. Repeat on the production-like provider before a production mapping decision.

10. Hands-on capstone: produce a mapping decision record

  1. Reset and seed all three SQLite strategy databases with identical deterministic data.
  2. Run W1–W5 from this lesson and save SQL, plans, timings, command counts, and migration diffs.
  3. Fill in the measurement manifest completely.
  4. Write one paragraph explaining which mapping you would choose for ServiceHub under the measured workload.
  5. Write one paragraph naming a workload change that would make you reconsider.
  6. State provider-specific validation still required before deployment.
  7. Describe migration/rollback ownership if changing an existing hierarchy mapping with real data.

Check your understanding

  1. Why must the dataset/query shape be identical across strategies?
  2. Is ToQueryString an execution plan?
  3. What additional evidence should accompany timings?
  4. Why does TPC make a new base-property migration broader?
  5. Why can SQLite results not settle a SQL Server production choice?
  6. What makes a mapping recommendation defensible?
Review the answers

Otherwise the experiment changes multiple variables and cannot attribute differences to mapping strategy.

No. It shows generated SQL/translation shape, not optimizer runtime behavior.

Plans, row counts, indexes, command counts, environment/version/topology, tracking/logging/cache settings, and sample distributions.

Inherited columns are duplicated into every concrete table, so the base property must be added to each.

Different engines/providers have different optimizers, indexes, statistics, types, key generation, SQL translation, and I/O behavior.

A stated workload plus reproducible evidence, operational/schema constraints, provider validation, and a willingness to revisit when those inputs change.

11. Chapter 07 production judgment and bridge

Inheritance mapping is an architectural choice with schema, SQL, key, constraint, migration, and performance consequences. Prefer the simplest mapping that satisfies the domain and measured workload, and keep mapping strategy changes rare because production data migration can be substantial. Chapter 08 now turns from model shape to the EF query pipeline itself: IQueryable, expression trees, deferred execution, translation, async execution, and generated SQL.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.

\n