Polymorphic Associations in SQL modellieren
AI generated
SELECT
JOIN
SQL · Datenmodellierung · Polymorphic Associations
Polymorphic Associations in SQL modellieren
warum ein generischer Fremdschluessel selten die richtige Antwort ist

Eine Polymorphic Association verspricht eine einzige Kommentar-Tabelle fuer Beitraege, Fotos und Videos gleichzeitig, doch der generische Fremdschluessel dahinter bricht die referentielle Integritaet der Datenbank. Dieser Beitrag zeigt, wie das klassische type-plus-id-Muster entsteht, warum es zu verwaisten Datensaetzen fuehrt, und mit welchen drei Alternativen sich dasselbe Problem loesen laesst, ohne auf Fremdschluessel-Constraints zu verzichten.

17 Min. Lesezeit Exclusive Arc · Junction Tables · Shared Supertype PostgreSQL · MySQL · SQL Server

1. Was eine Polymorphic Association ist und wo sie entsteht

Eine Polymorphic Association ist ein Spalten-Muster, bei dem eine Tabelle auf Zeilen in mehreren unterschiedlichen Elterntabellen verweisen kann, ohne dass ein einzelner dedizierter Fremdschluessel dafuer existiert. Statt einer Spalte post_id, die ausschliesslich auf posts zeigt, bekommt die Tabelle eine Kombination aus einer Typ-Spalte wie commentable_type und einer generischen Id-Spalte wie commentable_id. Je nach Inhalt der Typ-Spalte bezieht sich dieselbe Id-Spalte mal auf posts, mal auf photos, mal auf videos.

Das typische Beispiel ist eine Kommentarfunktion: Beitraege, Fotos und Videos sollen alle kommentierbar sein, aber niemand moechte drei fast identische Tabellen post_comments, photo_comments und video_comments pflegen. Eine Polymorphic Association loest das scheinbar elegant, indem eine einzige comments-Tabelle fuer alle drei Faelle zustaendig ist. Auch Tagging-Systeme, generische Anhaenge oder Aktivitaets-Feeds greifen haeufig auf dieses Muster zurueck, weil es auf den ersten Blick Redundanz vermeidet.

Viele ORMs wie Active Record oder Eloquent generieren dieses Muster automatisch, sobald ein Entwickler eine Beziehung als "polymorphic" deklariert. Das macht es auf Anwendungsebene bequem: ein Modell, eine Methode, ein Query-Interface fuer alle Zieltypen. Der Preis fuer diese Bequemlichkeit wird jedoch in der Datenbank bezahlt, denn genau dort, wo relationale Systeme am staerksten sind, naemlich beim Erzwingen von Integritaetsregeln, verliert eine Polymorphic Association ihre Wirkung fast vollstaendig.

2. Das klassische Muster: type-Spalte und generische id-Spalte

Die Grundform einer Polymorphic Association ist schnell erklaert: Eine Text- oder Enum-Spalte speichert den Namen der Zieltabelle beziehungsweise des Zielmodells, eine Integer-Spalte speichert den Primaerschluessel der referenzierten Zeile. Anwendungscode entscheidet zur Laufzeit, anhand des Werts in der Typ-Spalte, gegen welche Tabelle die Id tatsaechlich aufgeloest werden muss. Die Datenbank selbst kennt diese Logik nicht, sie sieht lediglich zwei unabhaengige Spalten ohne erzwungenen Zusammenhang.


-- Klassisches Polymorphic-Association-Schema, ohne echten Fremdschluessel
CREATE TABLE comments (
  comment_id       SERIAL PRIMARY KEY,
  commentable_type VARCHAR(50)  NOT NULL,   -- 'post', 'photo', 'video'
  commentable_id   INTEGER      NOT NULL,   -- verweist je nach Typ auf posts, photos oder videos
  author_id        INTEGER      NOT NULL REFERENCES users(user_id),
  body             TEXT         NOT NULL,
  created_at       TIMESTAMPTZ  NOT NULL DEFAULT now()
);

-- Anwendungscode muss selbst wissen, welche Tabelle gemeint ist
-- SELECT * FROM comments WHERE commentable_type = 'post' AND commentable_id = 42;
-- SELECT * FROM comments WHERE commentable_type = 'photo' AND commentable_id = 17;

-- Die Datenbank kann NICHT pruefen, ob commentable_id = 42
-- tatsaechlich in der durch commentable_type benannten Tabelle existiert

