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.
Inhaltsverzeichnis
- 1. Das Problem: OOP-Vererbung trifft relationales Modell
- 2. Single Table Inheritance im Detail
- 3. Class Table Inheritance im Detail
- 4. Concrete Table Inheritance im Detail
- 5. Discriminator-Spalten und Constraints richtig einsetzen
- 6. NULL-Handling bei Single Table Inheritance
- 7. Abfragen und Performance je nach Strategie
- 8. Migration zwischen den Vererbungsstrategien
- 9. Entscheidungskriterien: wann welches Pattern
- 10. Zusammenfassung
- 11. FAQ
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.