Start Debugging

TPH vs TPT vs TPC beim Vererbungsmapping in EF Core 11: Was sollten Sie wählen?

In EF Core 11 verwenden Sie standardmäßig TPH für fast jede Hierarchie, greifen nur zu TPC, wenn Sie überwiegend einen einzelnen Blatttyp abfragen und ein Benchmark den Vorteil belegt, und nutzen TPT nur, wenn eine externe Vorgabe es erzwingt.

In EF Core 11 (mit .NET 11 und C# 14) mappen Sie eine Klassenhierarchie mit Table-per-Hierarchy (TPH), sofern Sie keinen gemessenen Grund dagegen haben. TPH legt die gesamte Hierarchie in eine einzige Tabelle mit einer Diskriminatorspalte, sodass Lesevorgänge Scans einer einzelnen Tabelle ohne Joins sind. Greifen Sie nur dann zu Table-per-Concrete-Type (TPC), wenn Ihr Code überwiegend einen einzelnen Blatttyp abfragt und ein Benchmark auf Ihren Daten zeigt, dass er TPH schlägt. Verwenden Sie Table-per-Type (TPT) nur, wenn eine externe Vorgabe es erzwingt, denn Microsofts eigener Benchmark setzt TPT bei einer Abfrage des Basistyps auf ungefähr die doppelte Zeit und fast die doppelten Allokationen von TPH. Die Regel in einer Zeile: TPH als Standard, TPC für blattlastige Workloads, die schneller messen, TPT niemals aus freien Stücken.

Dieser Artikel ist die Entscheidung, nicht die vollständige Konfigurationsanleitung. Wenn Sie die Diskriminator-API, geteilte Spalten und die Mechanik nullbarer Spalten im Detail möchten, lesen Sie wie Sie das Table-per-Hierarchy-(TPH-)Vererbungsmapping in EF Core 11 konfigurieren. Hier stellen wir die drei Strategien nebeneinander, zeigen das Schema, das jede erzeugt, und benennen die Einschränkungen, die die Entscheidung für Sie treffen.

Die Funktionsmatrix auf einem Bildschirm

Nehmen Sie eine zweistufige Hierarchie: eine Basisklasse Blog und eine abgeleitete Klasse RssBlog, die ein RssUrl hinzufügt. Die drei Strategien mappen dies auf drei völlig verschiedene Schemata, und jeder Kompromiss unten folgt aus dieser Form.

DimensionTPHTPTTPC
Erzeugte Tabelleneine, gesamte Hierarchieeine pro Typ (inkl. abstrakter)eine pro konkretem Typ
Diskriminatorspaltejaneinnein
Spalten abgeleiteter Typennullbar, geteilte Tabelleeigene Tabelle, NOT NULL möglicheigene Tabelle, NOT NULL möglich
Basistyp-Abfrage (context.Blogs)ein SELECT, kein JoinLEFT JOIN über alle TabellenUNION ALL über konkrete Tabellen
Einzelblatt-Abfrage (OfType<RssBlog>)Diskriminator-PrädikatJoin Basis- + Blatt-Tabelleeine Tabelle, kein Filter
Speicherformbreit, dünn besetzt, viele Nullsnormalisiert, keine Nullsdenormalisiert, Spalten wiederholt
Schlüsselerzeugungbeliebig (Identity ok)beliebig (Identity auf Basis)geteilte Sequenz, kein einfaches Identity
FK-Constraint zum Basistypjajanein (Schlüssel in der Blatt-Tabelle)
Komplexe Typen / JSON-Spaltenjaja (neu in EF Core 11)ja (neu in EF Core 11)
Basistyp-Lesevorgang: relative Geschwindigkeitam schnellsten (Basis)~2x langsamer~gleich wie TPH
Microsofts Haltungempfohlener Standard”nur wenn dazu gezwungen”gut für Einzelblatt-Abfragen

Das Muster ist nicht subtil. TPH gewinnt oder liegt gleichauf in fast jeder Zeile, die zählt, TPC zieht damit gleich, außer wenn Sie typübergreifend abfragen, und TPT tauscht ein sauberer aussehendes Schema gegen Joins, die Sie zur Abfragezeit kosten. Drei dieser Zellen haben sich in EF Core 11 geändert: komplexe Typen und JSON-Spalten funktionieren nun auf TPT- und TPC-Hierarchien, was zuvor nicht unterstützt wurde und die Leute für jedes geerbte Wertobjekt zurück zu Owned Entities drängte. Das schließt einen der letzten nicht leistungsbezogenen Gründe, TPT und TPC zu meiden, ändert aber das Leistungsurteil nicht.

Was jede Strategie tatsächlich in die Datenbank schreibt

Die Schemata machen die abstrakten Kompromisse konkret. TPH ist eine einzige Tabelle mit einem Diskriminator und nullbaren abgeleiteten Spalten:

-- TPH: EF Core 11, SQL Server
CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL IDENTITY,
    [Url] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [RssUrl] nvarchar(max) NULL,          -- nullable: base Blogs have no RssUrl
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

TPT teilt jeden Typ in seine eigene Tabelle auf, verbunden durch einen Fremdschlüssel auf dem geteilten Primärschlüssel:

-- TPT: EF Core 11, SQL Server
CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL IDENTITY,
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL,
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
    CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId])
        REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);

