Vererbung in relationalen Datenbanken sauber abbilden
AI generated
SELECT
JOIN
SQL · Datenmodellierung · Relationale Datenbanken
Vererbung in relationalen Datenbanken sauber abbilden
Single Table, Class Table und Concrete Table Inheritance

Objektorientierte Vererbung passt nicht direkt auf das relationale Modell, weshalb jede Abbildung ein Kompromiss ist. Dieser Beitrag erklärt die drei etablierten Muster für Vererbung in Datenbanken, Single Table Inheritance, Class Table Inheritance und Concrete Table Inheritance, mit echten Tabellenentwürfen und den jeweiligen Tradeoffs bei NULL-Werten, Abfragen und Erweiterbarkeit.

17 Min. Lesezeit Single Table · Class Table · Concrete Table Standard-SQL · MySQL · PostgreSQL

1. Das Problem: OOP-Vererbung trifft relationales Modell

Objektorientierte Sprachen kennen Vererbung als natives Sprachkonstrukt: Eine Basisklasse "Zahlungsmethode" definiert gemeinsame Felder, "Kreditkarte" und "Banküberweisung" erben davon und ergänzen eigene Felder. Das relationale Modell kennt kein solches Konzept, es kennt nur Tabellen mit einer festen Spaltenmenge. Jede Abbildung von Vererbung in der Datenbank ist deshalb eine bewusste Entscheidung darüber, wie eine Klassenhierarchie auf eine oder mehrere Tabellen projiziert wird, mit unterschiedlichen Konsequenzen für Speicherplatz, Abfrageaufwand und referenzielle Integrität.

Die Herausforderung wird sichtbar, sobald mehrere Subtypen einer Basisklasse unterschiedliche zusätzliche Attribute tragen. Eine Kreditkartenzahlung braucht card_number und expiry_date, eine Banküberweisung braucht iban und bic. Werden alle Subtypen in einer Tabelle zusammengefasst, entstehen zwangsläufig Spalten, die für einen Teil der Zeilen sinnlos und leer sind. Wird jeder Subtyp in einer eigenen Tabelle gespeichert, verliert man die Möglichkeit, alle Zahlungsmethoden gemeinsam über eine einzige Abfrage zu erfassen, ohne mehrere Tabellen zu vereinigen.

Drei etablierte Muster lösen dieses Spannungsfeld auf unterschiedliche Weise: Single Table Inheritance bündelt alles in einer Tabelle, Class Table Inheritance verteilt gemeinsame und spezifische Felder auf mehrere verknüpfte Tabellen, Concrete Table Inheritance dupliziert gemeinsame Felder in jede Subtyp-Tabelle. Jedes dieser Muster für Vererbung in der Datenbank hat einen klaren Anwendungsbereich, den die folgenden Abschnitte im Detail zeigen.

2. Single Table Inheritance im Detail

Single Table Inheritance, kurz STI, speichert die gesamte Klassenhierarchie in genau einer Tabelle. Eine Discriminator-Spalte, meist "type" oder "payment_type" genannt, hält fest, um welchen Subtyp es sich in der jeweiligen Zeile handelt. Alle Spalten aller Subtypen existieren gleichzeitig in der Tabelle, wobei jede Zeile nur die Spalten füllt, die zu ihrem eigenen Subtyp passen, alle anderen bleiben NULL. Diese Form der Vererbung in der Datenbank ist die einfachste Umsetzung und funktioniert gut, wenn die Anzahl der subtypspezifischen Spalten klein bleibt und sich die Hierarchie selten ändert.

Der große Vorteil von STI ist die Abfrageeinfachheit: Ein einzelnes SELECT ohne JOIN liefert alle Zahlungsmethoden unabhängig vom Subtyp. Das ist besonders wertvoll für Listenansichten und Reports, die über die gesamte Hierarchie hinweg arbeiten. Der Nachteil zeigt sich, sobald die Subtypen viele unterschiedliche Felder tragen: Die Tabelle wird breit, viele Spalten sind für die meisten Zeilen NULL, und CHECK-Constraints, die sicherstellen, dass die richtigen Felder für den jeweiligen Typ gesetzt sind, werden schnell unübersichtlich.