Auf den ersten Blick wirkt das Muster attraktiv: Ein vierter kommentierbarer Typ, etwa product, laesst sich einfuehren, ohne eine einzige Migration zu schreiben. Es reicht, Zeilen mit dem neuen Typ-String einzufuegen. Diese scheinbare Flexibilitaet ist jedoch truegerisch, denn die Typ-Spalte ist reiner Text ohne jede Absicherung. Ein Tippfehler wie 'Post' statt 'post', ein umbenanntes Modell in der Anwendung oder eine vergessene Migration bei einem Refactoring bleiben von der Datenbank vollstaendig unbemerkt, obwohl sie Daten praktisch unauffindbar machen.

3. Warum dieses Muster referentielle Integritaet bricht

Der fundamentale Grund, warum eine Polymorphic Association mit klassischen Mitteln nicht sauber abgesichert werden kann, liegt in der SQL-Sprache selbst: Ein Fremdschluessel-Constraint bezieht sich immer auf genau eine Zieltabelle. Es gibt keine SQL-Syntax, die aussagt, "diese Spalte referenziert entweder Tabelle A, B oder C, abhaengig vom Wert einer anderen Spalte". Die Datenbank-Engine kann deshalb niemals pruefen, ob commentable_id tatsaechlich in der durch commentable_type benannten Tabelle existiert.

Ein konkretes Fehlerbild zeigt sich beim Loeschen: Wird ein Beitrag geloescht, etwa durch ein manuelles Aufraeum-Skript oder einen Bug in der Loeschroutine, bleiben die zugehoerigen Kommentare unveraendert in der comments-Tabelle stehen. Nichts in der Datenbank markiert diese Zeilen als ungueltig, weil kein Fremdschluessel existiert, der bei ON DELETE CASCADE greifen koennte. Die Anwendung zeigt in Listenansichten ploetzlich Kommentare zu einem Beitrag, den es gar nicht mehr gibt, oder ein Batch-Job stuerzt ab, weil ein Join gegen die falsche Tabelle keine Zeile findet.

Solche verwaisten Datensaetze fallen selten sofort auf. Sie sammeln sich unbemerkt an, insbesondere nach Datenmigrationen, teilweisen Loeschungen oder Import-Jobs, die eine Tabelle, aber nicht die andere anfassen. Wochen oder Monate spaeter zeigen sich die Folgen als kaputte Links, leere Detailseiten oder falsche Zaehler in Reports, waehrend die urspruengliche Ursache, eine ungeprüfte Polymorphic Association, laengst nicht mehr offensichtlich ist.

4. Alternative 1: Exclusive Arc mit mehreren nullable Fremdschluesseln

Die erste Alternative ersetzt die generische Id-Spalte durch mehrere spezifische, nullable Fremdschluessel-Spalten, eine pro moeglichem Zieltyp. Ein CHECK-Constraint stellt sicher, dass in jeder Zeile genau eine dieser Spalten gesetzt ist, waehrend alle anderen NULL bleiben. Dieses Muster heisst Exclusive Arc, weil von der Kommentar-Zeile aus gesehen genau eine der moeglichen Beziehungen aktiv sein darf.


-- Exclusive Arc: echte Fremdschluessel, aber pro Zieltyp eine eigene Spalte
CREATE TABLE comments (
  comment_id  SERIAL PRIMARY KEY,
  post_id     INTEGER REFERENCES posts(post_id)   ON DELETE CASCADE,
  photo_id    INTEGER REFERENCES photos(photo_id) ON DELETE CASCADE,
  video_id    INTEGER REFERENCES videos(video_id) ON DELETE CASCADE,
  author_id   INTEGER NOT NULL REFERENCES users(user_id),
  body        TEXT    NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now(),

  -- Genau eine der drei Spalten muss gesetzt sein, die anderen NULL
  CONSTRAINT exactly_one_parent CHECK (
    (post_id IS NOT NULL)::int +
    (photo_id IS NOT NULL)::int +
    (video_id IS NOT NULL)::int = 1
  )
);

Der Vorteil liegt auf der Hand: Jede Spalte hat einen echten Fremdschluessel, die Datenbank verweigert das Einfuegen einer ungueltigen Referenz und loescht abhaengige Kommentare automatisch per CASCADE, sobald der Elterndatensatz verschwindet. Der Nachteil ist Skalierbarkeit: Jeder neue kommentierbare Typ erfordert eine echte Schema-Migration mit einer zusaetzlichen Spalte und einer Anpassung des CHECK-Constraints. Bei drei oder vier Typen bleibt das uebersichtlich, bei einem Dutzend wird die Tabelle unhandlich breit.