TPC gibt jedem konkreten Typ eine eigenständige Tabelle mit jeder geerbten Spalte wiederholt, mit Schlüsseln auf Basis einer geteilten Sequenz:

-- TPC: EF Core 11, SQL Server
CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,             -- inherited column, repeated here
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);

Jede zu konfigurieren ist eine einzige Zeile auf der Wurzelentität. TPH ist der Standard und benötigt nichts; TPT und TPC aktivieren Sie mit einem Aufruf der Mapping-Strategie:

// EF Core 11: choosing a strategy on the root entity type
modelBuilder.Entity<Blog>().UseTphMappingStrategy(); // default, can be omitted
modelBuilder.Entity<Blog>().UseTptMappingStrategy(); // one table per type
modelBuilder.Entity<Blog>().UseTpcMappingStrategy(); // one table per concrete type

Wann Sie TPH wählen

TPH ist die richtige Antwort für die große Mehrheit der Hierarchien. Wählen Sie es, wenn:

Der einzige Preis, den Sie akzeptieren, ist, dass eine auf einem abgeleiteten Typ erforderliche Eigenschaft dennoch auf eine nullbare Spalte gemappt wird, weil Geschwisterzeilen sie leer lassen. Wenn datenbankseitig erzwungene Nicht-Nullbarkeit auf abgeleiteten Eigenschaften eine harte Anforderung ist, ist das der klassische Grund, TPH zu verlassen, und er zeigt auf TPT.

Wann Sie TPC wählen

TPC ist der Spezialist. Es kommt bei typübergreifenden Abfragen nahe an TPH heran und zieht in einer bestimmten Form davon:

Der Preis ist ein denormalisiertes Schema und umständliche Schlüssel. TPC kann keine einfache Identity-Spalte verwenden, weil es keine einzelne Tabelle gibt, die die Sequenz besitzt; EF Core 11 verwendet standardmäßig eine geteilte Datenbanksequenz (NEXT VALUE FOR [BlogSequence]), damit die Schlüssel über Geschwistertabellen hinweg eindeutig bleiben. In SQLite, das keine Sequenzen hat, ist die Erzeugung ganzzahliger Schlüssel für TPC nicht verfügbar, und Sie greifen auf clientseitig erzeugte GUIDs zurück. Und da der Primärschlüssel eines Basistyps in jeder konkreten Tabelle liegen kann, kann ein Fremdschlüssel, der den Basistyp referenziert, überhaupt nicht durch ein Datenbank-Constraint erzwungen werden. Wenn alle Ihre Schreibvorgänge über EF Core mit Navigationen laufen, ist das meist in Ordnung, aber es ist ein echter Verlust an Integrität auf Datenbankebene.

Wann Sie TPT wählen (und warum die Antwort meist “nicht” lautet)

TPT erzeugt das Schema, das Ihrem Klassendiagramm am ähnlichsten sieht: eine Tabelle pro Typ, verbunden über den Schlüssel. Diese Ästhetik ist die Falle. Greifen Sie nur dann zu TPT, wenn:

Wählen Sie TPT nicht, weil es sich sauberer anfühlt. Jede Basistyp-Abfrage joint über die gesamte Menge an Tabellen, und Joins sind eine der Hauptquellen relationaler Leistungsprobleme. Die Zahlen bestätigen das, was der nächste Abschnitt ist.

Der Benchmark: TPT kostet ungefähr 2x

Das ist kein Gerede. Microsofts eigener Vererbungs-Benchmark baut eine Hierarchie mit 7 Typen auf, befüllt 5000 Zeilen pro Typ (35000 Zeilen insgesamt) und lädt jede Zeile aus der Datenbank. Die Ergebnisse:

MethodeMittelAllokiert
TPH149.0 ms40 MB
TPT312.9 ms75 MB
TPC158.2 ms46 MB

