549 lines
15 KiB
Markdown
549 lines
15 KiB
Markdown
#datenbank
|
||
Kontext: Datenbanken
|
||
|
||
= ==Common Table Expression==
|
||
|
||
>`WITH`-Block in SQL-Abfrage
|
||
|
||
➡️[[#Zusammenfassung]]
|
||
|
||
---
|
||
Im Folgenden bekommst du eine umfassende, aber einsteigerfreundliche Einführung in CTEs („Common Table Expressions“) im Kontext von Datenbanken – mit Definition, Abgrenzung, Nutzen, Herausforderungen und praxisnahen Beispielen.
|
||
|
||
---
|
||
|
||
## 1. Grundidee: Was ist eine CTE?
|
||
|
||
**CTE** steht für **Common Table Expression**.
|
||
Vereinfacht gesagt ist eine CTE:
|
||
|
||
> Eine **temporäre, benannte Ergebnismenge**, die du in einer SQL-Abfrage definierst und im Anschluss in derselben Abfrage wie eine Tabelle verwenden kannst.
|
||
|
||
Man kann sich das vorstellen wie:
|
||
|
||
- „Ich speichere mir ein Zwischenergebnis unter einem Namen…“
|
||
- „…und benutze dieses Zwischenergebnis dann in der eigentlichen Anfrage.“
|
||
|
||
Grundform:
|
||
|
||
```sql
|
||
WITH name_der_cte AS (
|
||
-- irgendeine SELECT-Abfrage
|
||
SELECT ...
|
||
FROM ...
|
||
WHERE ...
|
||
)
|
||
SELECT *
|
||
FROM name_der_cte;
|
||
```
|
||
|
||
Wichtig:
|
||
|
||
- Die CTE existiert **nur für diese eine Abfrage**.
|
||
- Sie wird **innerhalb der Abfrage** definiert und direkt **danach** genutzt.
|
||
- Man kann CTEs **mehrfach referenzieren**, als wären es echte Tabellen.
|
||
|
||
---
|
||
|
||
## 2. Ein einfaches Beispiel
|
||
|
||
Stell dir vor, du hast eine Tabelle `bestellungen`:
|
||
|
||
- `id`
|
||
- `kunde_id`
|
||
- `betrag`
|
||
- `datum`
|
||
|
||
Du möchtest:
|
||
|
||
1. Alle Bestellungen des Jahres 2024 herausfiltern.
|
||
2. Dann auf Basis dieser gefilterten Daten den Gesamtumsatz pro Kunde berechnen.
|
||
|
||
Ohne CTE könnte man das mit einer verschachtelten Abfrage lösen; mit CTE sieht es lesbarer aus:
|
||
|
||
```sql
|
||
WITH bestellungen_2024 AS (
|
||
SELECT *
|
||
FROM bestellungen
|
||
WHERE datum >= '2024-01-01'
|
||
AND datum < '2025-01-01'
|
||
)
|
||
SELECT
|
||
kunde_id,
|
||
SUM(betrag) AS umsatz_2024
|
||
FROM bestellungen_2024
|
||
GROUP BY kunde_id;
|
||
```
|
||
|
||
Die CTE `bestellungen_2024` ist hier:
|
||
„Alle Bestellungen aus 2024“, und wird anschließend in der Hauptabfrage verwendet.
|
||
|
||
---
|
||
|
||
## 3. Abgrenzung zu ähnlichen oder verwandten Begriffen
|
||
|
||
### 3.1 CTE vs. Unterabfrage (Subquery / Derived Table)
|
||
|
||
**Unterabfrage**: Eine Abfrage innerhalb einer anderen Abfrage, z. B.:
|
||
|
||
```sql
|
||
SELECT
|
||
kunde_id,
|
||
SUM(betrag) AS umsatz_2024
|
||
FROM (
|
||
SELECT *
|
||
FROM bestellungen
|
||
WHERE datum >= '2024-01-01'
|
||
AND datum < '2025-01-01'
|
||
) AS b2024
|
||
GROUP BY kunde_id;
|
||
```
|
||
|
||
Unterschiede:
|
||
|
||
- **CTE**: Wird am Anfang mit `WITH` definiert, trägt einen Namen und kann **mehrfach** benutzt werden.
|
||
- **Subquery**: Steht direkt im `FROM` oder `WHERE`, ist meist **anonymer** und schwerer zu lesen, vor allem bei komplexen Konstrukten.
|
||
|
||
Funktional können CTEs und Subqueries oft das Gleiche – CTEs verbessern eher **Struktur und Lesbarkeit**.
|
||
|
||
---
|
||
|
||
### 3.2 CTE vs. View (Sicht)
|
||
|
||
**View** (Sicht) ist wie eine **gespeicherte Abfrage** in der Datenbank:
|
||
|
||
```sql
|
||
CREATE VIEW bestellungen_2024 AS
|
||
SELECT *
|
||
FROM bestellungen
|
||
WHERE datum >= '2024-01-01'
|
||
AND datum < '2025-01-01';
|
||
```
|
||
|
||
Dann kannst du schreiben:
|
||
|
||
```sql
|
||
SELECT kunde_id, SUM(betrag)
|
||
FROM bestellungen_2024
|
||
GROUP BY kunde_id;
|
||
```
|
||
|
||
Unterschiede:
|
||
|
||
- **View**:
|
||
- Wird **dauerhaft** in der Datenbank definiert.
|
||
- Hat einen Namen, kann von **vielen Abfragen** und auch anderen Nutzer:innen verwendet werden.
|
||
- Ändert sich nur über `CREATE OR REPLACE VIEW` / `ALTER VIEW`.
|
||
- **CTE**:
|
||
- Gilt **nur für eine einzige Abfrage**.
|
||
- Ist daher ideal für **einmalige** oder **sehr spezialisierte** Zwischenschritte.
|
||
- Erfordert keine Rechte zum Erstellen von Objekten in der Datenbank (kein `CREATE VIEW`).
|
||
|
||
Kurz:
|
||
Views = dauerhafte, wiederverwendbare Bausteine.
|
||
CTEs = temporäre, einmalige Bausteine innerhalb einer Abfrage.
|
||
|
||
---
|
||
|
||
### 3.3 CTE vs. temporäre Tabelle
|
||
|
||
Viele Datenbanken kennen **temporäre Tabellen**, z. B. `#temp_tab` in SQL Server oder `CREATE TEMP TABLE` in PostgreSQL:
|
||
|
||
```sql
|
||
CREATE TEMP TABLE bestellungen_2024 AS
|
||
SELECT *
|
||
FROM bestellungen
|
||
WHERE datum >= '2024-01-01'
|
||
AND datum < '2025-01-01';
|
||
|
||
SELECT kunde_id, SUM(betrag)
|
||
FROM bestellungen_2024
|
||
GROUP BY kunde_id;
|
||
```
|
||
|
||
Unterschiede:
|
||
|
||
- **Temporäre Tabelle**:
|
||
- Wird physisch (zumindest logisch) in der Datenbank angelegt.
|
||
- Existiert für die Dauer einer Session oder Transaktion.
|
||
- Kann **indiziert** werden (Index hinzufügen) und so bei großen Datenmengen Performancevorteile bringen.
|
||
- **CTE**:
|
||
- Keine echte Tabelle, eher eine „logische Abfragekomponente“.
|
||
- Keine eigenen Indexe.
|
||
- Gilt nur innerhalb einer Abfrage.
|
||
|
||
---
|
||
|
||
### 3.4 CTE vs. Stored Procedure / Function
|
||
|
||
**Stored Procedures** und **Functions** sind Programmierbausteine auf Datenbankseite, z. B.:
|
||
|
||
- Prozeduren: führen mehrere Schritte, ggf. mit Kontrollstrukturen aus (IF, WHILE,…).
|
||
- Funktionen: geben einen Wert oder eine Tabelle zurück.
|
||
|
||
Unterschiede:
|
||
|
||
- CTE ist **Teil einer einzelnen SQL-Select/Insert/Update/Delete-Abfrage**.
|
||
- Stored Proc / Function ist **Code**, den man **speichert, versioniert und immer wieder aufrufen** kann.
|
||
|
||
CTEs können innerhalb von Stored Procedures verwendet werden – sie sind eher Bausteine **auf Abfrage-Ebene**, nicht auf Programm-Ebene.
|
||
|
||
---
|
||
|
||
## 4. Welche Probleme werden durch CTEs gelöst?
|
||
|
||
### 4.1 Bessere Lesbarkeit und Struktur
|
||
|
||
Statt eine riesige, komplexe Abfrage mit vielen verschachtelten Unterabfragen zu schreiben, kann man sich die Abfrage in **logische Schritte** zerlegen:
|
||
|
||
1. `WITH schritt1 AS (...)`
|
||
2. `, schritt2 AS (...)`
|
||
3. `SELECT ... FROM schritt2 ...`
|
||
|
||
Beispiel:
|
||
Du möchtest erst „aktive Kunden“ bestimmen und dann nur deren Bestellungen summieren:
|
||
|
||
```sql
|
||
WITH aktive_kunden AS (
|
||
SELECT id, name
|
||
FROM kunden
|
||
WHERE status = 'aktiv'
|
||
),
|
||
bestellungen_aktive_kunden AS (
|
||
SELECT
|
||
b.kunde_id,
|
||
SUM(b.betrag) AS umsatz
|
||
FROM bestellungen b
|
||
JOIN aktive_kunden k ON k.id = b.kunde_id
|
||
GROUP BY b.kunde_id
|
||
)
|
||
SELECT
|
||
k.name,
|
||
bak.umsatz
|
||
FROM bestellungen_aktive_kunden bak
|
||
JOIN aktive_kunden k ON k.id = bak.kunde_id;
|
||
```
|
||
|
||
Jeder CTE beschreibt einen **klaren Teilschritt**, das erleichtert Verstehen und Warten des Codes.
|
||
|
||
---
|
||
|
||
### 4.2 Wiederverwendung innerhalb einer Abfrage
|
||
|
||
Oft brauchst du ein bestimmtes Zwischenergebnis **mehrmals in derselben Abfrage**.
|
||
Ohne CTE müsstest du:
|
||
|
||
- Dieselbe Unterabfrage mehrfach schreiben (redundant, fehleranfällig)
|
||
- Oder du machst Temp-Tabellen / Views, die aber mehr Setup erfordern.
|
||
|
||
Mit CTE definierst du das einmal und verwendest es mehrfach:
|
||
|
||
```sql
|
||
WITH bestellungen_2024 AS (
|
||
SELECT *
|
||
FROM bestellungen
|
||
WHERE datum >= '2024-01-01'
|
||
AND datum < '2025-01-01'
|
||
)
|
||
SELECT
|
||
(SELECT COUNT(*) FROM bestellungen_2024) AS anzahl_gesamt,
|
||
(SELECT SUM(betrag) FROM bestellungen_2024) AS umsatz_gesamt;
|
||
```
|
||
|
||
---
|
||
|
||
### 4.3 Schrittweise Transformationen (ETL-artige Abläufe)
|
||
|
||
Man kann komplexe Datenverarbeitungen in **mehrere CTE-Schritte** aufteilen, z. B.:
|
||
|
||
1. Rohdaten bereinigen (ungültige Werte rausfiltern)
|
||
2. Daten anreichern (Join mit Lookup-Tabellen)
|
||
3. Aggregationen berechnen
|
||
4. Ergebnis selektieren
|
||
|
||
Schema:
|
||
|
||
```sql
|
||
WITH raw_data AS (
|
||
SELECT * FROM import_tabelle
|
||
),
|
||
cleaned_data AS (
|
||
SELECT *
|
||
FROM raw_data
|
||
WHERE wert IS NOT NULL
|
||
),
|
||
enriched_data AS (
|
||
SELECT
|
||
c.*,
|
||
l.beschreibung
|
||
FROM cleaned_data c
|
||
LEFT JOIN lookup l ON c.code = l.code
|
||
),
|
||
aggregated AS (
|
||
SELECT
|
||
beschreibung,
|
||
COUNT(*) AS anzahl,
|
||
AVG(wert) AS durchschnitt
|
||
FROM enriched_data
|
||
GROUP BY beschreibung
|
||
)
|
||
SELECT *
|
||
FROM aggregated
|
||
ORDER BY anzahl DESC;
|
||
```
|
||
|
||
Solche „linearen“ CTE-Ketten sind sehr hilfreich, um komplexe ETL-Schritte innerhalb einer SQL-Abfrage klar zu definieren.
|
||
|
||
---
|
||
|
||
### 4.4 Rekursive Abfragen (Hierarchien, Bäume, Graphen)
|
||
|
||
Ein **besonderer Typ** von CTE ist die **rekursive CTE**. Damit kann man Strukturen abfragen, die **hierarchisch** sind, z. B.:
|
||
|
||
- Mitarbeiter und ihre Vorgesetzten (Organigramm)
|
||
- Kategorien und Unterkategorien (Baumstrukturen)
|
||
- Stücklisten (Bauteil besteht aus Unterteilen, die wiederum Unterteile haben)
|
||
- Graphen/Netzwerke mit Verbindungen
|
||
|
||
Grundidee:
|
||
|
||
- Es gibt eine **Anker-Abfrage** (Startpunkt).
|
||
- Und eine **rekursive Abfrage**, die sich immer wieder selbst referenziert.
|
||
|
||
Beispiel: Mitarbeiter-Hierarchie
|
||
Tabelle `mitarbeiter`:
|
||
|
||
- `id`
|
||
- `name`
|
||
- `chef_id` (Verweis auf `id` in derselben Tabelle, NULL für Chef ganz oben)
|
||
|
||
```sql
|
||
WITH RECURSIVE hierarchie AS (
|
||
-- 1) Anker: Chef (oberste Ebene)
|
||
SELECT
|
||
id,
|
||
name,
|
||
chef_id,
|
||
0 AS ebene
|
||
FROM mitarbeiter
|
||
WHERE chef_id IS NULL
|
||
|
||
UNION ALL
|
||
|
||
-- 2) Rekursiver Teil: alle Mitarbeiter, die einem bereits gefundenen Mitarbeiter unterstellt sind
|
||
SELECT
|
||
m.id,
|
||
m.name,
|
||
m.chef_id,
|
||
h.ebene + 1 AS ebene
|
||
FROM mitarbeiter m
|
||
JOIN hierarchie h ON m.chef_id = h.id
|
||
)
|
||
SELECT *
|
||
FROM hierarchie
|
||
ORDER BY ebene, id;
|
||
```
|
||
|
||
Ergebnis:
|
||
Alle Mitarbeiter mit einer Spalte `ebene`, die angibt, wie weit sie vom obersten Chef entfernt sind.
|
||
|
||
**Ohne rekursive CTEs** wäre das sehr umständlich oder gar nicht (standardkonform) direkt in SQL möglich. Manche Systeme haben dafür eigene Syntax (z. B. `CONNECT BY` in Oracle), rekursive CTEs sind der **SQL-Standard**-Weg.
|
||
|
||
---
|
||
|
||
## 5. Weitere praxisnahe Beispiele
|
||
|
||
### 5.1 Alltagsszenario: „Top-Produkte pro Monat“
|
||
|
||
Gegeben:
|
||
|
||
- `verkaeufe` mit Spalten:
|
||
- `produkt_id`
|
||
- `datum`
|
||
- `menge`
|
||
- `umsatz`
|
||
|
||
Ziel:
|
||
Pro Monat die **Top 3 Produkte nach Umsatz** anzeigen.
|
||
|
||
Ein möglicher Weg mit CTE:
|
||
|
||
```sql
|
||
WITH monatliche_umsaetze AS (
|
||
SELECT
|
||
DATE_TRUNC('month', datum) AS monat,
|
||
produkt_id,
|
||
SUM(umsatz) AS umsatz_monat
|
||
FROM verkaeufe
|
||
GROUP BY DATE_TRUNC('month', datum), produkt_id
|
||
),
|
||
ranking AS (
|
||
SELECT
|
||
monat,
|
||
produkt_id,
|
||
umsatz_monat,
|
||
ROW_NUMBER() OVER (
|
||
PARTITION BY monat
|
||
ORDER BY umsatz_monat DESC
|
||
) AS rang_im_monat
|
||
FROM monatliche_umsaetze
|
||
)
|
||
SELECT
|
||
monat,
|
||
produkt_id,
|
||
umsatz_monat,
|
||
rang_im_monat
|
||
FROM ranking
|
||
WHERE rang_im_monat <= 3
|
||
ORDER BY monat, rang_im_monat;
|
||
```
|
||
|
||
Die Logik wird nachvollziehbar in zwei Schritte aufgeteilt:
|
||
|
||
1. summieren pro Monat und Produkt
|
||
2. pro Monat ranken und Top 3 auswählen
|
||
|
||
---
|
||
|
||
### 5.2 Kalender-CTE: fehlende Tage auffüllen
|
||
|
||
Manchmal hat man z. B. nur Daten für Tage, an denen Verkäufe stattfanden, möchte aber **jeden Tag im Zeitraum** sehen (auch wenn Umsatz 0 ist).
|
||
Mit einer rekursiven CTE kann man einen Kalender erzeugen.
|
||
|
||
Beispiel (PostgreSQL-Syntax):
|
||
|
||
```sql
|
||
WITH RECURSIVE kalender AS (
|
||
SELECT DATE '2024-01-01' AS tag
|
||
UNION ALL
|
||
SELECT tag + INTERVAL '1 day'
|
||
FROM kalender
|
||
WHERE tag < DATE '2024-01-31'
|
||
)
|
||
SELECT
|
||
k.tag,
|
||
COALESCE(SUM(v.umsatz), 0) AS umsatz
|
||
FROM kalender k
|
||
LEFT JOIN verkaeufe v ON DATE(v.datum) = k.tag
|
||
GROUP BY k.tag
|
||
ORDER BY k.tag;
|
||
```
|
||
|
||
So erhältst du für **jeden Tag im Januar 2024** einen Umsatzwert – bei Tagen ohne Verkäufe eben 0.
|
||
|
||
---
|
||
|
||
## 6. Welche Herausforderungen und Stolpersteine gibt es?
|
||
|
||
### 6.1 Performance (Leistungsfähigkeit)
|
||
|
||
CTEs sind primär ein **Lesbarkeits-Feature**, aber sie können die Performance beeinflussen – je nach Datenbank:
|
||
|
||
- Einige Datenbanken „materialisieren“ CTEs:
|
||
- Das Zwischenergebnis wird wirklich berechnet und zwischengespeichert.
|
||
- Mehrfachzugriff ist dann ggf. schneller, aber die Initialberechnung kann teurer sein.
|
||
- Andere optimieren CTEs ähnlich wie Unterabfragen:
|
||
- Sie werden im Optimizer „eingefaltet“, d. h. wie eine direkte Teilabfrage behandelt.
|
||
|
||
Typische Punkte:
|
||
|
||
- **Mehrfache Verwendung großer CTEs**:
|
||
- Wenn eine CTE sehr groß ist und du mehrfach darauf zugreifst, kann das teuer werden.
|
||
- In manchen Systemen ist dann eine temporäre Tabelle mit Index effizienter.
|
||
- **Rekursive CTEs**:
|
||
- Können viel CPU und Zeit brauchen, wenn die Hierarchie sehr tief oder stark verzweigt ist.
|
||
- Es gibt oft eine maximale Rekursionstiefe (z. B. 100 oder 32767), die begrenzt, wie tief die Rekursion geht.
|
||
|
||
Praxis-Tipp:
|
||
|
||
- CTEs sind super für mittlere Komplexität und moderate Datenmengen.
|
||
- Bei sehr großen Datenmengen und Performanceproblemen:
|
||
- Ausführungsplan anschauen.
|
||
- Mit Temp-Tabellen, Indizes oder Views experimentieren.
|
||
|
||
---
|
||
|
||
### 6.2 Übermäßige Verschachtelung
|
||
|
||
Wenn man CTEs zu exzessiv nutzt, z. B. 20 oder mehr CTEs in einer Abfrage, wird es:
|
||
|
||
- schwer zu lesen
|
||
- schwierig zu debuggen
|
||
- komplex zu warten
|
||
|
||
Hier gilt:
|
||
**Abstraktion, wo sinnvoll, aber nicht übertreiben.**
|
||
|
||
Manchmal ist es besser:
|
||
|
||
- Teile der Logik in eine View zu legen,
|
||
- oder in eine Stored Procedure / Function,
|
||
- oder die Logik in mehrere kleinere Abfragen aufzuteilen.
|
||
|
||
---
|
||
|
||
### 6.3 Datenbankspezifische Unterschiede
|
||
|
||
Nicht jede Datenbank:
|
||
|
||
- unterstützt CTEs gleich
|
||
- oder hat genau dieselbe Syntax.
|
||
|
||
Beispiele:
|
||
|
||
- **PostgreSQL**: Unterstützt `WITH` und `WITH RECURSIVE`.
|
||
- **SQL Server**: Unterstützt CTEs, Rekursion ohne extra `RECURSIVE`-Keyword.
|
||
- **MySQL**: Ab Version 8.0 gibt es CTEs, rekursiv mit `WITH RECURSIVE`.
|
||
- **Oracle**: Unterstützt CTEs, hatte aber historisch eigene Hierarchie-Syntax (`CONNECT BY`).
|
||
|
||
Wenn du in mehreren Systemen arbeitest, lohnt ein Blick in die Dokumentation der jeweiligen Datenbank.
|
||
|
||
---
|
||
|
||
### 6.4 CTEs haben keine eigenen Indizes
|
||
|
||
Da CTEs keine physischen Tabellen sind, kannst du:
|
||
|
||
- keine Indexe darauf erstellen
|
||
- keine Statistiken direkt auf CTE-Ebene pflegen
|
||
|
||
D. h.:
|
||
|
||
- Der Optimizer muss die Abfrage so gut wie möglich auf Grundlage der zugrundeliegenden Tabellen optimieren.
|
||
- Bei Performanceproblemen kann eine echte (temporäre) Tabelle mit Index manchmal besser sein.
|
||
|
||
---
|
||
|
||
## 7. Wann sollte man CTEs einsetzen? – Orientierung
|
||
|
||
**Geeignet für CTEs:**
|
||
|
||
- Wenn eine Abfrage **schwer lesbar** ist, weil sie viele Unterabfragen enthält.
|
||
- Wenn du **dasselbe Zwischenergebnis mehrfach** in einer Abfrage benötigst.
|
||
- Wenn du **schrittweise Transformationen** ausdrücken möchtest („Step 1, Step 2, …“).
|
||
- Wenn du **Hierarchien** oder rekursive Strukturen mit SQL abbilden willst.
|
||
|
||
**Eher nicht ideal:**
|
||
|
||
- Für dauerhafte, von vielen Personen genutzte Logik → eher **Views** oder **Stored Procedures**.
|
||
- Für sehr große Zwischenergebnisse, die oft wiederverwendet werden → evtl. **temporäre Tabellen** mit Index.
|
||
- Wenn Performance schon kritisch ist → CTE-Einsatz testen und Ausführungspläne prüfen.
|
||
|
||
---
|
||
|
||
## Zusammenfassung
|
||
|
||
- **Definition**: CTE = „Common Table Expression“ = eine **temporäre, benannte Ergebnismenge** innerhalb einer Abfrage, definiert mit `WITH`.
|
||
- **Nutzen**:
|
||
- Klarere Struktur und bessere Lesbarkeit komplexer SQL-Abfragen.
|
||
- Wiederverwendbarkeit eines Zwischenergebnisses innerhalb derselben Abfrage.
|
||
- Möglichkeit, komplexe Transformationen sequenziell auszudrücken.
|
||
- Unterstützung rekursiver Abfragen (Hierarchien/Bäume).
|
||
- **Abgrenzung**:
|
||
- Gegenüber Subqueries: funktional ähnlich, aber meist lesbarer.
|
||
- Gegenüber Views: CTEs sind **temporär**, Views **dauerhaft**.
|
||
- Gegenüber Temp-Tabellen: CTEs sind logisch, Temp-Tabellen physisch und indexierbar.
|
||
- **Herausforderungen**:
|
||
- Performanceabhängig vom Datenbanksystem und vom Aufbau der Abfrage.
|
||
- Übermäßiger Gebrauch kann Abfragen unübersichtlich machen.
|
||
- Rekursive CTEs können komplex und teuer werden, brauchen sorgfältige Bedingungen. |