-- Single Table Inheritance: all subtypes in one table
CREATE TABLE payment_method (
    payment_method_id  BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id         BIGINT UNSIGNED NOT NULL,
    payment_type         VARCHAR(20) NOT NULL,   -- discriminator column
    -- fields shared by all subtypes
    created_at           TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    -- fields specific to credit_card
    card_number          VARCHAR(19),
    card_expiry          DATE,
    -- fields specific to bank_transfer
    iban                 VARCHAR(34),
    bic                  VARCHAR(11),
    CONSTRAINT chk_payment_type
        CHECK (payment_type IN ('credit_card', 'bank_transfer')),
    CONSTRAINT chk_credit_card_fields
        CHECK (payment_type <> 'credit_card'
               OR (card_number IS NOT NULL AND card_expiry IS NOT NULL)),
    CONSTRAINT chk_bank_transfer_fields
        CHECK (payment_type <> 'bank_transfer'
               OR (iban IS NOT NULL AND bic IS NOT NULL))
);

3. Class Table Inheritance im Detail

Class Table Inheritance, kurz CTI, geht den entgegengesetzten Weg: Eine Basistabelle enthält alle gemeinsamen Felder, für jeden Subtyp existiert eine eigene Tabelle mit nur den spezifischen Feldern, verknüpft über einen Fremdschlüssel, der gleichzeitig Primärschlüssel der Subtyp-Tabelle ist. Diese Form der Vererbung in der Datenbank entspricht am ehesten der objektorientierten Struktur selbst und vermeidet konsequent NULL-Werte, weil jede Tabelle nur Spalten enthält, die für ihre Zeilen tatsächlich Sinn ergeben.

Der Nachteil von CTI ist der Abfrageaufwand: Um eine vollständige Kreditkartenzahlung mit allen Basisfeldern zu lesen, braucht es einen JOIN zwischen "payment_method" und "credit_card_payment". Für Listenansichten, die alle Zahlungsmethoden unabhängig vom Subtyp zeigen sollen, sind mehrere JOINs mit UNION notwendig, einer pro Subtyp, was mit wachsender Anzahl an Subtypen immer aufwendiger wird. CTI eignet sich deshalb besonders, wenn Subtypen viele eigene Felder haben und meist einzeln, nicht gemeinsam über die Hierarchie hinweg abgefragt werden.


