Wer in einem 200-Zeilen-SELECT-Statement nicht erkennen kann, wo die WHERE-Klausel anfängt und wo sie aufhört, hat ein Strukturproblem — kein Inhaltsproblem. Dieser Artikel zeigt das Layout, das auch lange Statements navigierbar hält.
→ Teil einer Reihe. Dieser Artikel ist Teil 2 und behandelt den Statement-Aufbau (
SELECT,WHERE,FROM,JOIN). Die Bezeichner-, Delimiter-, Komma- und Alias-Grundlagen stehen in Teil 1 — Bezeichner, Delimiter, Kommata, Aliase.
TL;DR — was dieser Artikel liefert:
- Hauptelemente (
SELECT,FROM,WHERE,GROUP BY,HAVING,ORDER BY) gehören in separate Zeilen mit konsistenter Einrückung. WHERE-Klausel — Operanden und Operatoren spaltenartig ausrichten, gleichwertige Constraints gleich eingerückt; die Klammer-Struktur wird visuell lesbar.FROM-Klausel — Tabelle direkt hinter demJOIN-Operator,ON-Schlüsselwort in eigene Zeile, JOIN-Constraints wie eine Mini-WHERE-Klausel.- Postgres-Brücke + Auto-Formatter am Ende —
[brackets]vs."quotes",DISTINCT ON,LATERAL; sqlfluff und pgFormatter als Ergänzung zur manuellen Disziplin, kein Ersatz.
Voraussetzung: SSMS oder ein anderer SQL-Editor mit konfigurierbarer Tab-Weite reicht; eine lebende AdventureWorks-Datenbank ist nicht erforderlich — die Beispiele zeigen Pattern, keine ausführbare Pipelines.
Allgemeines zur Einrückung
Ein Analogon zur SQL-Struktur ist die Gliederung eines Inhaltsverzeichnisses. Die folgende eingerückte Version ist gegenüber der flachen Variante (siehe weiter unten) deutlich schneller zu erfassen:
1. Kapitel der ersten Ebene
1.1. Kapitel der zweiten Ebene
1.1.1. Kapitel der dritten Ebene
1.1.2. Kapitel der dritten Ebene
1.2. Kapitel der zweiten Ebene
1.2.1. Kapitel der dritten Ebene
1.2.2. Kapitel der dritten Ebene
1.2.3. Kapitel der dritten Ebene
1.2.4. Kapitel der dritten Ebene
1.3. Kapitel der zweiten Ebene
1.3.1. Kapitel der dritten Ebene
1.3.2. Kapitel der dritten Ebene
1.3.3. Kapitel der dritten Ebene
2. Kapitel der ersten Ebene
2.1. Kapitel der zweiten Ebene
2.1.1. Kapitel der dritten Ebene
2.1.2. Kapitel der dritten Ebene
2.2. Kapitel der zweiten Ebene
2.2.1. Kapitel der dritten Ebene
2.2.2. Kapitel der dritten Ebene
Zum Vergleich ein Inhaltsverzeichnis ohne Einrückung
1. Kapitel der ersten Ebene
1.1. Kapitel der zweiten Ebene
1.1.1. Kapitel der dritten Ebene
1.1.2. Kapitel der dritten Ebene
1.2. Kapitel der zweiten Ebene
1.2.1. Kapitel der dritten Ebene
1.2.2. Kapitel der dritten Ebene
1.2.3. Kapitel der dritten Ebene
1.2.4. Kapitel der dritten Ebene
1.3. Kapitel der zweiten Ebene
1.3.1. Kapitel der dritten Ebene
1.3.2. Kapitel der dritten Ebene
1.3.3. Kapitel der dritten Ebene
2. Kapitel der ersten Ebene
2.1. Kapitel der zweiten Ebene
2.1.1. Kapitel der dritten Ebene
2.1.2. Kapitel der dritten Ebene
2.2. Kapitel der zweiten Ebene
2.2.1. Kapitel der dritten Ebene
2.2.2. Kapitel der dritten Ebene
Linksbündig ausgerichtete Inhaltsverzeichnisse funktionieren auch — dann allerdings über zusätzliche Formatierungs-Optionen wie Groß-/Klein-Schreibung, Fett oder Kursiv für die unterschiedlichen Ebenen. Im SQL-Editor sind solche Optionen typisch nicht verfügbar (SSMS rendert Plain-Text mit Syntax-Highlighting, kein Fett für Bezeichner). Daher bleibt als strukturierendes Mittel die Einrückung.
Grundlegende Sprachelemente
Ein SELECT-Statement besteht aus den folgenden grundlegenden Klauseln:
SELECTFROMWHEREGROUP BYHAVINGORDER BY
Betrachtet man diese als Elemente der ersten Ebene, sind die jeweils zulässigen Sprachelemente der zweiten Ebene gemäß dem Indentation-Prinzip einzurücken. Daraus ergibt sich die folgende grundlegende Struktur eines SQL-Statements:
1: SELECT
2: Feldliste
3: FROM
4: Datenquellen
5: WHERE
6: Bedingungen auf Datenquelle
7: GROUP BY
8: Gruppierungsfelder
9: HAVING
10: Bedingungen auf Aggregationen
11: ORDER BY
12: Sortier-Felder
In jedem Fall sollten die Hauptelemente eines SQL-Statements in separaten Zeilen notiert werden.
Als Abgrenzung hierzu möchte ich zwei Beispiele für Formatierungen geben, die häufig zu finden sind und die diese klare Strukturierung nicht berücksichtigen. In beiden Beispielen ist der Leser des Statements dazu gezwungen das Statement zumindest in Teilen zu lesen, um zu erfassen, wo ein Hauptelement beginnt und wo er aufhört.
Linksbündige Ausrichtung von Haupt- und Unterelementen
Gelegentlich findet man, dass Elemente der obersten Ebene und die Elemente der nächsten Ebene gleichermaßen eingerückt sind. Zu beobachten ist das insbesondere innerhalb der FROM-Klausel: die Datenquellen (Tabellen, Views, CTEs) sind genauso eingerückt wie das einleitende Schlüsselwort FROM:
1: SELECT
2: Feldliste
3: FROM
4: Tabelle1
5: JOIN Tabelle2 ON [...]
6: JOIN Tabelle2 ON [...]
7: WHERE
8: Bedingungen auf Datenquelle
9: GROUP BY
10: Gruppierungsfelder
11: HAVING
12: Bedingungen auf Aggregationen
13: ORDER BY
14: Sortier-Felder
Rechtsbündige Ausrichtung von Schlüsselwörtern
In diesem Beispiel sind die grundlegenden Klauseln des SELECT-Statements — ohne Berücksichtigung des Schlüsselwortes BY — rechtsbündig ausgerichtet. Durch diese Art der Einrückung entsteht zusätzlich ein erhöhter Aufwand für die Ausrichtung der Elemente, da man mit unterschiedlich starken Einrückungen arbeiten muss.
1: SELECT Feld1, Feld2, Feld3
2: FROM Tabelle1
3: LEFT JOIN Tabelle2 ON [...]
4: LEFT JOIN Tabelle2 ON [...]
5: WHERE Bedingung1
6: OR Bedingung2
7: OR Bedingung3
8: GROUP BY Gruppierungsfelder
9: HAVING Bedingung1
10: OR Bedingung2
11: OR Bedingung3
12: ORDER BY Sortier-Felder
13: WHERE Klausel
SELECT Feldliste
Die natürliche Leserichtung eines SQL-Statements ist von links nach rechts und von oben nach unten. Mit Tastatur und Maus geht die vertikale Navigation um einiges leichter als die horizontale Navigation. Das Mausrad und die Bild auf/Bild ab-Tasten erlauben eine schnelle vertikale Navigation auch innerhalb langer komplexer Statements, wenn Feldlisten untereinander geschrieben werden.
Feldnamen sollten daher als vertikale Liste mit vorangestellten Kommas geschrieben werden — die ausführliche Begründung (Komma-Lesbarkeit, Blockauswahl-Pattern) steht in Teil 1, Abschnitt „Das Komma“. Je Zeile ist nur ein Feld zu notieren. Da die Feldliste dem Schlüsselwort SELECT logisch untergeordnet ist, sind die Feldnamen entsprechend der vereinbarten Einrückungsweite einzurücken.
1: SELECT
2: Feld1
3: ,Feld2
4: ,Feld3
5: FROM [...]
6: WHERE [...]
7: GROUP BY
8: Feld1
9: ,Feld2
10: ,Feld3
11: HAVING [...]
12: ORDER BY
13: Feld1
14: ,Feld2
15: ,Feld3
WHERE-Klausel
Die WHERE-Klausel wird hier vor der FROM-Klausel erläutert, weil dieselben Regeln auch für die FROM– und HAVING-Klauseln gelten. Eine WHERE-Klausel enthält einen oder mehrere Constraints, die durch logische Operatoren verknüpft sind. Bei der Formatierung der Constraints sind zwei Punkte besonders zu berücksichtigen:
- Ausrichtung von Operanden
- Einrückung gleichwertiger Constraints
Ausrichtung von Operanden
Ein einfacher Constraint besteht aus zwei Operanden und einem Operator (=, !=, <>, IN, NOT IN etc.). In einem zusammengesetzten Constraint mit mehreren Einzel-Constraints sollten Operanden und Operatoren linksbündig untereinander ausgerichtet werden. Im folgenden Beispiel sind die Feldnamen unterschiedlich lang und es werden unterschiedliche Operatoren angewendet:
1: [...]
2: WHERE
3: T01.[Feld___1] = 'Irgendwas'
4: AND T01.[Feld__2] <> 1
5: AND T01.[Feld_____3] NOT IN (1, 2, 3)
6: AND T01.[Feld4] = T02.[Feld5]
So entsteht eine tabellenartige Notation, die schnelle visuelle Navigation innerhalb der Constraint-Bestandteile erlaubt.
Einrückung gleichwertiger Constraints
Enthält die WHERE-Klausel mehr als einen Constraint, werden diese mit logischen Operatoren wie AND oder OR verknüpft. Bei komplexen Ausdrücken sind Klammern erforderlich, um die Auswertungsreihenfolge festzulegen. Je nach Komplexität des Gesamtausdrucks entstehen tief verschachtelte Strukturen.
Um komplexe und verschachtelte Ausdrücke lesbar zu halten, sollte die Strukturierung und Formatierung einer WHERE-Klausel besondere Aufmerksamkeit bekommen: gleichwertige Constraints werden untereinander mit gleicher Einrückung notiert, eine logische Verknüpfung gleichwertiger Constraints wird mit einer Einrückung notiert, die der Verarbeitungsreihenfolge entspricht.
1: [...]
2: WHERE (
3: (
4: [Operand01] = [Operand02]
5: OR [Operand03] = [Operand05]
6: OR [Operand05] = [Operand06]
7: )
8: AND (
9: [Operand07] = [Operand08]
10: OR [Operand09] = [Operand10]
11: )
12: AND [Operand11] = [Operand12]
13: )
14: OR (
15: [Operand13] = [Operand14]
16: )
Die logischen Verknüpfungen werden bei dieser Einrückung visuell lesbar. Der folgende Screenshot derselben WHERE-Klausel in Notepad++ macht den Effekt noch deutlicher, weil das Editor-Feature der vertikalen Hilfslinien an den Tab-Stops die Klammer-Hierarchie unterstreicht:

FROM-Klausel
Wie bei den anderen Hauptelementen werden die untergeordneten Elemente der FROM-Klausel eingerückt notiert. In der Regel handelt es sich um Datenquellen — Tabellen, Views und Common Table Expressions (CTEs).
Sub-SELECTs sollten durch CTEs ersetzt werden — Lesbarkeit, Mehrfachverwendung, einfacheres Debugging. Siehe FAQ am Artikel-Ende für die Pointe und die Postgres-Spezifika (MATERIALIZED / NOT MATERIALIZED).
Für die Formatierung einer JOIN-Klausel gibt es vier Bausteine:
- Tabelle (oder View / CTE)
JOIN-OperatorON-SchlüsselwortJOIN-Constraints
Im folgenden Codebeispiel hat der Leser keinen visuellen Anker für die Identifikation der FROM-Klausel-Elemente:
1: FROM
2: Tabelle1 T01
3: JOIN Tabelle2 T02
4: ON T01.[FK] = T02.[ID]
5: JOIN Tabelle3 T030 ON
6: T02.[FK] = T03.[ID]
7: JOIN Tabelle4 T04
8: ON T03.[FK] = T04.[ID]
9: WHERE
10: [...]
Die Elemente der FROM-Klausel gehören der Lesbarkeit wegen in separate Zeilen. Ausnahme: die herangejointe Tabelle steht direkt hinter dem JOIN-Operator. Das ON-Schlüsselwort steht linksbündig zum JOIN-Operator in der folgenden Zeile. Für die JOIN-Constraints gelten dieselben Regeln wie für die WHERE-Klausel.
1: SELECT
2: [...]
3: FROM
4: Tabelle1 T01
5: JOIN Tabelle2 T02
6: ON
7: T01.[FK] = T02.[ID]
8: JOIN Tabelle3 T030
9: ON
10: T02.[FK1] = T03.[FK1]
11: AND T02.[Feld2] = T03.[Feld2]
12: JOIN Tabelle4 T04
13: ON
14: T03.[FK] = T04.[ID]
15: WHERE
16: [...]
GROUP BY, HAVING, ORDER BY
Für die verbleibenden Hauptelemente gelten dieselben Prinzipien wie für die SELECT-Feldliste und die WHERE-Klausel — kurz zusammengefasst:
GROUP BYträgt eine Feldliste wie ein verkleinertesSELECT— pro Zeile ein Feld mit vorangestelltem Komma, eingerückt nach Konvention.HAVINGist eine Constraint-Liste wieWHERE— Operanden spaltenartig ausgerichtet, gleichwertige Constraints gleich eingerückt. Der Unterschied zurWHERE-Klausel ist semantisch (post-GROUP BY-Filter), nicht typografisch.ORDER BYträgt wieder eine Feldliste — analog zurGROUP BY-Klausel, mit optionalemASC/DESCje Feld (in einer eigenen Spalte gehalten, wenn beide Sortier-Richtungen im Statement vorkommen).
1: SELECT
2: T01.[Region]
3: ,T01.[Year]
4: ,SUM(T01.[Sales]) AS [Total]
5: FROM
6: [dbo].[FactSales] T01
7: GROUP BY
8: T01.[Region]
9: ,T01.[Year]
10: HAVING
11: SUM(T01.[Sales]) >= 1000
12: AND COUNT(*) >= 10
13: ORDER BY
14: T01.[Region] ASC
15: ,T01.[Year] DESC
Auto-Formatter und „Formatieren ist Lernen“
Der Akt des Einrückens, Alias-Ausrichtens und Klammer-Setzens zwingt den Entwickler, das Statement vollständig zu lesen und die Tabellen-Beziehungen mental zu modellieren. Auto-Formatter liefern das Layout — sie liefern nicht das mentale Modell, das beim Schreiben entsteht. Im Zeitalter von Copilot und Cursor ist das doppelt relevant: generiertes SQL ohne Verständnis ist ein Risiko — technisch korrekte Abfragen, die die fachliche Frage trotzdem nicht beantworten.
Pragmatische Empfehlung: erst manuell formatieren, dann einen Formatter als finalen Konsistenz-Pass laufen lassen.
Postgres-Brücke
Die in diesem Artikel gezeigten Layout-Regeln sind engine-neutral — sie gelten 1:1 auch für Postgres. Es gibt nur eine Handvoll Stellen, an denen sich Postgres anders verhält, und keine davon ändert das Format-Pattern:
- Identifier-Quoting: Postgres nutzt
"double quotes", T-SQL[brackets]. ANSI-Standard ist Quotes. Bei case-sensitiven Bezeichnern in Postgres wird das Quoting semantisch relevant (folgt in einem eigenen Artikel zu Case-Sensitivity in SQL Server vs. Postgres). DISTINCT ON: Postgres-Idiom für „erste Zeile pro Gruppe“. In T-SQL überROW_NUMBER() OVER (PARTITION BY …) … WHERE rn = 1zu emulieren. Formatierung analogSELECT-Feldliste.LATERAL JOIN: Postgres erlaubt korrelierte Sub-Queries direkt in derFROM-Klausel. T-SQL nutztCROSS APPLY/OUTER APPLY. Formatierung wie ein normalerJOIN.RETURNING: Postgres-INSERT/UPDATE/DELETEliefert die betroffenen Zeilen zurück. T-SQL nutztOUTPUT. Beide werden als eigene Klausel-Zeile mit Feldliste formatiert.- CTEs:
WITH …ist ANSI und in beiden Engines identisch. Postgres hat zusätzlich die HintsMATERIALIZED/NOT MATERIALIZED(siehe FAQ). JOIN-Indentation, WHERE-Klammer-Pattern, ORDER-BY-Listen: völlig identisch.
Die Format-Disziplin trägt also auf beiden Engines. Zu den Postgres-Bezeichner-Spezifika gibt’s einen eigenen Folge-Artikel.
FAQ
Warum Sub-SELECTs durch CTEs ersetzen?
CTEs sind lesbarer (das Statement liest sich top-down statt verschachtelt), wiederverwendbar (eine CTE darf in derselben Abfrage mehrfach referenziert werden) und debugging-freundlicher (eine CTE-Definition kann isoliert in einem SELECT * FROM cte_name-Smoketest getestet werden). Sub-SELECTs sind nur dann das bessere Werkzeug, wenn sie wirklich nur einmal verwendet werden und die Lesbarkeit dadurch nicht leidet — was selten ist.
Soll der JOIN-Operator immer explizit (INNER, LEFT, RIGHT, FULL) gekennzeichnet werden?
Ja. Der Default JOIN = INNER JOIN ist Mitlesern nicht immer geläufig, und die Diff-Erkennung von JOIN zu LEFT JOIN ist visuell schwierig. Konsequente Schreibweise: immer INNER JOIN / LEFT JOIN / etc. ausgeschrieben.
Gilt das Format-Pattern auch für Postgres?
Ja, 1:1 — siehe Abschnitt Postgres-Brücke. Die einzige praktische Anpassung ist das Identifier-Quoting ("…" statt […]). Die Layout-Regeln für SELECT-Feldliste, WHERE-Klausel, FROM-Klausel und JOIN-Indentation sind engine-neutral.
Reicht ein Auto-Formatter wie sqlfluff oder pgFormatter — muss ich noch selbst formatieren?
Auto-Formatter liefern das Layout, aber nicht den Lerneffekt. Wer ein 200-Zeilen-Statement nur durch den Formatter laufen lässt, hat das Statement nicht gelesen. Wer es manuell strukturiert, baut die mentalen Modelle der Tabellen-Beziehungen auf — und entdeckt bei dieser Gelegenheit häufig logische Fehler. Pragmatischer Workflow: erst manuell, dann Formatter als Konsistenz-Polish.
Was ist mit MATERIALIZED / NOT MATERIALIZED für CTEs in Postgres?
Postgres optimiert seit Version 12 CTEs standardmäßig wie Sub-Queries (Predicate-Pushdown ist möglich). Mit WITH cte_name AS MATERIALIZED (…) lässt sich das alte Verhalten erzwingen (CTE wird zwischengespeichert), mit NOT MATERIALIZED das neue Verhalten explizit setzen. Für Performance-kritische Abfragen mit teuren CTEs lohnt der Blick in die Postgres-Doku zu MATERIALIZED. In T-SQL gibt’s keinen Äquivalent — CTEs werden dort immer inline-optimiert.
Wo finde ich Teil 1 (Bezeichner, Delimiter, Kommata, Aliase)?
Teil 1 — Formatierung von SQL Statements. Dort geht’s um die kleineren Bausteine: reguläre vs. delimited Identifier, das Komma vorne vs. hinten, systematische T01/T02-Aliase, qualifizierte Feldnamen. Teil 1 + Teil 2 zusammen ergeben einen vollständigen Style-Guide für SELECT-Statements.