Wie Jeffrey Ullmans Kernideen moderne Datenbanken antreiben: relationale Algebra, Umschreibregeln, Joins und compilerähnliche Planung, die Systeme skalierbar machen.

Die meisten, die SQL schreiben, Dashboards bauen oder eine langsame Abfrage tunen, profitieren von Jeffrey Ullmans Arbeit — selbst wenn sie seinen Namen nie gehört haben. Ullman ist Informatiker und Lehrender; seine Forschung und Lehrbücher haben definiert, wie Datenbanken Daten beschreiben, über Abfragen nachdenken und sie effizient ausführen.
Wenn eine Datenbank-Engine dein SQL in etwas übersetzt, das schnell läuft, stützt sie sich auf Ideen, die präzise und anpassbar sein müssen. Ullman half dabei, die Bedeutung von Abfragen zu formalisieren (damit das System sie sicher umschreiben kann) und die Datenbank- mit der Compiler-Denke zu verbinden (damit eine Abfrage geparst, optimiert und in ausführbare Schritte übersetzt wird).
Dieser Einfluss ist unscheinbar, weil er nicht als Button in deinem BI-Tool erscheint. Er zeigt sich als:
JOIN schnell laufenDieser Beitrag nutzt Ullmans Kernideen als Tour durch die Datenbank-Interna, die in der Praxis am meisten zählen: wie relationale Algebra SQL zugrunde liegt, wie Abfrageumschreibungen die Bedeutung bewahren, warum kostenbasierte Optimierer bestimmte Entscheidungen treffen und wie Join-Algorithmen oft entscheiden, ob ein Job Sekunden oder Stunden braucht.
Wir ziehen auch einige compilerähnliche Konzepte heran — Parsen, Umschreiben und Planen — weil Datenbank-Engines mehr wie ausgefeilte Compiler arbeiten, als viele vermuten.
Ein kurzes Versprechen: Wir halten die Darstellung akkurat, aber vermeiden mathe-lastige Beweise. Ziel sind mentale Modelle, die du beim nächsten Performance-, Skalierungs- oder unerwarteten Abfrageverhalten im Job anwenden kannst.
Wenn du je eine SQL-Abfrage geschrieben und erwartet hast, sie „meint nur eine Sache“, verlässt du dich auf Ideen, die Jeffrey Ullman populär gemacht und formalisiert hat: ein klares Datenmodell plus präzise Wege, was eine Abfrage fordert.
Im Kern behandelt das relationale Modell Daten als Tabellen (Relationen). Jede Tabelle hat Zeilen (Tupel) und Spalten (Attribute). Das klingt heute offensichtlich, wichtig ist aber die Disziplin, die daraus entsteht:
Diese Einordnung erlaubt es, Korrektheit und Performance vorhersehbar zu machen. Wenn du weißt, was eine Tabelle repräsentiert und wie Zeilen identifiziert werden, kannst du vorhersagen, was Joins tun sollten, was Duplikate bedeuten und warum bestimmte Filter Ergebnisse verändern.
Ullmans Lehre nutzt oft die relationale Algebra als eine Art Abfrage-Rechner: eine kleine Menge von Operationen (Selektion, Projektion, Join, Union, Differenz), die du kombinieren kannst, um auszudrücken, was du willst.
Warum das für die praktische Arbeit mit SQL wichtig ist: Datenbanken übersetzen SQL in eine algebraische Form und schreiben sie dann in eine andere äquivalente Form um. Zwei Abfragen, die unterschiedlich aussehen, können algebraisch gleich sein — so können Optimierer Joins neu anordnen, Filter nach unten ziehen oder redundante Arbeit entfernen, ohne die Bedeutung zu verändern.
SQL ist überwiegend „was“, Engines optimieren aber häufig mit algebraischen „wie“-Techniken.
SQL-Dialekte unterscheiden sich (Postgres vs. Snowflake vs. MySQL), aber die Grundlagen nicht. Schlüssel, Beziehungen und algebraische Äquivalenzen helfen dir zu erkennen, wann eine Abfrage logisch falsch ist, wann sie nur langsam ist und welche Änderungen die Bedeutung über Plattformen hinweg erhalten.
Die relationale Algebra ist die „Mathematik unter“ SQL: eine kleine Menge von Operatoren, die das gewünschte Ergebnis beschreiben. Ullmans Arbeit hat diese Operatorensicht klar und lehrbar gemacht — und sie ist noch immer das mentale Modell, das die meisten Optimierer verwenden.
Eine Datenbankabfrage lässt sich als Pipeline weniger Bausteine ausdrücken:
WHERE in SQL)SELECT col1, col2)JOIN ... ON ...)UNION)EXCEPT)Weil die Menge klein ist, wird es leichter, über Korrektheit zu argumentieren: Sind zwei algebraische Ausdrücke äquivalent, liefern sie für jeden gültigen Datenbankzustand dieselbe Tabelle.
Nehmen wir eine vertraute Abfrage:
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Konzeptionell ist das:
Beginne mit einem Join von customers und orders: customers ⋈ orders
Selektiere nur Bestellungen über 100: σ(o.total > 100)(...)
Projiziere die gewünschte Spalte: π(c.name)(...)
Das ist nicht die genaue interne Notation jeder Engine, aber die richtige Idee: SQL wird zu einem Operatorbaum.
Viele verschiedene Bäume können dasselbe bedeuten. Filter lassen sich oft früher anwenden (σ vor einem großen Join) und Projektionen können ungenutzte Spalten frühzeitig entfernen (π früher anwenden).
Diese Äquivalenzregeln erlauben es einer Datenbank, deine Abfrage in einen günstigeren Plan umzuschreiben, ohne das Ergebnis zu ändern. Wenn man Abfragen als Algebra sieht, wird „Optimierung“ weniger magisch und mehr regelgesteuerte Umformung.
Wenn du SQL schreibst, führt die Datenbank es nicht „wie geschrieben“ aus. Sie übersetzt deine Anweisung in einen Abfrageplan: eine strukturierte Darstellung der auszuführenden Arbeit.
Ein gutes mentales Modell ist ein Baum von Operatoren. Blätter lesen Tabellen oder Indizes; innere Knoten transformieren und kombinieren Zeilen. Gängige Operatoren sind Scan, Filter/Selektion, Projektion, Join, Gruppierung/Aggregation und Sortierung.
Datenbanken trennen Planung typischerweise in zwei Ebenen:
Ullmans Einfluss zeigt sich in der Betonung auf bedeutungserhaltenden Transformationen: den logischen Plan in vielen Varianten umordnen, ohne die Antwort zu ändern, und dann eine effiziente physische Strategie wählen.
Bevor die finale Ausführungsstrategie gewählt wird, wenden Optimierer algebraische „Aufräum“-Regeln an. Diese Umschreibungen ändern nicht das Ergebnis; sie reduzieren unnötige Arbeit.
Gängige Beispiele:
Angenommen, du willst Bestellungen von Nutzern in einem Land:
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Eine naive Umsetzung könnte alle Nutzer mit allen Bestellungen joinen und anschließend auf Kanada filtern. Eine bedeutungserhaltende Umschreibung zieht den Filter nach unten, sodass der Join weniger Zeilen berührt:
country = 'CA' filternorder_id und total projizierenIn Planungsterminologie versucht der Optimierer Join(Users, Orders) → Filter(country='CA') → Project(order_id,total) in etwas wie Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total) zu verwandeln — gleiche Antwort, weniger Arbeit.
Diese Umschreibungen sind leicht zu übersehen, weil du sie nie tippst — und doch sind sie ein Hauptgrund, warum dasselbe SQL in einer Datenbank schnell und in einer anderen langsam laufen kann.
Wenn du eine SQL-Abfrage ausführst, betrachtet die Datenbank mehrere gültige Wege, dasselbe Ergebnis zu erreichen, und wählt dann den, der am günstigsten eingeschätzt wird. Dieser Entscheidungsprozess heißt kostenbasierte Optimierung — und ist ein sehr praktischer Ort, an dem Ullman-artige Theorie im Alltag sichtbar wird.
Ein Kostenmodell ist ein Bewertungssystem, das der Optimierer benutzt, um Alternativpläne zu vergleichen. Die meisten Engines schätzen Kosten anhand weniger Kernressourcen:
Das Modell muss nicht perfekt sein; es muss oft genug in die richtige Richtung zeigen, um gute Pläne zu wählen.
Bevor er Pläne bewertet, fragt der Optimierer an jedem Schritt: Wie viele Zeilen wird das hier produzieren? Das ist die Kardinalitätsabschätzung.
Wenn du WHERE country = 'CA' filterst, schätzt die Engine, welcher Anteil der Tabelle passt. Beim Join von Kunden und Bestellungen schätzt sie, wie viele Paare aufgrund des Join-Keys übereinstimmen. Diese Zeilenzahlen bestimmen, ob ein Index-Scan besser ist als ein Full-Scan, ein Hash-Join besser als ein Nested-Loop, oder ob ein Sortieren klein oder riesig wird.
Die Schätzungen des Optimierers basieren auf Statistiken: Counts, Wertverteilungen, Nullraten und manchmal Korrelationen zwischen Spalten.
Sind Statistiken veraltet oder fehlen sie, kann die Engine Zeilenzahlen um Größenordnungen falsch schätzen. Ein auf dem Papier günstiger Plan kann in der Realität teuer werden — typische Symptome sind plötzliche Verlangsamungen nach Datenwachstum, „zufällige" Planwechsel oder Joins, die unerwartet auf Festplatte auslagern.
Bessere Schätzungen erfordern oft mehr Aufwand: detailliertere Stats, Sampling oder das Durchprobieren mehrerer Kandidatenpläne. Planung selbst kostet Zeit, insbesondere bei komplexen Abfragen.
Deshalb balancieren Optimierer zwei Ziele:
Wenn du EXPLAIN liest, hilft dir dieses Verständnis: Der Optimierer versucht nicht, „clever“ zu sein — er versucht, vorhersehbar richtig zu sein mit begrenzten Informationen.
Ullmans Arbeit half eine einfache, aber mächtige Idee zu verbreiten: SQL wird nicht so sehr „ausgeführt“ wie übersetzt. Nirgends ist das offensichtlicher als bei Joins. Zwei Abfragen, die dieselben Zeilen liefern, können sich in der Laufzeit massiv unterscheiden, je nachdem, welchen Join-Algorithmus die Engine wählt und in welcher Reihenfolge sie Tabellen joined.
Nested Loop Join ist konzeptionell simpel: für jede Zeile links, finde passende Zeilen rechts. Schnell, wenn die linke Seite klein ist und die rechte Seite einen passenden Index hat.
Hash Join baut eine Hashtabelle aus einem Input (meist dem kleineren) und fragt sie mit dem anderen ab. Stark für große, unsortierte Inputs mit Gleichheitsbedingungen (z. B. A.id = B.id), benötigt aber Speicher; Spill auf die Festplatte kann den Vorteil aufheben.
Merge Join geht zwei Inputs in sortierter Reihenfolge durch. Gut, wenn beide Seiten bereits nach dem Join-Key geordnet sind (z. B. durch Indizes), oder wenn das Sortieren günstig ist.
Bei drei oder mehr Tabellen explodiert die Anzahl möglicher Join-Reihenfolgen. Wenn man zuerst zwei große Tabellen joined, kann dies ein riesiges Zwischenresultat erzeugen, das alles verlangsamt. Besser ist oft, mit dem selektivsten Filter zu starten (wenige Zeilen) und nach außen zu joinen, um Zwischenresultate klein zu halten.
Indizes beschleunigen nicht nur Lookups — sie machen bestimmte Join-Strategien überhaupt erst sinnvoll. Ein Index auf dem Join-Key kann einen teuren Nested Loop in ein schnelles „pro Zeile seek“ verwandeln. Fehlen Indizes oder sind sie unbrauchbar, bevorzugt die Engine vielleicht Hash-Joins oder große Sorts für Merge-Joins.
Datenbanken führen SQL nicht einfach aus — sie kompilieren es. Ullmans Einfluss reicht über Datenbank-Theorie hinaus zur Compiler-Denke, und diese Verbindung erklärt, warum Abfrage-Engines wie Toolchains für Programmiersprachen arbeiten: sie übersetzen, schreiben um und optimieren, bevor sie etwas tun.
Wenn du eine Abfrage sendest, ist der erste Schritt sehr compiler-ähnlich. Die Engine tokenisiert Keywords und Bezeichner, prüft Grammatik und baut einen Parse-Tree (meist vereinfacht zum Abstract Syntax Tree) auf. Hier werden grundlegende Fehler abgefangen: fehlende Kommata, mehrdeutige Spaltennamen, ungültige GROUP-BY-Regeln.
Ein hilfreiches Modell: SQL ist eine Programmiersprache, deren „Programm“ Datenbeziehungen statt Schleifen beschreibt.
Compiler wandeln Syntax in eine Intermediate Representation (IR) um. Datenbanken tun Ähnliches: Sie übersetzen SQL in logische Operatoren wie:
GROUP BY)Diese logische Form ist näher an relationaler Algebra als an SQL-Text und erleichtert das Denken über Bedeutung und Äquivalenz.
Compiler-Optimierungen erhalten das Programmverhalten bei gleichzeitig geringeren Ausführungskosten. Datenbank-Optimierer machen dasselbe, mit Regelwerken wie:
Das ist die Datenbank-Variante von „dead code elimination“: nicht identische Techniken, aber dieselbe Philosophie — Semantik bewahren, Kosten reduzieren.
Wenn deine Abfrage langsam ist, starr nicht nur auf das SQL. Schau dir den Abfrageplan an, wie du Compiler-Output anschauen würdest. Ein Plan sagt dir, was die Engine tatsächlich gewählt hat: Join-Reihenfolge, Index-Nutzung und wo Zeit verbracht wird.
Praktische Folgerung: Lerne EXPLAIN als Leistungssatz zu lesen — das macht Tuning evidenzbasiert statt ratenbasiert. Mehr dazu: /blog/practical-query-optimization-habits.
Gute Abfrage-Performance beginnt oft, bevor du SQL schreibst. Ullmans Schema-Design-Theorie (insbesondere Normalisierung) zielt darauf ab, Daten so zu strukturieren, dass die DB Konsistenz und Vorhersagbarkeit behält, während sie wächst.
Normalisierung will:
Diese Korrektheitsgewinne führen später zu Performancevorteilen: weniger duplizierte Felder, kleinere Indizes und weniger teure Updates.
Du musst keine Beweise kennen, um die Ideen zu nutzen:
Denormalisierung kann sinnvoll sein, wenn:
Wichtig ist, bewusst zu denormalisieren und einen Prozess zu haben, die Duplikate synchron zu halten.
Das Schema beeinflusst, was der Optimierer tun kann. Klare Schlüssel und Fremdschlüssel ermöglichen bessere Join-Strategien, sicherere Umschreibungen und genauere Zeilenschätzungen. Übermäßige Duplikation kann Indizes aufblähen und Schreibvorgänge verlangsamen; mehrwertige Spalten verhindern effiziente Prädikate. Mit wachsendem Datenvolumen spielen diese frühen Modellierungsentscheidungen oft eine größere Rolle als Mikro-Optimierungen einzelner Abfragen.
Wenn ein System skaliert, geht es selten nur darum, größere Maschinen hinzuzufügen. Häufiger ist die Herausforderung, dieselbe Abfragebedeutung beizubehalten, während die Engine eine sehr andere physische Strategie wählt, um Laufzeiten vorhersehbar zu halten. Ullmans Fokus auf formale Äquivalenzen ist genau das, was diese Strategieänderungen erlaubt, ohne Ergebnisse zu verändern.
Bei kleinen Datenmengen funktionieren viele Pläne. Bei Skala kann der Unterschied zwischen Tabellenscan, Indexnutzung oder vorcomputiertem Ergebnis Sekunden vs. Stunden bedeuten. Theorie hilft, weil der Optimierer eine sichere Menge von Umschreiberegeln braucht (z. B. Filter früher ziehen, Joins umordnen), die das Ergebnis nicht verändern — selbst wenn sie die ausgeführte Arbeit radikal verändern.
Partitionierung (nach Datum, Kunde, Region usw.) teilt eine logische Tabelle in viele physische Teile. Das beeinflusst die Planung:
Der SQL-Text bleibt gleich, aber der beste Plan hängt davon ab, wo die Zeilen liegen.
Materialisierte Views sind gespeicherte Zwischenausdrücke. Kann die Engine beweisen, dass deine Abfrage mit einem gespeicherten Ergebnis übereinstimmt (oder auf dieses umgeschrieben werden kann), kann sie teure Arbeit wie wiederholte Joins und Aggregationen durch einen schnellen Lookup ersetzen. Das ist relationale Algebra in der Praxis: Äquivalenz erkennen und wiederverwenden.
Caching beschleunigt wiederholte Lesezugriffe, kann aber eine Abfrage nicht retten, die zu viel Daten scannen, riesige Zwischen-Joins erzeugen oder umfangreiche Shuffles durchführen muss. Bei Skalierungsproblemen ist die Lösung oft: reduziere die Datenmenge (Layout/Partitionierung), reduziere wiederholte Berechnung (materialisierte Views) oder ändere den Plan — nicht nur „mehr Cache".
Ullmans Einfluss zeigt sich in einer einfachen Mentalität: Behandle eine langsame Abfrage als Absichtserklärung, die die Datenbank frei umschreiben darf, und verifiziere dann, was sie tatsächlich getan hat. Du musst kein Theoretiker sein, um davon zu profitieren — du brauchst nur eine wiederholbare Routine.
Beginne mit den Teilen, die normalerweise die Laufzeit dominieren:
Wenn du nur eins tust: finde den ersten Operator, bei dem die Zeilenanzahl explodiert. Das ist meist die Ursache.
Diese sind leicht zu schreiben und erstaunlich teuer:
WHERE LOWER(email) = ... kann Indexnutzung verhindern (verwende eine normalisierte Spalte oder einen funktionalen Index, falls unterstützt).Relationale Algebra führt zu zwei praktischen Schritten:
Eine gute Hypothese könnte lauten: „Dieser Join ist teuer, weil zu viele Zeilen gejoint werden; wenn wir orders vorab auf die letzten 30 Tage filtern, sinkt der Join-Input.“
Ein einfaches Entscheidungsprinzip:
EXPLAIN vermeidbare Arbeit zeigt (unnötige Joins, spätes Filtern, nicht-sargable Prädikate).Ziel ist nicht „cleveres SQL“, sondern vorhersehbar kleinere Zwischenresultate — genau die Art von äquivalenzerhaltenden Verbesserungen, die Ullmans Ideen erleichtern.
Die Konzepte sind nicht nur für DBAs. Wenn du eine Anwendung auslieferst, triffst du Datenbank- und Abfrageplan-Entscheidungen, ob du es merkst oder nicht: Schema-Form, Schlüsselauswahl, Abfragemuster und Data-Access-Layer beeinflussen, was der Optimierer kann.
Wenn du einen vibe-coding-Workflow nutzt (z. B. eine React + Go + PostgreSQL App aus einem Chat-Interface in Koder.ai generierst), sind Ullman-artige Modelle ein praktisches Sicherheitsnetz: Du kannst das generierte Schema auf saubere Keys und Beziehungen prüfen, die Abfragen der App inspizieren und Performance mit EXPLAIN validieren, bevor Probleme in Produktion auftauchen. Je schneller du von „Abfrage-Absicht → Plan → Fix" iterierst, desto mehr Wert bring dir beschleunigte Entwicklung.
Du musst Theorie nicht als eigenes Hobby betreiben. Der schnellste Weg, von Ullman-Grundlagen zu profitieren, ist, gerade genug zu lernen, um Abfragepläne sicher zu lesen — und dann auf deiner eigenen Datenbank zu üben.
Suche nach diesen Büchern und Vorlesungsthemen (keine Affiliation — nur verbreitete Einstiegsquellen):
Beginne klein und binde jeden Schritt an etwas Beobachtbares:
Wähle 2–3 reale Abfragen und iteriere:
IN gegen EXISTS tauschen, Prädikate früher anwenden, unnötige Spalten entfernen und Ergebnisse vergleichen.Nutze klare, plan-basierte Sprache:
Das ist der praktische Gewinn von Ullmans Grundlagen: Du erhältst ein gemeinsames Vokabular, um Performance zu erklären — ohne zu raten.
Jeffrey Ullman half dabei zu formalisieren, wie Datenbanken die Bedeutung von Abfragen darstellen und wie sie Abfragen sicher in schnellere, äquivalente Formen transformieren können. Dieses Fundament zeigt sich immer dann, wenn eine Engine eine Abfrage umschreibt, Joins neu anordnet oder einen anderen Ausführungsplan wählt — und dabei garantiert, dass das Ergebnis gleich bleibt.
Relationale Algebra ist eine kleine Menge von Operatoren (Select/Selektion, Project/Projektion, Join, Union, Difference), die genau beschreiben, welches Ergebnis eine Abfrage liefern soll. Engines übersetzen SQL häufig in einen algebraähnlichen Operatorbaum, um Äquivalenzregeln (z. B. Filter früher anwenden) anzuwenden, bevor sie eine Ausführungsstrategie wählen.
Weil die Optimierung darauf basiert, dass eine umgeschriebene Abfrage das gleiche Ergebnis liefert. Äquivalenzregeln erlauben dem Optimierer, beispielsweise:
WHERE-Filter vor einen Join zu ziehenSolche Änderungen können den Aufwand drastisch reduzieren, ohne die Bedeutung zu verändern.
Ein logischer Plan beschreibt was zu berechnen ist (Filter, Join, Aggregation) unabhängig von Speicher- oder Hardwaredetails. Ein physischer Plan beschreibt wie das ausgeführt wird (Index-Scan vs. Full-Scan, Hash-Join vs. Nested-Loop, Parallelität, Sort-Strategien). Die meisten Performance-Unterschiede ergeben sich aus den physischen Entscheidungen, die durch logische Umschreibungen ermöglicht werden.
Cost-based Optimization vergleicht mehrere gültige Pläne und wählt den mit den geringsten geschätzten Kosten. Kosten werden typischerweise durch praktische Faktoren getrieben: verarbeitete Zeilen, I/O, CPU und Speicher (einschließlich ob Hash/Sort auf die Festplatte auslagert).
Cardinality Estimation ist die Schätzung des Optimierers, „wie viele Zeilen an diesem Schritt herauskommen“. Diese Schätzungen bestimmen Join-Reihenfolge, Join-Typ und ob ein Index-Scan sinnvoll ist. Wenn Schätzungen falsch sind (häufig wegen veralteter/fehlender Statistiken), können plötzliche Verlangsamungen, große Auslagerungen oder überraschende Planwechsel auftreten.
Konzentriere dich auf ein paar hohe Signale:
Behandle den Plan wie kompilierten Code: er zeigt, was die Engine tatsächlich entschieden hat.
Normalisierung reduziert duplizierte Fakten und Update-Anomalien, was oft kleinere Tabellen und Indizes sowie verlässlichere Joins bedeutet. Denormalisierung kann für Analytics oder bei stark leseorientierten Mustern sinnvoll sein, muss aber bewusst erfolgen (Klare Aktualisierungsregeln, bekannte Redundanz), damit die Korrektheit nicht leidet.
Skalieren bedeutet oft, die gleiche Abfragebedeutung beizubehalten, während die Engine eine andere physische Strategie wählt. Typische Werkzeuge sind:
Caching hilft bei wiederholten Leseoperationen, kann aber eine Abfrage, die zu viel Daten berührt oder riesige Zwischen-Joins erzeugt, nicht dauerhaft retten.