Formatierung von SQL Statements (Teil 2) — Statement-Aufbau: SELECT, WHERE, FROM, JOIN

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 (SELECTWHEREFROMJOIN). Die Bezeichner-, Delimiter-, Komma- und Alias-Grundlagen stehen in Teil 1 — Bezeichner, Delimiter, Kommata, Aliase.

TL;DR — was dieser Artikel liefert:

  • Hauptelemente (SELECTFROMWHEREGROUP BYHAVINGORDER 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 dem JOIN-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 ONLATERAL; 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:

  • SELECT
  • FROM 
  • WHERE 
  • GROUP BY 
  • HAVING 
  • ORDER 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 (=!=<>INNOT 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:

WHERE-Klausel in Notepad++ mit vertikalen Einrückungs-Hilfslinien an den Tab-Stops, die die Verschachtelung der Klammer-Konstrukte sichtbar machen.

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-Operator
  • ON-Schlüsselwort
  • JOIN-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 BY trägt eine Feldliste wie ein verkleinertes SELECT — pro Zeile ein Feld mit vorangestelltem Komma, eingerückt nach Konvention.
  • HAVING ist eine Constraint-Liste wie WHERE — Operanden spaltenartig ausgerichtet, gleichwertige Constraints gleich eingerückt. Der Unterschied zur WHERE-Klausel ist semantisch (post-GROUP BY-Filter), nicht typografisch.
  • ORDER BY trägt wieder eine Feldliste — analog zur GROUP BY-Klausel, mit optionalem ASC / DESC je 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 über ROW_NUMBER() OVER (PARTITION BY …) … WHERE rn = 1 zu emulieren. Formatierung analog SELECT-Feldliste.
  • LATERAL JOIN: Postgres erlaubt korrelierte Sub-Queries direkt in der FROM-Klausel. T-SQL nutzt CROSS APPLY / OUTER APPLY. Formatierung wie ein normaler JOIN.
  • RETURNING: Postgres-INSERT/UPDATE/DELETE liefert die betroffenen Zeilen zurück. T-SQL nutzt OUTPUT. Beide werden als eigene Klausel-Zeile mit Feldliste formatiert.
  • CTEs: WITH … ist ANSI und in beiden Engines identisch. Postgres hat zusätzlich die Hints MATERIALIZED / 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 (INNERLEFTRIGHTFULL) 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.