5. Alternative 2: separate Zuordnungstabellen pro Zieltyp

Die zweite Alternative trennt Inhalt und Zuordnung vollstaendig: Die comments-Tabelle enthaelt nur noch generische Kommentarinformationen ohne jeden Bezug zu einem Elterntyp. Fuer jede Beziehung existiert eine eigene, schlanke Zuordnungstabelle mit zwei echten Fremdschluesseln, die die Verbindung zwischen Kommentar und konkretem Zieltyp herstellt.


-- Basis-Tabelle ohne jeden Bezug zu einem Elterntyp
CREATE TABLE comments (
  comment_id  SERIAL PRIMARY KEY,
  author_id   INTEGER NOT NULL REFERENCES users(user_id),
  body        TEXT    NOT NULL,
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Eine Zuordnungstabelle pro Zieltyp, jeweils mit echten Fremdschluesseln
CREATE TABLE post_comments (
  comment_id  INTEGER PRIMARY KEY REFERENCES comments(comment_id) ON DELETE CASCADE,
  post_id     INTEGER NOT NULL REFERENCES posts(post_id) ON DELETE CASCADE
);

CREATE TABLE photo_comments (
  comment_id  INTEGER PRIMARY KEY REFERENCES comments(comment_id) ON DELETE CASCADE,
  photo_id    INTEGER NOT NULL REFERENCES photos(photo_id) ON DELETE CASCADE
);

-- Abfrage: alle Kommentare zu einem bestimmten Beitrag
SELECT c.* FROM comments c
JOIN post_comments pc ON pc.comment_id = c.comment_id
WHERE pc.post_id = 42;

Dieses Muster erzwingt vollstaendige referentielle Integritaet in beide Richtungen, ohne die Basistabelle mit ungenutzten nullable Spalten aufzublaehen. Ein neuer Zieltyp bedeutet lediglich eine neue, kleine Zuordnungstabelle, ohne die bestehende comments-Tabelle anzufassen. Der Nachteil ist zusaetzlicher Join-Aufwand bei jeder Abfrage sowie die Notwendigkeit, per Anwendungslogik oder zusaetzlichem Constraint sicherzustellen, dass ein Kommentar nicht gleichzeitig in mehreren Zuordnungstabellen auftaucht.

6. Alternative 3: gemeinsame Supertype-Tabelle als Dispatch-Ebene

Die dritte Alternative loest das Problem an der Wurzel, indem sie eine duenne gemeinsame Tabelle einfuehrt, auf die alle kommentierbaren Entitaeten per eigenem Fremdschluessel verweisen. Diese Supertype-Tabelle traegt selbst kaum fachliche Daten, sie dient ausschliesslich als Dispatch-Ebene: ein einziger Primaerschluessel, den die comments-Tabelle jetzt mit einem einzigen, echten Fremdschluessel referenzieren kann, weil es nur noch ein Zielobjekt gibt.


-- Duenne Supertype-Tabelle: nur Dispatch, keine fachlichen Daten
CREATE TABLE commentables (
  commentable_id   SERIAL PRIMARY KEY,
  entity_type      VARCHAR(20) NOT NULL CHECK (entity_type IN ('post','photo','video'))
);

-- Jede Subtype-Tabelle referenziert die Supertype-Tabelle 1:1
CREATE TABLE posts (
  post_id         INTEGER PRIMARY KEY REFERENCES commentables(commentable_id),
  title           VARCHAR(255) NOT NULL,
  body            TEXT
);

CREATE TABLE photos (
  photo_id        INTEGER PRIMARY KEY REFERENCES commentables(commentable_id),
  storage_path    VARCHAR(500) NOT NULL
);

-- comments referenziert nur noch EINE Tabelle mit echtem Fremdschluessel
CREATE TABLE comments (
  comment_id      SERIAL PRIMARY KEY,
  commentable_id  INTEGER NOT NULL REFERENCES commentables(commentable_id) ON DELETE CASCADE,
  author_id       INTEGER NOT NULL REFERENCES users(user_id),
  body            TEXT    NOT NULL,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

Damit ist die Polymorphic Association aus Sicht der Datenbank keine polymorphe Beziehung mehr, sondern eine ganz normale, vollstaendig ueberpruefbare Fremdschluessel-Beziehung zu genau einer Tabelle. Wird ein Beitrag geloescht, entfernt ON DELETE CASCADE automatisch den Eintrag in commentables und damit alle daran haengenden Kommentare. Der Preis ist ein zusaetzlicher Join, wenn Anwendungscode vom Kommentar aus wissen will, ob das Ziel ein Beitrag oder ein Foto ist, sowie eine gewisse Disziplin beim Anlegen neuer Subtype-Tabellen, die konsequent ueber die Supertype-Tabelle verknuepft werden muessen.

7. Performance und Indexierung bei polymorphen Fremdschluesseln

Unabhaengig davon, welches Muster gewaehlt wird, hat die Indexierung direkten Einfluss auf die Abfragegeschwindigkeit. Beim klassischen type-plus-id-Muster ist ein einzelner Index auf commentable_id allein nutzlos, weil derselbe Id-Wert bei unterschiedlichen Typen mehrfach vorkommen kann. Notwendig ist ein zusammengesetzter Index ueber beide Spalten in der Reihenfolge (commentable_type, commentable_id), damit der Datenbank-Planer beide Filterbedingungen gemeinsam nutzen kann.


-- Zusammengesetzter Index ist bei der klassischen Polymorphic Association Pflicht
CREATE INDEX idx_comments_commentable
  ON comments (commentable_type, commentable_id);

-- Ohne diesen Index scannt die Datenbank moeglicherweise die gesamte Tabelle,
-- weil commentable_id allein keine eindeutige Filterung ermoeglicht
EXPLAIN ANALYZE
SELECT * FROM comments
WHERE commentable_type = 'post' AND commentable_id = 42;

-- Bei stark ungleich verteilten Typen (z.B. 95% 'post', 5% 'video')
-- kann der Planer bei seltenen Typen auf einen Bitmap-Scan wechseln,
-- bei haeufigen Typen aber einen vollen Tabellenscan bevorzugen

Ein weiterer Performance-Aspekt betrifft Abfragen, die ueber alle Zieltypen hinweg aggregieren sollen, etwa "zeige die zehn zuletzt kommentierten Objekte, egal ob Beitrag, Foto oder Video". Bei der klassischen Polymorphic Association erfordert das oft mehrere separate Abfragen mit anschliessendem UNION ALL, da ein einzelner Join gegen mehrere moegliche Zieltabellen in Standard-SQL nicht existiert. Die Supertype-Loesung aus Abschnitt 6 vereinfacht genau diesen Fall, weil ein einziger Join gegen die Dispatch-Tabelle bereits ausreicht, um Metadaten zu allen Typen gemeinsam zu laden.

8. Anwendungsseitige Konsistenzpruefung als Kompromiss

In manchen Projekten bleibt die klassische Polymorphic Association trotz ihrer Schwaechen die pragmatische Wahl, etwa in fruehen Prototyping-Phasen, bei sehr haeufig wechselnden Zieltypen oder wenn ein bestehendes ORM-Framework das Muster fest vorschreibt. In diesen Faellen lohnt sich zumindest ein minimaler Kompromiss: Ein CHECK-Constraint auf die Typ-Spalte, der nur bekannte Werte zulaesst, verhindert wenigstens Tippfehler und stille Drift durch umbenannte Modelle.

Ergaenzend hilft ein regelmaessig laufender Konsistenz-Check, meist als naechtlicher Batch-Job, der pro Typ prueft, ob alle referenzierten Ids tatsaechlich noch existieren, und Abweichungen an ein Monitoring-System meldet. Das ersetzt keine echte referentielle Integritaet, verhindert aber, dass verwaiste Datensaetze monatelang unbemerkt bleiben. Wer neue Projekte plant, sollte dennoch von Anfang an eine der drei Alternativen aus den Abschnitten vier bis sechs bevorzugen, weil nachtraegliches Aufraeumen einer historisch gewachsenen Polymorphic Association deutlich aufwendiger ist als die richtige Wahl beim ersten Entwurf.

9. Polymorphic Associations im Vergleich

Die folgende Tabelle stellt die vier besprochenen Ansaetze fuer eine Polymorphic Association nebeneinander, bewertet nach referentieller Integritaet, Aufwand bei einem neuen Zieltyp und der Komplexitaet typischer Abfragen.

Muster Referentielle Integritaet Aufwand neuer Typ Join-Komplexitaet
Klassisch (type + id) Keine Sehr gering, nur neuer String Anwendungsseitiges Dispatching noetig
Exclusive Arc Vollstaendig Migration plus CHECK-Anpassung Gering, direkte Fremdschluessel
Zuordnungstabellen Vollstaendig Neue kleine Tabelle Ein zusaetzlicher Join pro Typ
Shared Supertype Vollstaendig Neue Subtype-Tabelle mit FK Ein Join gegen Dispatch-Tabelle

10. Zusammenfassung

Eine Polymorphic Association loest ein reales Modellierungsproblem, naemlich eine Tabelle mit mehreren moeglichen Elterntypen zu verbinden, aber das klassische type-plus-id-Muster bezahlt diese Flexibilitaet mit dem Verlust referentieller Integritaet. Kein SQL-Fremdschluessel kann gegen mehrere Zieltabellen gleichzeitig pruefen, wodurch verwaiste Datensaetze, kaputte Links und stille Dateninkonsistenzen entstehen koennen, die oft erst spaet auffallen.

Exclusive Arc, separate Zuordnungstabellen und eine gemeinsame Supertype-Tabelle loesen dasselbe fachliche Problem mit echten, von der Datenbank ueberpruefbaren Fremdschluesseln. Welche der drei Alternativen passt, haengt von der Anzahl der Zieltypen, der Aenderungsfrequenz und den typischen Abfragemustern ab. Wer eine Polymorphic Association dennoch bewusst einsetzt, sollte zumindest CHECK-Constraints auf die Typ-Spalte und regelmaessige Konsistenzpruefungen einplanen, um die groessten Risiken abzufedern.

Polymorphic Associations in SQL, das Wichtigste auf einen Blick

Das Grundproblem

Ein Fremdschluessel kann immer nur genau eine Zieltabelle referenzieren, niemals mehrere gleichzeitig ueber eine Typ-Spalte.

Exclusive Arc

Mehrere nullable Fremdschluessel-Spalten plus CHECK-Constraint fuer genau eine gesetzte Spalte pro Zeile.

Zuordnungstabellen

Basistabelle ohne Elternbezug, pro Zieltyp eine eigene, schlanke Junction-Tabelle mit echten Fremdschluesseln.

Shared Supertype

Duenne Dispatch-Tabelle als gemeinsames Ziel, dadurch wird aus der polymorphen Beziehung eine ganz normale.

11. FAQ: Polymorphic Associations in SQL modellieren

1Was ist eine Polymorphic Association?
Eine Typ-Spalte plus generische Id-Spalte, die eine Tabelle auf Zeilen in mehreren unterschiedlichen Elterntabellen verweisen laesst.
2Warum kann die Datenbank sie nicht absichern?
Ein Fremdschluessel referenziert immer genau eine Tabelle, es gibt keine Syntax fuer wechselnde Zieltabellen abhaengig von einer Typ-Spalte.
3Was ist ein Exclusive Arc?
Mehrere nullable Fremdschluessel-Spalten mit CHECK-Constraint, der genau eine gesetzte Spalte pro Zeile erzwingt.
4Wann separate Zuordnungstabellen?
Bei vielen oder haeufig wechselnden Zieltypen, weil jede Zuordnungstabelle klein bleibt statt die Basistabelle zu verbreitern.
5Was macht eine Shared-Supertype-Tabelle?
Sie ist gemeinsames Ziel aller Subtypen, dadurch braucht die abhaengige Tabelle nur einen einzigen echten Fremdschluessel.
6Welcher Index ist notwendig?
Ein zusammengesetzter Index auf (type, id), weil die Id-Spalte allein keine eindeutige Filterung erlaubt.
7Wie erkennt man verwaiste Kommentare?
Nur mit einem zusaetzlichen Konsistenz-Check-Job, die Datenbank selbst meldet solche Waisen nicht automatisch.
8Ersetzt CHECK einen Fremdschluessel?
Nein, CHECK verhindert nur unbekannte Typ-Werte, prueft aber nicht die Existenz der referenzierten Zeile.
9Warum braucht man oft UNION ALL?
Weil ein einzelner Join nicht gegen mehrere Zieltabellen gleichzeitig arbeiten kann, Abfragen ueber alle Typen muessen per UNION ALL zusammengefuehrt werden.
10Lohnt sich eine Migration weg davon?
Bei kleinen stabilen Typmengen ja, insbesondere Richtung Shared Supertype. Bei sehr volatilen Typen ist ein Kompromiss oft pragmatischer.