TPT ist ungefähr 2,1x langsamer als TPH und allokiert fast die doppelte Menge Speicher, weil das Laden der Hierarchie sieben Tabellen joint. TPC liegt bei dieser Abfrage aller Typen innerhalb von etwa 6 Prozent von TPH und würde TPH bei einer Einzelblatt-Abfrage übertreffen, bei der es eine Tabelle liest und TPH weiterhin die geteilte Tabelle mit einem Diskriminatorfilter scannt. Die Methodik zählt: Dies ist eine Basistyp-Abfrage, die jede Tabelle berührt, was der schwächste Fall von TPC und von TPT ist, sodass die Lücke, die Sie in Ihrer Workload sehen, davon abhängt, wie oft Sie typübergreifend gegenüber einem Blatttyp abfragen. Die Kernaussage ist dennoch über Läufe hinweg stabil: TPT zahlt eine Join-Steuer, die TPH und TPC nicht zahlen, und kein Argument der Schema-Ästhetik kauft das zurück.

Führen Sie den Benchmark gegen Ihr eigenes Modell aus, bevor Sie eine unumkehrbare Entscheidung treffen. Eine Vererbungsstrategie nach dem Vorliegen von Produktionsdaten zu ändern bedeutet eine Schemamigration, die Zeilen zwischen Tabellen verschiebt, daher ist dies eine Entscheidung, die es wert ist, einmal früh gemessen zu werden.

Die Fallstricke, die für Sie entscheiden

Drei Einschränkungen können die Strategie unabhängig von der Vorliebe entscheiden.

Die erste ist datenbankseitig erzwungene Nicht-Nullbarkeit auf einer abgeleiteten Eigenschaft. TPH kann das nicht, weil die geteilte Spalte für die Geschwisterzeilen nullbar sein muss. Wenn Sie brauchen, dass die Datenbank (nicht nur Ihre Anwendung) garantiert, dass jedes CardPayment ein Last4 hat, benötigen Sie diese Spalte in ihrer eigenen Tabelle, was TPT oder TPC bedeutet.

Die zweite ist die Schlüsselerzeugung auf Ihrer Datenbank. TPC benötigt Sequenzen für ganzzahlige Schlüssel. In SQL Server ist das automatisch, aber in SQLite können Sie mit TPC überhaupt keine ganzzahligen Identity-Schlüssel verwenden und müssen auf GUIDs umsteigen. Wenn Sie auf SQLite sind und ganzzahlige Schlüssel wollen, fällt TPC weg.

Die dritte ist die Fremdschlüssel-Integrität zum Basistyp. Wenn andere Tabellen Ihren Basistyp referenzieren und Sie möchten, dass die Datenbank diese Referenzen erzwingt, kann TPC Ihnen das Constraint nicht geben. TPH und TPT können es. Dies allein schließt TPC für viele normalisierte Schemata aus.

Eine Sache ist bei allen drei gleich: Sie können den Typ einer Entität nicht zur Laufzeit ändern. Ein CardPayment in ein BankTransferPayment zu verwandeln ist in jeder Strategie ein Delete plus ein Insert, weil der Diskriminator (oder die Tabelle selbst) den Typ kodiert. Das ist eine Realität der Modellierung, kein Unterscheidungsmerkmal.

Die Empfehlung, klar gesagt

Verwenden Sie standardmäßig TPH. Es ist am schnellsten für die häufige typübergreifende Abfrage, am einfachsten, um dagegen zu schreiben, die einzige Strategie ohne Reibung bei der Schlüsselerzeugung und der von Microsoft empfohlene Standard für ein breites Spektrum an Szenarien. Greifen Sie nur dann zu TPC, wenn Ihre Workload von Abfragen eines einzelnen Blatttyps dominiert wird und ein Benchmark auf Ihren Daten zeigt, dass es TPH schlägt, und akzeptieren Sie das denormalisierte Schema, die Schlüssel aus einer geteilten Sequenz und das fehlende FK-Constraint zum Basistyp, die damit einhergehen. Verwenden Sie TPT nur, wenn ein externer Faktor Ihnen keine Wahl lässt, und tun Sie es im Wissen, dass Sie eine Abfragesteuer von ungefähr 2x für ein Schema zahlen, das ordentlicher aussieht.

Das mentale Modell ist dasselbe, das die Zahlen erzwingen: eine Tabelle ist schnell, viele per Join verbundene Tabellen sind langsam, und viele Tabellen ohne Join sind schnell, aber denormalisiert. Wenn diese Entscheidung Teil eines breiteren Versionswechsels ist, tauchen die Änderungen an Vererbung und Mapping tendenziell zusammen mit denen im Migrationsleitfaden von EF Core 6 zu EF Core 11 auf.

Weiterführende Lektüre

Quellen

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Zurück