-- Class Table Inheritance: base table plus one table per subtype
CREATE TABLE payment_method (
    payment_method_id  BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id         BIGINT UNSIGNED NOT NULL,
    payment_type         VARCHAR(20) NOT NULL,
    created_at           TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE credit_card_payment (
    payment_method_id  BIGINT UNSIGNED PRIMARY KEY,
    card_number          VARCHAR(19) NOT NULL,
    card_expiry          DATE NOT NULL,
    CONSTRAINT fk_credit_card_payment
        FOREIGN KEY (payment_method_id)
        REFERENCES payment_method (payment_method_id)
        ON DELETE CASCADE
);

CREATE TABLE bank_transfer_payment (
    payment_method_id  BIGINT UNSIGNED PRIMARY KEY,
    iban                 VARCHAR(34) NOT NULL,
    bic                  VARCHAR(11) NOT NULL,
    CONSTRAINT fk_bank_transfer_payment
        FOREIGN KEY (payment_method_id)
        REFERENCES payment_method (payment_method_id)
        ON DELETE CASCADE
);

4. Concrete Table Inheritance im Detail

Concrete Table Inheritance verzichtet vollständig auf eine gemeinsame Basistabelle. Jeder Subtyp bekommt eine eigenständige Tabelle, die sowohl die gemeinsamen als auch die spezifischen Felder enthält, dupliziert über alle Subtyp-Tabellen hinweg. Diese Form der Vererbung in der Datenbank eliminiert jeden JOIN-Aufwand für den Zugriff auf einen einzelnen Subtyp, weil jede Tabelle in sich vollständig ist. Für performancekritische Zugriffe auf genau einen bekannten Subtyp ist das die schnellste Variante.

Der Preis dafür ist Redundanz auf Schemaebene: Jede Änderung an einem gemeinsamen Feld, etwa eine neue Spalte "notes", muss in jeder Subtyp-Tabelle einzeln nachgezogen werden. Ein globaler Fremdschlüssel, der auf "irgendeine Zahlungsmethode unabhängig vom Subtyp" verweisen soll, lässt sich ohne zusätzliche Konstruktion wie eine übergreifende ID-Tabelle nicht sauber abbilden, weil kein gemeinsamer Primärschlüsselraum existiert. Concrete Table Inheritance eignet sich deshalb vor allem, wenn Subtypen selten übergreifend referenziert werden und die Anzahl der Subtypen klein und stabil ist.


-- Concrete Table Inheritance: shared fields duplicated per subtype
CREATE TABLE credit_card_payment (
    credit_card_payment_id  BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id               BIGINT UNSIGNED NOT NULL,
    created_at                 TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    card_number                 VARCHAR(19) NOT NULL,
    card_expiry                 DATE NOT NULL
);

CREATE TABLE bank_transfer_payment (
    bank_transfer_payment_id  BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    customer_id                 BIGINT UNSIGNED NOT NULL,
    created_at                   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    iban                         VARCHAR(34) NOT NULL,
    bic                          VARCHAR(11) NOT NULL
);

5. Discriminator-Spalten und Constraints richtig einsetzen

Die Discriminator-Spalte ist bei Single Table Inheritance das zentrale Steuerungselement und verdient besondere Sorgfalt. Ein VARCHAR mit freien Werten lädt zu Tippfehlern ein, weshalb ein CHECK-Constraint mit einer festen Werteliste, wie im ersten Beispiel gezeigt, Pflicht ist. Bei Datenbanken, die native ENUM-Typen unterstützen, wie PostgreSQL mit CREATE TYPE oder MySQL mit dem ENUM-Spaltentyp, lässt sich derselbe Schutz auch über den Spaltentyp selbst erreichen, was die Absicht im Schema noch klarer macht.

Auch bei Class Table Inheritance ist ein Discriminator in der Basistabelle sinnvoll, selbst wenn er für die referenzielle Integrität nicht zwingend notwendig ist. Er erlaubt, den konkreten Subtyp eines Datensatzes zu bestimmen, ohne alle Subtyp-Tabellen mit LEFT JOIN durchsuchen zu müssen. Diese Spalte sollte durch einen Trigger oder eine Anwendungsschicht konsistent mit der tatsächlich existierenden Subtyp-Zeile gehalten werden, da die Datenbank diese Konsistenz zwischen Discriminator-Wert und tatsächlich befüllter Subtyp-Tabelle ohne zusätzliche Mechanismen nicht automatisch garantiert.


-- PostgreSQL: native ENUM type instead of a plain VARCHAR with CHECK
CREATE TYPE payment_type_enum AS ENUM ('credit_card', 'bank_transfer');

ALTER TABLE payment_method
    ALTER COLUMN payment_type TYPE payment_type_enum
    USING payment_type::payment_type_enum;

-- MySQL: ENUM column type achieves the same protection
CREATE TABLE payment_method (
    payment_method_id  BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
    payment_type         ENUM('credit_card', 'bank_transfer') NOT NULL
);

6. NULL-Handling bei Single Table Inheritance

Der größte praktische Kritikpunkt an Single Table Inheritance ist die Menge an NULL-Werten, die mit jedem weiteren Subtyp wächst. Bei fünf Subtypen mit je drei spezifischen Feldern trägt jede Zeile im schlimmsten Fall zwölf NULL-Werte und nur drei tatsächlich gesetzte Werte. Dieses Muster der Vererbung in der Datenbank verschwendet zwar keinen relevanten Speicherplatz, weil moderne Datenbanksysteme NULL-Werte effizient kodieren, erschwert aber die Lesbarkeit des Schemas erheblich, weil aus der Tabellendefinition allein nicht ersichtlich ist, welche Felder tatsächlich zusammengehören.

CHECK-Constraints, wie im STI-Beispiel gezeigt, mildern das Problem, indem sie zumindest verhindern, dass ein Datensatz mit payment_type gleich credit_card ohne card_number gespeichert wird. Diese Constraints wachsen jedoch linear mit der Anzahl der Subtypen und werden bei mehr als vier oder fünf Subtypen schnell unübersichtlich. In solchen Fällen ist der Wechsel zu Class Table Inheritance meist der bessere Weg, auch wenn er zusätzliche JOINs beim Lesen erfordert.

7. Abfragen und Performance je nach Strategie

Bei Single Table Inheritance ist jede Abfrage, die alle Subtypen umfasst, ein einfaches SELECT ohne JOIN, was sowohl für den Entwickler als auch für den Query-Planer die günstigste Variante ist. Bei Class Table Inheritance braucht dieselbe Abfrage entweder mehrere LEFT JOINs, einen pro Subtyp-Tabelle, oder eine UNION über mehrere gezielte JOINs. Beide Varianten sind spürbar teurer als das einfache SELECT bei STI, besonders wenn die Hierarchie mehr als drei oder vier Subtypen umfasst.

Bei Concrete Table Inheritance ist der Zugriff auf einen einzelnen bekannten Subtyp am schnellsten, weil keine gemeinsame Tabelle involviert ist. Eine Abfrage über die gesamte Hierarchie hinweg erfordert dagegen ein UNION ALL über alle Subtyp-Tabellen, wobei jede Tabelle ihre eigenen Indizes mitbringt und der Query-Planer jede Teilabfrage separat optimieren muss. Für Reporting-Abfragen über die gesamte Hierarchie ist das in der Regel die langsamste der drei Strategien.


-- Querying across the full hierarchy with Class Table Inheritance
SELECT
    pm.payment_method_id,
    pm.payment_type,
    cc.card_number,
    bt.iban
FROM payment_method pm
LEFT JOIN credit_card_payment cc ON cc.payment_method_id = pm.payment_method_id
LEFT JOIN bank_transfer_payment bt ON bt.payment_method_id = pm.payment_method_id
WHERE pm.customer_id = 100;

-- Same query with Concrete Table Inheritance, via UNION ALL
SELECT payment_method_id, 'credit_card' AS payment_type, card_number, NULL AS iban
FROM credit_card_payment WHERE customer_id = 100
UNION ALL
SELECT payment_method_id, 'bank_transfer' AS payment_type, NULL AS card_number, iban
FROM bank_transfer_payment WHERE customer_id = 100;

8. Migration zwischen den Vererbungsstrategien

Ein Wechsel der Vererbungsstrategie ist im laufenden Betrieb aufwendig, aber nicht ungewöhnlich, wenn ein System wächst und die ursprüngliche Wahl nicht mehr passt. Der typische Weg führt von Single Table Inheritance zu Class Table Inheritance, wenn die Anzahl der Subtypen zunimmt und die STI-Tabelle zu breit wird. Die Migration erfolgt in Schritten: Zuerst werden die neuen Subtyp-Tabellen angelegt, dann werden die Daten aus der breiten Tabelle je nach Discriminator-Wert in die passende Subtyp-Tabelle kopiert, erst danach werden die nicht mehr benötigten Spalten aus der Basistabelle entfernt.

Wichtig bei jeder Migration der Vererbung in der Datenbank ist, dass die Anwendungsschicht während der Übergangsphase beide Strukturen gleichzeitig unterstützen muss, meist über ein Feature-Flag oder eine abstrahierende Repository-Schicht. Das endgültige Entfernen der alten Spalten sollte erst erfolgen, nachdem die neue Struktur produktiv validiert wurde, idealerweise mit einem separaten Deployment-Schritt, der die alte Struktur nur noch schreibgeschützt vorhält, bevor sie komplett entfernt wird.

9. Entscheidungskriterien: wann welches Pattern

Die Wahl der richtigen Strategie für Vererbung in der Datenbank hängt von drei Faktoren ab: der Anzahl der Subtypen, der Anzahl subtypspezifischer Felder und der Häufigkeit hierarchieübergreifender Abfragen. Single Table Inheritance passt am besten zu wenigen Subtypen mit wenigen zusätzlichen Feldern und häufigen Abfragen über die gesamte Hierarchie. Class Table Inheritance passt zu vielen Subtypen mit vielen eigenen Feldern, bei denen NULL-Vermeidung und Datenintegrität wichtiger sind als Abfrageeinfachheit.

Concrete Table Inheritance ist die richtige Wahl, wenn Subtypen praktisch nie gemeinsam abgefragt werden und maximale Lesegeschwindigkeit für einzelne Subtypen im Vordergrund steht, etwa bei stark spezialisierten Event-Log-Tabellen. In der Praxis wird häufig ein hybrider Ansatz gewählt: Die am häufigsten gemeinsam abgefragten Subtypen bleiben in einer STI-Tabelle, seltene oder stark abweichende Subtypen bekommen eine eigene Class-Table-Struktur.

Kriterium Single Table Class Table Concrete Table
Abfrage über alle Subtypen Einfaches SELECT Mehrere LEFT JOINs UNION ALL nötig
NULL-Werte Viele, wachsen mit Subtypen Keine Keine
Zugriff auf einen Subtyp Direkt, ungenutzte Spalten Ein JOIN nötig Direkt, kein JOIN
Schema-Wartung bei neuem Feld Eine Tabelle ändern Gezielt eine Tabelle Jede Tabelle einzeln
Passend für Wenige, ähnliche Subtypen Viele, verschiedene Subtypen Isolierte Spezialtabellen

Mironsoft

Datenmodellierung, Schema-Design und Datenbankberatung

Eine Klassenhierarchie, die im Schema nicht mehr passt?

Wir bewerten bestehende Vererbungsstrukturen, zeigen den passenden Migrationspfad und setzen die Umstellung ohne Datenverlust und ohne Downtime für euer laufendes System um.

Schema-Review

Analyse bestehender Hierarchien auf NULL-Dichte und Abfragemuster

Migration

Schrittweiser Wechsel zwischen Vererbungsstrategien ohne Datenverlust

Constraint-Design

CHECK-Constraints und Discriminator-Spalten sauber implementieren

10. Zusammenfassung

Vererbung in der Datenbank lässt sich mit drei etablierten Mustern abbilden, die jeweils unterschiedliche Kompromisse eingehen. Single Table Inheritance ist einfach zu implementieren und abzufragen, erkauft das aber mit wachsenden Mengen an NULL-Werten. Class Table Inheritance vermeidet NULL-Werte konsequent, verlangt dafür JOINs für hierarchieübergreifende Abfragen. Concrete Table Inheritance liefert den schnellsten Zugriff auf einzelne Subtypen, dupliziert aber gemeinsame Felder über alle Subtyp-Tabellen hinweg und erschwert die Schema-Wartung.

Keine der drei Strategien ist universell überlegen, die richtige Wahl hängt von Anzahl und Ähnlichkeit der Subtypen sowie vom typischen Abfragemuster ab. Wer Vererbung in der Datenbank von Anfang an bewusst plant, statt sie zufällig entstehen zu lassen, vermeidet spätere aufwendige Migrationen zwischen den Strategien und behält ein Schema, das sowohl lesbar als auch performant bleibt.

Vererbung in relationalen Datenbanken, das Wichtigste auf einen Blick

Single Table Inheritance

Eine Tabelle, ein Discriminator, einfache Abfragen, wachsende Anzahl NULL-Werte.

Class Table Inheritance

Basistabelle plus Subtyp-Tabellen, keine NULL-Werte, JOINs für gemeinsame Abfragen.

Concrete Table Inheritance

Eigenständige Tabellen pro Subtyp, schnellster Einzelzugriff, doppelte gemeinsame Felder.

Entscheidung

Anzahl der Subtypen, Menge eigener Felder und Häufigkeit hierarchieübergreifender Abfragen abwägen.

11. FAQ: Vererbung in relationalen Datenbanken sauber abbilden

1Was bedeutet Vererbung in einer relationalen Datenbank?
Die Abbildung einer Klassenhierarchie mit gemeinsamen und spezifischen Feldern auf Tabellen, da das relationale Modell kein natives Vererbungskonzept kennt.
2Was ist Single Table Inheritance?
Alle Subtypen liegen in einer Tabelle, eine Discriminator-Spalte markiert den Subtyp, nicht benötigte Spalten bleiben pro Zeile NULL.
3Was ist Class Table Inheritance?
Eine Basistabelle enthält gemeinsame Felder, jeder Subtyp bekommt eine eigene Tabelle, verknüpft über einen Fremdschlüssel, der auch Primärschlüssel ist.
4Was ist Concrete Table Inheritance?
Jeder Subtyp bekommt eine eigenständige Tabelle mit gemeinsamen und spezifischen Feldern, ohne gemeinsame Basistabelle.
5Welches Muster hat die wenigsten NULL-Werte?
Class Table und Concrete Table Inheritance vermeiden NULL-Werte vollständig. Single Table Inheritance erzeugt mit mehr Subtypen zunehmend NULL-Werte.
6Wie schütze ich die Discriminator-Spalte?
Mit einem CHECK-Constraint auf eine feste Werteliste oder mit einem nativen ENUM-Typ, sofern unterstützt.
7Welche Strategie ist am schnellsten für einen Subtyp?
Concrete Table Inheritance, weil kein JOIN nötig ist. Der Nachteil ist doppelte Pflege gemeinsamer Felder über alle Tabellen hinweg.
8Kann ich zwischen Strategien migrieren?
Ja, über schrittweise Migration mit neuer Struktur, Datenkopie, übergangsweiser Doppelunterstützung und Entfernen der alten Struktur nach Validierung.
9Wann Class Table statt Single Table Inheritance?
Sobald viele Subtypen mit vielen eigenen Feldern existieren und die STI-Tabelle unübersichtlich wird.
10Wie frage ich alle Subtypen bei Class Table Inheritance ab?
Über LEFT JOINs auf jede Subtyp-Tabelle oder UNION ALL, beide Varianten sind teurer als das einfache SELECT bei Single Table Inheritance.