Daten abfragen mit SQL
Im vorherigen Kapitel haben wir gelernt, wie man Tabellen erstellt, Daten einfügt und Daten auch einfach ausliest. Jetzt gehen wir einen Schritt weiter: Wir lernen, wie man gezielt nach Daten sucht, sie filtert, sortiert und analysiert - das Herzstück jeder Datenbank!
SQL ist eine deklarative Sprache: Wir beschreiben, was wir haben möchten, nicht wie die Datenbank es finden soll. Das macht SQL mächtig und gleichzeitig einfach zu lernen.
Die Grundstruktur von SELECT
Im vorigen Kapitel haben wir bereits den SELECT Befehl kennengelernt. Doch neben dieser einfachen gezeigten Abfrage können wir auch noch mehr damit machen. Eine SELECT-Abfrage hat folgende Grundstruktur:
SELECT spalten
FROM tabelle
-- ↓↓ Weitere Klauseln ↓↓
WHERE bedingung
ORDER BY sortierung;
Übersetzt bedeutet das:
"Wähle diese Spalten aus dieser Tabelle, aber nur die Zeilen, die diese Bedingung erfüllen, und sortiere das Ergebnis nach dieser Sortierung."
Wir werden uns nun in diesem Kapitel ansehen, welche weiteren Möglichkeiten wir haben, um Daten aus einer Datenbank abzufragen und schon einfache Analysen durchführen.
Datenbank-Setup
Für die Beispiele in diesem Kapitel verwenden wir eine Lieferketten-Datenbank. Diese verwaltet Materiallieferungen für ein produzierendes Unternehmen - ein typisches Szenario im Wirtschaftsingenieurwesen.
Führe folgenden Code aus, um die Beispieldatenbank zu erstellen:
-- Datenbank erstellen
CREATE DATABASE lieferkette_db;
-- Zur Datenbank wechseln
\c lieferkette_db
-- Tabelle für Lieferungen erstellen
CREATE TABLE lieferungen (
lieferung_id INTEGER PRIMARY KEY,
lieferant VARCHAR(100),
material VARCHAR(100),
menge INTEGER,
preis_pro_einheit NUMERIC(10,2),
lieferdatum DATE,
status VARCHAR(20),
zielstandort VARCHAR(50)
);
-- Beispieldaten einfuegen
INSERT INTO lieferungen VALUES
(1, 'Stahlwerk Salzgitter', 'Stahlblech 2mm', 500, 12.50, '2024-03-15', 'Geliefert', 'Lager Nord'),
(2, 'Aluminium AG', 'Aluminiumprofile', 200, 25.00, '2024-03-18', 'Geliefert', 'Lager Sued'),
(3, 'Kunststoff GmbH', 'PVC-Platten', 150, 8.75, '2024-03-20', 'In Transit', 'Lager Nord'),
(4, 'Stahlwerk Salzgitter', 'Stahlprofil IPE200', 80, 45.00, '2024-03-22', 'Bestellt', 'Lager Ost'),
(5, 'Schrauben Meyer', 'Sechskantschrauben M8', 5000, 0.15, '2024-03-16', 'Geliefert', 'Lager Nord'),
(6, 'Aluminium AG', 'Aluminiumblech 3mm', 300, 18.50, '2024-03-25', 'Bestellt', 'Lager Sued'),
(7, 'Elektronik Weber', 'Sensoren Typ A', 100, 12.90, '2024-03-19', 'Geliefert', 'Lager Ost'),
(8, 'Kunststoff GmbH', 'Gummiband 10mm', 800, 2.30, '2024-03-21', 'In Transit', 'Lager Nord'),
(9, 'Schrauben Meyer', 'Muttern M8', 5000, 0.08, '2024-03-17', 'Geliefert', 'Lager Nord'),
(10, 'Elektronik Weber', 'Kabelbaum 5m', 50, 22.00, '2024-03-28', 'Bestellt', 'Lager Ost');
Diese Tabelle enthält typische Lieferinformationen: Lieferant, Material, Menge, Preis, Lieferdatum, Status und Zielstandort.
Filtern mit WHERE
Bislang haben wir bei der Abfrage von Daten entweder alles abgefragt (*) oder gewisse Spalten ausgewählt. Welche Zeilen / Tuples aber geladen werden sollen, haben wir bisher nicht eingrenzt. Daher wurden zuvor alle Zeilen geladen.
Mit der WHERE-Klausel können wir aber nun Datensätze nach bestimmten Kriterien filtern. Der grundlegende Syntax lautet wiefolgt:
SELECT *
FROM tabellenname
WHERE bedingung;
Einfache Vergleiche
Wie auch in der Mathematik stehen uns für die Bedingungen verschiedenste Vergleichsoperatoren zur Verfügung.
Mit diesen Vergleichsoperatoren können wir nun Filter-Bedingungen für die Abfrage der Daten festlegen.
Lieferungen von spezifischen Lieferanten
-- Alle Lieferungen von 'Stahlwerk Salzgitter'
SELECT * FROM lieferungen
WHERE lieferant = 'Stahlwerk Salzgitter';
lieferung_id | lieferant | material | menge | preis_pro_einheit | lieferdatum | status | zielstandort
--------------+----------------------+---------------------+-------+-------------------+-------------+----------+--------------
1 | Stahlwerk Salzgitter | Stahlblech 2mm | 500 | 12.50 | 2024-03-15 | Geliefert| Lager Nord
4 | Stahlwerk Salzgitter | Stahlprofil IPE200 | 80 | 45.00 | 2024-03-22 | Bestellt | Lager Ost
(2 rows)
weitere Beispiele
Menge größer 200
-- Lieferungen mit Menge größer 200
SELECT lieferant, material, menge
FROM lieferungen
WHERE menge > 200;
lieferant | material | menge
----------------------+-----------------------+-------
Stahlwerk Salzgitter | Stahlblech 2mm | 500
Schrauben Meyer | Sechskantschrauben M8 | 5000
Aluminium AG | Aluminiumblech 3mm | 300
Kunststoff GmbH | Gummiband 10mm | 800
Schrauben Meyer | Muttern M8 | 5000
(5 rows)
Alle außer gelieferte Lieferungen
-- Alle außer gelieferte Lieferungen
SELECT lieferant, material, status
FROM lieferungen
WHERE status != 'Geliefert';
lieferant | material | status
----------------------+--------------------+------------
Kunststoff GmbH | PVC-Platten | In Transit
Stahlwerk Salzgitter | Stahlprofil IPE200 | Bestellt
Aluminium AG | Aluminiumblech 3mm | Bestellt
Kunststoff GmbH | Gummiband 10mm | In Transit
Elektronik Weber | Kabelbaum 5m | Bestellt
(5 rows)
Verknüpfte Vergleiche
Neben den einfachen Vergleichen können wir mehrere Bedingungen auch zu komplexeren verknüpften Vergleichen kombinieren. Dazu stehen uns AND, OR, NOT, IN und BETWEEN zur Verfügung.
Inklusive
BETWEEN ist inklusive - beide Grenzen sind mit eingeschlossen
Große Lieferungen die bereits geliefert wurden
-- Große Lieferungen (> 200) die bereits geliefert wurden
SELECT lieferant, material, menge, status
FROM lieferungen
WHERE menge > 200 AND status = 'Geliefert';
lieferant | material | menge | status
----------------------+-----------------------+-------+-----------
Stahlwerk Salzgitter | Stahlblech 2mm | 500 | Geliefert
Schrauben Meyer | Sechskantschrauben M8 | 5000 | Geliefert
Schrauben Meyer | Muttern M8 | 5000 | Geliefert
(3 rows)
weitere Beispiele
Lieferungen die in Transit oder Bestellt sind
-- Lieferungen die in Transit oder Bestellt sind
SELECT lieferant, material, status
FROM lieferungen
WHERE status = 'In Transit' OR status = 'Bestellt';
lieferant | material | status
----------------------+--------------------+------------
Kunststoff GmbH | PVC-Platten | In Transit
Stahlwerk Salzgitter | Stahlprofil IPE200 | Bestellt
Aluminium AG | Aluminiumblech 3mm | Bestellt
Kunststoff GmbH | Gummiband 10mm | In Transit
Elektronik Weber | Kabelbaum 5m | Bestellt
(5 rows)
Alle außer gelieferte Lieferungen
-- Alle außer gelieferte Lieferungen
SELECT lieferant, material, status
FROM lieferungen
WHERE NOT status = 'Geliefert';
lieferant | material | status
----------------------+--------------------+------------
Kunststoff GmbH | PVC-Platten | In Transit
Stahlwerk Salzgitter | Stahlprofil IPE200 | Bestellt
Aluminium AG | Aluminiumblech 3mm | Bestellt
Kunststoff GmbH | Gummiband 10mm | In Transit
Elektronik Weber | Kabelbaum 5m | Bestellt
(5 rows)
Lieferungen zwischen 100 und 500 Einheiten
-- Lieferungen zwischen 100 und 500 Einheiten
SELECT lieferant, material, menge
FROM lieferungen
WHERE menge BETWEEN 100 AND 500;
lieferant | material | menge
----------------------+--------------------+-------
Stahlwerk Salzgitter | Stahlblech 2mm | 500
Aluminium AG | Aluminiumprofile | 200
Kunststoff GmbH | PVC-Platten | 150
Aluminium AG | Aluminiumblech 3mm | 300
Elektronik Weber | Sensoren Typ A | 100
(5 rows)
Lieferungen von bestimmten Lieferanten
-- Lieferungen von bestimmten Lieferanten
SELECT lieferant, material, menge
FROM lieferungen
WHERE lieferant IN ('Aluminium AG', 'Elektronik Weber');
Das ist äquivalent zu:
WHERE lieferant = 'Aluminium AG' OR lieferant = 'Elektronik Weber'
lieferant | material | menge
------------------+--------------------+-------
Aluminium AG | Aluminiumprofile | 200
Aluminium AG | Aluminiumblech 3mm | 300
Elektronik Weber | Sensoren Typ A | 100
Elektronik Weber | Kabelbaum 5m | 50
(4 rows)
Muster mit LIKE
Oft wissen wir nicht genau, nach welchem exakten Wert wir suchen. Zum Beispiel:
- "Alle Materialien, die mit 'Stahl' beginnen"
- "Alle Lieferanten, die 'GmbH' im Namen haben"
- "Alle Materialien mit einem Namen der Länge 10"
Für solche Mustersuchen verwenden wir den LIKE-Operator zusammen mit Platzhaltern. Platzhalter sind spezielle Zeichen, welche für ein beliebiges oder mehrere beliebige Zeichen stehen. Die mag im ersten Moment etwas verwirrend klingen, ist aber in der Praxis durchaus praktisch. Im Grunde gibt es zwei verschiedene Platzhalter:
Mit diesen zwei Zeichen und deren Bedeutung können wir bereits komplexere Abfragen erstellen. Beispielhaft Muster für LIKE Abfragen sind
Alle Lieferanten filtern
-- Alle Lieferanten mit 'GmbH' im Namen
SELECT lieferant, material
FROM lieferungen
WHERE lieferant LIKE '%GmbH%';
lieferant | material
-----------------+----------------
Kunststoff GmbH | PVC-Platten
Kunststoff GmbH | Gummiband 10mm
(2 rows)
weitere Beispiele
Alle Materialien die mit 'Stahl' beginnen
-- Alle Materialien die mit 'Stahl' beginnen
SELECT lieferant, material
FROM lieferungen
WHERE material LIKE 'Stahl%';
lieferant | material
----------------------+--------------------
Stahlwerk Salzgitter | Stahlblech 2mm
Stahlwerk Salzgitter | Stahlprofil IPE200
(2 rows)
Alle Lieferanten deren Name mit 'Weber' endet
-- Alle Lieferanten deren Name mit 'Weber' endet
SELECT lieferant, material
FROM lieferungen
WHERE lieferant LIKE '%Weber';
lieferant | material
------------------+----------------
Elektronik Weber | Sensoren Typ A
Elektronik Weber | Kabelbaum 5m
(2 rows)
Alle Materialien die 'blech' im Namen enthalten
-- Alle Materialien die 'blech' enthalten
SELECT lieferant, material
FROM lieferungen
WHERE material LIKE '%blech%';
lieferant | material
----------------------+--------------------
Stahlwerk Salzgitter | Stahlblech 2mm
Aluminium AG | Aluminiumblech 3mm
(2 rows)
Groß-/Kleinschreibung
LIKE ist in PostgreSQL standardmäßig case-sensitive. Dies bedeutet, es wird sehr genau zwischen Groß- und Kleinschreibung unterschieden!
LIKE 'stahl%'findet NICHT "Stahlblech"LIKE 'Stahl%'findet "Stahlblech"
Für case-insensitive Suche verwende ILIKE:
SELECT material FROM lieferungen WHERE material ILIKE 'stahl%'; -- findet "Stahlblech"
material
--------------------
Stahlblech 2mm
Stahlprofil IPE200
(2 rows)
Sortieren mit ORDER BY
Standardmäßig werden Abfrageergebnisse in keiner bestimmten Reihenfolge zurückgegeben - die Datenbank entscheidet selbst, wie sie die Daten ausgibt. Wenn wir eine definierte Sortierung benötigen (z.B. alphabetisch, nach Datum, nach Zahlen), verwenden wir ORDER BY.
Mit ORDER BY können wir Ergebnisse nach einer oder mehreren Spalten sortieren - sowohl aufsteigend (A→Z, 0→9, alt→neu) als auch absteigend (Z→A, 9→0, neu→alt). Der grundlegende Syntax lautet wiefolgt:
SELECT *
FROM tabellenname
ORDER BY attribut ASC; -- oder DESC
Die zwei verschiedenen Sortierrichtungen können mit Hilfe zweier Schlüsselwörter ausgewählt werden:
Lieferant und Menge sortieren
-- Erst nach Lieferant (A-Z), dann nach Menge (größte zuerst)
SELECT lieferant, material, menge
FROM lieferungen
ORDER BY lieferant ASC, menge DESC;
lieferant | material | menge
----------------------+-----------------------+-------
Aluminium AG | Aluminiumblech 3mm | 300
Aluminium AG | Aluminiumprofile | 200
Elektronik Weber | Sensoren Typ A | 100
Elektronik Weber | Kabelbaum 5m | 50
Kunststoff GmbH | Gummiband 10mm | 800
Kunststoff GmbH | PVC-Platten | 150
Schrauben Meyer | Sechskantschrauben M8 | 5000
Schrauben Meyer | Muttern M8 | 5000
Stahlwerk Salzgitter | Stahlblech 2mm | 500
Stahlwerk Salzgitter | Stahlprofil IPE200 | 80
(10 rows)
Erklärung: Die Daten werden zuerst nach lieferant alphabetisch sortiert. Innerhalb jedes Lieferanten werden die Materialien nach menge absteigend sortiert (größte zuerst).
weitere Beispiele
Nach Material sortiert (A-Z)
-- Nach Material sortiert (A-Z)
SELECT material, lieferant
FROM lieferungen
ORDER BY material; -- ASC ist Standard und kann weggelassen werden
material | lieferant
-----------------------+----------------------
Aluminiumblech 3mm | Aluminium AG
Aluminiumprofile | Aluminium AG
Gummiband 10mm | Kunststoff GmbH
Kabelbaum 5m | Elektronik Weber
Muttern M8 | Schrauben Meyer
PVC-Platten | Kunststoff GmbH
Sechskantschrauben M8 | Schrauben Meyer
Sensoren Typ A | Elektronik Weber
Stahlblech 2mm | Stahlwerk Salzgitter
Stahlprofil IPE200 | Stahlwerk Salzgitter
(10 rows)
Nach Lieferdatum sortiert (neueste zuerst)
-- Nach Lieferdatum sortiert (neueste zuerst)
SELECT material, lieferdatum
FROM lieferungen
ORDER BY lieferdatum DESC;
material | lieferdatum
-----------------------+-------------
Kabelbaum 5m | 2024-03-28
Aluminiumblech 3mm | 2024-03-25
Stahlprofil IPE200 | 2024-03-22
Gummiband 10mm | 2024-03-21
PVC-Platten | 2024-03-20
Sensoren Typ A | 2024-03-19
Aluminiumprofile | 2024-03-18
Muttern M8 | 2024-03-17
Sechskantschrauben M8 | 2024-03-16
Stahlblech 2mm | 2024-03-15
(10 rows)
Alternativ kann man auch die Position der Spalte im SELECT angeben:
SELECT lieferant, material, lieferdatum
FROM lieferungen
ORDER BY 3 DESC; -- Sortiere nach der 3. Spalte (lieferdatum)
Nicht empfohlen!
Diese Schreibweise ist weniger lesbar und sollte nur in Ausnahmefällen verwendet werden. Besser ist es, den Spaltennamen explizit anzugeben: ORDER BY lieferdatum DESC
Sortierung und NULL-Werte
Was passiert eigentlich, wenn eine Spalte NULL-Werte (leere Einträge) enthält?
Standardverhalten
Das Standardverhalten in PostgreSQL ist:
- Bei
ASC(aufsteigend): NULL-Werte kommen am Ende - Bei
DESC(absteigend): NULL-Werte kommen am Anfang
Doch dieses Verhalten ist nicht in Stein gemeiselt. Wir können dies auch gezielt steuern und festlegen.
-- NULL-Werte zuerst, dann aufsteigend sortieren
ORDER BY spalte ASC NULLS FIRST;
-- NULL-Werte am Ende, dann aufsteigend sortieren
ORDER BY spalte ASC NULLS LAST;
Ergebnismenge begrenzen mit LIMIT
Alle bisher kennengelernten Befehle liefern uns als Rückgabe sämtliche Datensätze - sofern diese die geforderten Bedingungen erfüllen. Manchmal möchten wir aber nur eine bestimmte Anzahl - zum Beispiel: Die Top 5 der teuersten Lieferungen.
Dafür gibt es die Klausel LIMIT und optional dazugehörigt OFFSET.
Klausel
Eine Klausel ist ein Teil einer SQL-Anweisung, der eine bestimmte Aufgabe hat. Beispielsweise:
SELECT-KlauselFROM-KlauselWHERE-KlauselGROUP BY-KlauselHAVING-KlauselORDER BY-Klausel
Grundlegende Verwendung
Wir starten mit der einfachen Verwendung von LIMIT zur Limitierung der Rückgabeergebnisse. Der grundlegende Syntax kann wiefolgt beschrieben werden:
SELECT spalten
FROM tabelle
ORDER BY sortierung
LIMIT anzahl;
Wenn wir diesem Syntax folgen können wir beispielsweise die ersten 3 Ergebnisse zurückgeben lassen.
Die 3 günstigsten Materialien
-- Die 3 günstigsten Materialien (nach Preis pro Einheit)
SELECT material, preis_pro_einheit, lieferant
FROM lieferungen
ORDER BY preis_pro_einheit ASC
LIMIT 3;
material | preis_pro_einheit | lieferant
-----------------------+-------------------+-----------------
Muttern M8 | 0.08 | Schrauben Meyer
Sechskantschrauben M8 | 0.15 | Schrauben Meyer
Gummiband 10mm | 2.30 | Kunststoff GmbH
(3 rows)
LIMIT ohne ORDER BY
Wenn du LIMIT ohne ORDER BY verwendest, ist das Ergebnis nicht vorhersehbar - die Datenbank gibt irgendwelche Zeilen zurück! Verwende daher immer ORDER BY zusammen mit LIMIT.
Zeilen überspringen mit OFFSET
Nun kann es vorkommen, dass wir nicht die ersten N Ergebnisse auslesen möchten, sondern erst bei einem gewissen Wert beginnend. Mit OFFSET können wir die ersten N Zeilen überspringen und erst ab einer bestimmten Position Ergebnisse zurückgeben.
SELECT spalten
FROM tabelle
ORDER BY sortierung
LIMIT anzahl OFFSET überspringen;
Werte ab 3. Position auslesen
-- Überspringe die ersten 3, zeige die nächsten 3 Lieferungen (nach Datum)
SELECT material, lieferdatum, lieferant
FROM lieferungen
ORDER BY lieferdatum ASC
LIMIT 3 OFFSET 3;
material | lieferdatum | lieferant
------------------+-------------+------------------
Aluminiumprofile | 2024-03-18 | Aluminium AG
Sensoren Typ A | 2024-03-19 | Elektronik Weber
PVC-Platten | 2024-03-20 | Kunststoff GmbH
(3 rows)
Erklärung: Die ersten 3 Lieferungen (15., 16., 17. März) werden übersprungen, dann werden die nächsten 3 zurückgegeben.
Paginierung
LIMIT und OFFSET werden häufig für Paginierung verwendet - z.B. wenn eine Webseite Ergebnisse seitenweise anzeigt.
Bei sehr großen OFFSET-Werten (z.B. OFFSET 10000) kann die Performance leiden, da die Datenbank alle übersprungenen Zeilen dennoch laden und durchlaufen muss. Für große Datensätze gibt es bessere Ansätze (z.B. Cursor-basierte Paginierung).
Aggregatfunktionen - Daten zusammenfassen
Bisher haben wir einzelne Datensätze abgefragt - jede Zeile wurde einzeln zurückgegeben. Manchmal interessieren uns aber zusammengefasste Informationen über viele Datensätze:
- Wie viele Lieferungen haben wir insgesamt?
- Was ist die durchschnittliche Menge?
- Was ist der höchste oder niedrigste Preis?
- Was ist die Summe aller Mengen?
Für solche statistischen Auswertungen verwenden wir Aggregatfunktionen. Sie fassen mehrere Zeilen zu einem einzigen Ergebniswert zusammen.
SELECT funktion AS ergebnisname
FROM tabelle;
Neben der Funktion ist es sinnvoll einen Alias Namen für das Ergebnis der Berechnung mittels AS zum vergeben.
Günstigster und teuerster Preis pro Einheit
-- Günstigster und teuerster Preis pro Einheit
SELECT
MIN(preis_pro_einheit) AS guenstigster_preis,
MAX(preis_pro_einheit) AS teuerster_preis
FROM lieferungen;
guenstigster_preis | teuerster_preis
--------------------+-----------------
0.08 | 45.00
(1 row)
Erklärung: Wir wir sehen, können wir mehrere Aggregatfunktionen in einer Abfrage kombinieren.
weitere Beispiele
Wie viele Lieferungen gibt es insgesamt?
-- Wie viele Lieferungen gibt es insgesamt?
SELECT COUNT(*) AS anzahl_lieferungen
FROM lieferungen;
anzahl_lieferungen
--------------------
10
(1 row)
Erklärung: COUNT(*) zählt alle Zeilen in der Tabelle - unabhängig vom Inhalt.
Wie viele Lieferungen sind bereits geliefert?
-- Wie viele Lieferungen sind bereits geliefert?
SELECT COUNT(*) AS anzahl_geliefert
FROM lieferungen
WHERE status = 'Geliefert';
anzahl_geliefert
------------------
5
(1 row)
Erklärung: COUNT(*) zählt alle Zeilen in der Tabelle - unabhängig vom Inhalt. WHERE status = 'Geliefert' filtert die Zeilen, die den Status "Geliefert" haben.
Durchschnittlicher Preis pro Einheit
-- Durchschnittlicher Preis pro Einheit
SELECT AVG(preis_pro_einheit) AS durchschnittspreis
FROM lieferungen;
durchschnittspreis
---------------------
14.7180000000000000
(1 row)
Erklärung: AVG() berechnet den arithmetischen Mittelwert aller Preise pro Einheit.
Gesamtmenge aller Lieferungen
-- Gesamtmenge aller Lieferungen
SELECT SUM(menge) AS gesamtmenge
FROM lieferungen;
gesamtmenge
-------------
12180
(1 row)
Hinweis: SUM() funktioniert nur mit numerischen Spalten (INTEGER, NUMERIC, etc.)
NULL-Werte werden ignoriert
Aggregatfunktionen (außer COUNT(*)) ignorieren NULL-Werte!
COUNT(spalte)zählt nur Nicht-NULL-WerteAVG(spalte)berechnet den Durchschnitt nur aus vorhandenen WertenSUM(spalte)summiert nur vorhandene Werte
Gruppieren mit GROUP BY
Mit den Aggregatfunktionen können wir bereits einfache Analysen unseres Datensatzes durchführen. Wir können Summen bilden, Mittelwerte berechnen oder Werte zählen. Doch häufig kann es sein, dass wir diese Analysen nach gewissen Gruppen unterteilen wollen:
- Wie viele Lieferungen gibt es pro Lieferant?
- Was ist die durchschnittliche Menge pro Zielstandort?
- Wie viele Lieferungen gibt es pro Status?
Um diese Fragen zu beantworten, verwenden wir GROUP BY - es fasst Zeilen mit gleichen Werten zusammen und erlaubt Aggregationen pro Gruppe.
SELECT gruppenspalte, aggregatfunktion(spalte)
FROM tabelle
GROUP BY gruppenspalte;
Wie viele Lieferungen gibt es pro Lieferant?
-- Wie viele Lieferungen gibt es pro Lieferant?
SELECT lieferant, COUNT(*) AS anzahl
FROM lieferungen
GROUP BY lieferant;
lieferant | anzahl
----------------------+--------
Schrauben Meyer | 2
Stahlwerk Salzgitter | 2
Elektronik Weber | 2
Aluminium AG | 2
Kunststoff GmbH | 2
(5 rows)
Erklärung: Die Datenbank gruppiert alle Lieferungen nach lieferant und zählt, wie viele Lieferungen in jeder Gruppe sind.
weiteres Beispiel
Günstigste und teuerste Lieferung pro Lieferant
-- Günstigste und teuerste Lieferung pro Lieferant
SELECT
lieferant,
MIN(preis_pro_einheit) AS guenstigster,
MAX(preis_pro_einheit) AS teuerster
FROM lieferungen
GROUP BY lieferant;
lieferant | guenstigster | teuerster
----------------------+--------------+-----------
Schrauben Meyer | 0.08 | 0.15
Stahlwerk Salzgitter | 12.50 | 45.00
Elektronik Weber | 12.90 | 22.00
Aluminium AG | 18.50 | 25.00
Kunststoff GmbH | 2.30 | 8.75
(5 rows)
Die GROUP BY Klausel wird fast ausschließlich in Kombination mit einer Aggregatfunktion verwendet. Prinzipiell ist es syntaktisch erlaubt ein GROUP BY ohne Aggregation zu verwenden - semantisch ist es aber meist sinnlos. Das Ergebnis wäre ident mit jenem von DISTINCT - es liefert nur die einzigartigen Werte einer Spalte.
Spannend wird es, wenn wir mehrere Aggregatfunktionen gleichzeitig auf dieselbe Gruppierung anwenden. Damit können wir umfassendere Statistiken über unseren Datensatz erzeugen.
Umfassende Statistik pro Zielstandort
SELECT
zielstandort,
COUNT(*) AS anzahl,
SUM(menge) AS gesamtmenge,
AVG(preis_pro_einheit) AS durchschnittspreis
FROM lieferungen
GROUP BY zielstandort
ORDER BY anzahl DESC;
zielstandort | anzahl | gesamtmenge | durchschnittspreis
--------------+--------+-------------+---------------------
Lager Nord | 5 | 11450 | 4.7560000000000000
Lager Ost | 3 | 230 | 26.6333333333333333
Lager Sued | 2 | 500 | 21.7500000000000000
(3 rows)
Erklärung: Für jeden Zielstandort sehen wir die Anzahl der Lieferungen, die Gesamtmenge und den durchschnittlichen Preis pro Einheit.
Um unsere Analyse noch weiter zu verfeinern, können wir auch mehreren Spalten gleichzeitig gruppieren. Dabei wird jede einzigartige Kombination der Spaltenwerte separat aufgeführt und die entsprechenden Analysen durchgeführt.
Gruppierung nach Zielstandort UND Status
SELECT
zielstandort,
status,
COUNT(*) AS anzahl
FROM lieferungen
GROUP BY zielstandort, status
ORDER BY anzahl DESC;
zielstandort | status | anzahl
--------------+------------+--------
Lager Nord | Geliefert | 3
Lager Nord | In Transit | 2
Lager Ost | Bestellt | 2
Lager Ost | Geliefert | 1
Lager Sued | Bestellt | 1
Lager Sued | Geliefert | 1
(6 rows)
Erklärung: Jede Kombination aus zielstandort und status bildet eine eigene Gruppe.
SELECT-Regel für GROUP BY
Wenn du GROUP BY verwendest, dürfen nur
- Spalten, die in GROUP BY stehen
- Aggregatfunktionen
in der SELECT Klausel vorkommen. Dies bedeutet, dass nachfolgendes Beispiel zu einem Fehler führen würde:
SELECT lieferant, material, COUNT(*) AS anzahl -- ✗ material steht nicht in GROUP BY!
FROM lieferungen
GROUP BY lieferant;
ERROR: Spalte »lieferungen.material« muss in der GROUP-BY-Klausel erscheinen oder in einer Aggregatfunktion verwendet werden
LINE 1: SELECT lieferant, material, COUNT(*) AS anzahl -- ? materia...
^
Warum? Wenn wir nach lieferant gruppieren, gibt es in der Gruppe "Aluminium AG" zwei verschiedene Materialien ("Aluminiumprofile" und "Aluminiumblech 3mm"). Die Datenbank weiß nicht, welches sie anzeigen soll!
ORDER BY mit GROUP BY
Nach der Gruppierung können wir das Ergebnis mit ORDER BY sortieren:
SELECT lieferant, COUNT(*) AS anzahl
FROM lieferungen
GROUP BY lieferant
ORDER BY anzahl DESC; -- Sortiere nach Anzahl (absteigend)
Wir können nach:
- Der Gruppenspalte sortieren:
ORDER BY lieferant - Einem Aggregat-Ergebnis sortieren:
ORDER BY anzahl DESC
Gruppen filtern mit HAVING
Mit WHERE haben wir bis jetzt einzelne Zeilen vor der Gruppierung filtern. Was aber, wenn wir Gruppen nach ihrer Aggregation filtern möchten?
Zum Beispiel:
- Welche Lieferanten haben mehr als 100 Einheiten geliefert?
- Welche Zielstandorte haben einen durchschnittlichen Preis über 20 EUR?
Hier kommt HAVING ins Spiel - es filtert Gruppen nach Aggregationsergebnissen.
SELECT gruppenspalte, aggregatfunktion(spalte)
FROM tabelle
GROUP BY gruppenspalte
HAVING bedingung_für_aggregat;
Der Unterschied zwischen WHERE und HAVING kann wiefolgt zusammengefasst werden
Zielstandorte mit durchschnittlichem Preis > 10 EUR
-- Zielstandorte mit durchschnittlichem Preis > 10 EUR
SELECT
zielstandort,
AVG(preis_pro_einheit) AS durchschnittspreis,
COUNT(*) AS anzahl
FROM lieferungen
GROUP BY zielstandort
HAVING AVG(preis_pro_einheit) > 10;
zielstandort | durchschnittspreis | anzahl
--------------+---------------------+--------
Lager Ost | 26.6333333333333333 | 3
Lager Sued | 21.7500000000000000 | 2
(2 rows)
Erklärung: Nur Zielstandorte, deren durchschnittlicher Preis über 10 EUR liegt, werden angezeigt.
weitere Beispiele
Lieferanten mit mehr als 1 Lieferung
-- Lieferanten mit mehr als 1 Lieferung
SELECT lieferant, COUNT(*) AS anzahl
FROM lieferungen
GROUP BY lieferant
HAVING COUNT(*) > 1;
lieferant | anzahl
----------------------+--------
Schrauben Meyer | 2
Stahlwerk Salzgitter | 2
Elektronik Weber | 2
Aluminium AG | 2
Kunststoff GmbH | 2
(5 rows)
Erklärung: Erst werden die Lieferungen nach Lieferant gruppiert und gezählt. Dann werden nur die Gruppen angezeigt, die mehr als 1 Lieferung haben.
Zielstandorte mit mehr als 1 gelieferter Lieferung
-- Zielstandorte mit mehr als 1 gelieferter Lieferung
SELECT zielstandort, COUNT(*) AS anzahl_geliefert
FROM lieferungen
WHERE status = 'Geliefert' -- Filtert Zeilen VOR Gruppierung
GROUP BY zielstandort
HAVING COUNT(*) > 1; -- Filtert Gruppen NACH Aggregation
zielstandort | anzahl_geliefert
--------------+------------------
Lager Nord | 3
(1 row)
Ablauf:
WHEREfiltert alle Zeilen → nur Lieferungen mit Status "Geliefert"GROUP BYgruppiert nach ZielstandortCOUNT(*)zählt Lieferungen pro ZielstandortHAVINGfiltert Gruppen → nur Standorte mit mehr als 1 Lieferung
Lieferanten mit Gesamtmenge > 200 UND durchschnittlichem Preis < 20
-- Lieferanten mit Gesamtmenge > 200 UND durchschnittlichem Preis < 20
SELECT
lieferant,
SUM(menge) AS gesamtmenge,
AVG(preis_pro_einheit) AS durchschnittspreis
FROM lieferungen
GROUP BY lieferant
HAVING SUM(menge) > 200 AND AVG(preis_pro_einheit) < 20;
lieferant | gesamtmenge | durchschnittspreis
-----------------+-------------+------------------------
Schrauben Meyer | 10000 | 0.11500000000000000000
Kunststoff GmbH | 950 | 5.5250000000000000
(2 rows)
Erklärung: Kombiniert mehrere HAVING-Bedingungen mit AND - nur Lieferanten mit Gesamtmenge > 200 UND durchschnittlichem Preis < 20.
HAVING ohne GROUP BY?
Technisch ist HAVING ohne GROUP BY erlaubt - die gesamte Tabelle wird dann als eine einzige Gruppe behandelt was in den allermeisten Fällen sinnlos ist:
SELECT COUNT(*) AS anzahl
FROM lieferungen
HAVING COUNT(*) > 5;
Eindeutige Werte mit DISTINCT
Manchmal möchten wir wissen, welche verschiedenen Werte in einer Spalte vorkommen - ohne Duplikate. Zum Beispiel: Welche verschiedenen Lieferanten gibt es?
Dafür verwenden wir DISTINCT - es entfernt Duplikate und zeigt jeden Wert nur einmal.
SELECT DISTINCT spalte
FROM tabelle;
Welche verschiedenen Lieferanten gibt es?
-- Welche verschiedenen Lieferanten gibt es?
SELECT DISTINCT lieferant
FROM lieferungen;
lieferant
----------------------
Schrauben Meyer
Stahlwerk Salzgitter
Elektronik Weber
Aluminium AG
Kunststoff GmbH
(5 rows)
Erklärung: Obwohl es 10 Lieferungen gibt, werden nur die 5 verschiedenen Lieferanten angezeigt (ohne Wiederholungen).
weitere Beispiele
Welche einzigartigen Kombinationen von Zielstandort und Status gibt es?
-- Welche einzigartigen Kombinationen von Zielstandort und Status gibt es?
SELECT DISTINCT zielstandort, status
FROM lieferungen
ORDER BY zielstandort, status;
zielstandort | status
--------------+------------
Lager Nord | Geliefert
Lager Nord | In Transit
Lager Ost | Bestellt
Lager Ost | Geliefert
Lager Sued | Bestellt
Lager Sued | Geliefert
(6 rows)
Erklärung: DISTINCT arbeitet hier auf der Kombination beider Spalten - jede einzigartige Kombination wird nur einmal angezeigt.
Alle Zielstandorte alphabetisch sortiert
-- Alle Zielstandorte alphabetisch sortiert
SELECT DISTINCT zielstandort
FROM lieferungen
ORDER BY zielstandort;
zielstandort
--------------
Lager Nord
Lager Ost
Lager Sued
(3 rows)
Erklärung: DISTINCT kann mit ORDER BY kombiniert werden, um die eindeutigen Werte sortiert auszugeben.
Welche Lieferanten haben bereits geliefert?
-- Welche Lieferanten haben bereits geliefert?
SELECT DISTINCT lieferant
FROM lieferungen
WHERE status = 'Geliefert';
lieferant
----------------------
Aluminium AG
Elektronik Weber
Schrauben Meyer
Stahlwerk Salzgitter
(4 rows)
Erklärung: Erst werden die Zeilen mit WHERE gefiltert, dann werden die eindeutigen Lieferanten ermittelt.
Performance-Hinweis
DISTINCT kann bei großen Tabellen langsam sein, da die Datenbank alle Zeilen verarbeiten und Duplikate entfernen muss. Wenn möglich, kombiniere es mit WHERE, um die Datenmenge vorher zu reduzieren:
-- Besser: Erst filtern, dann DISTINCT
SELECT DISTINCT lieferant
FROM lieferungen
WHERE preis_pro_einheit > 10; -- Reduziert Datenmenge
Übung ✍️
Nun ist es an der Zeit, unser erlerntes Wissen auf unser Gesamtprojekt anzuwenden! In diesem Kapitel erweitern wir das Produktionsplanungssystem für TecGuy GmbH.
Im vorherigen Kapitel haben wir die Datenbank produktionsplanung_db mit den Tabellen produktionsauftraege und maschinen erstellt.
Übungsvorbereitung - Datenbank zurücksetzen
Falls du das vorherige Kapitel nicht abgeschlossen hast oder neu starten möchtest, führe nachfolgendes Setup aus. Es löscht alle bestehenden Daten und erstellt den korrekten Ausgangszustand für dieses Kapitel.
Setup
-- Zu anderer Datenbank wechseln
\c postgres
-- Zur Datenbank wechseln (oder neu erstellen)
DROP DATABASE IF EXISTS produktionsplanung_db;
CREATE DATABASE produktionsplanung_db;
\c produktionsplanung_db
-- Tabelle für Maschinen erstellen
CREATE TABLE maschinen (
maschinen_id INTEGER PRIMARY KEY,
maschinenname VARCHAR(100),
maschinentyp VARCHAR(50),
produktionshalle VARCHAR(50),
anschaffungsjahr INTEGER,
maschinenstatus VARCHAR(20),
wartungsintervall_tage INTEGER
);
-- Tabelle für Produktionsaufträge erstellen
CREATE TABLE produktionsauftraege (
auftrag_id INTEGER PRIMARY KEY,
auftragsnummer VARCHAR(20),
kunde VARCHAR(100),
produkt VARCHAR(100),
menge INTEGER,
startdatum DATE,
lieferdatum DATE,
status VARCHAR(20),
maschinen_id INTEGER
);
-- Maschinen-Daten einfügen
INSERT INTO maschinen VALUES
(1, 'CNC-Fraese Alpha', 'CNC-Fraese', 'Halle A', 2020, 'Aktiv', 90),
(2, 'Drehbank Delta', 'Drehbank', 'Halle A', 2018, 'Aktiv', 120),
(3, 'Presse Gamma', 'Presse', 'Halle B', 2019, 'Wartung', 60),
(4, 'Schweissroboter Beta', 'Schweissroboter', 'Halle C', 2021, 'Aktiv', 90);
-- Produktionsaufträge-Daten einfügen (erste 4 aus Kapitel 1)
INSERT INTO produktionsauftraege VALUES
(1, 'AUF-2024-001', 'BMW AG', 'Getriebegehäuse', 500, '2024-04-01', '2024-04-15', 'In Produktion', 1),
(2, 'AUF-2024-002', 'Audi AG', 'Kurbelwelle', 200, '2024-04-10', '2024-04-20', 'Geplant', 2),
(3, 'AUF-2024-003', 'Mercedes-Benz', 'Pleuelstange', 350, '2024-04-05', '2024-04-18', 'In Produktion', 2),
(4, 'AUF-2024-004', 'Porsche AG', 'Kolben', 150, '2024-04-12', '2024-04-25', 'Geplant', 4);
Wir erweitern nun unsere Tabelle mit zusätzlichen Produktionsaufträgen, um unsere Analysen aussagekräftiger zu machen:
-- Weitere Produktionsaufträge hinzufügen
INSERT INTO produktionsauftraege VALUES
(5, 'AUF-2024-005', 'BMW AG', 'Kurbelwelle', 300, '2024-04-15', '2024-04-22', 'In Produktion', 2),
(6, 'AUF-2024-006', 'Volkswagen AG', 'Kolben', 400, '2024-04-20', '2024-04-28', 'Geplant', 4),
(7, 'AUF-2024-007', 'Mercedes-Benz', 'Getriebegehäuse', 250, '2024-04-22', '2024-04-30', 'Abgeschlossen', 1),
(8, 'AUF-2024-008', 'Audi AG', 'Pleuelstange', 180, '2024-04-08', '2024-04-16', 'Abgeschlossen', 2),
(9, 'AUF-2024-009', 'Porsche AG', 'Kurbelwelle', 120, '2024-04-28', '2024-05-05', 'Geplant', 2),
(10, 'AUF-2024-010', 'BMW AG', 'Kolben', 350, '2024-04-12', '2024-04-19', 'In Produktion', 4);
Aufgabe 1: Einfache Abfragen mit WHERE
Zeige alle Aufträge ...
- ... von BMW AG
- ... mit Status "In Produktion"
- ... mit einer Menge größer als 200
- ... für das Produkt "Kolben"
💡 Tip anzeigen
WHERE&=WHERE&=WHERE&>WHERE&=
⚡ Lösung anzeigen
- BVW AG
SELECT * FROM produktionsauftraege WHERE kunde = 'BMW AG'; - In Produktion
SELECT * FROM produktionsauftraege WHERE status = 'In Produktion'; - Menge > 200
SELECT * FROM produktionsauftraege WHERE menge > 200; - Kolben
SELECT * FROM produktionsauftraege WHERE produkt = 'Kolben';
Aufgabe 2: Verknüpfte Bedingungen
Zeige alle Aufträge ...
- ... von BMW AG, die in Produktion sind
- ... mit Lieferdatum zwischen dem 15. und 20. April
- ... von BMW AG, Audi AG oder Mercedes-Benz
- ... die nicht den Status "Geplant" haben
💡 Tip anzeigen
WHERE&ANDWHERE,AND&BETWEENWHERE&INWHERE NOT
⚡Lösung anzeigen
-
BMW AG & in Produktion
SELECT * FROM produktionsauftraege WHERE kunde = 'BMW AG' AND status = 'In Produktion'; -
Lieferdatum
SELECT * FROM produktionsauftraege WHERE lieferdatum BETWEEN '2024-04-15' AND '2024-04-20'; -
BMW AG, Audi AG & Mercedes Benz
SELECT * FROM produktionsauftraege WHERE kunde IN ('BMW AG', 'Audi AG', 'Mercedes-Benz'); -
Nicht Status geplant
SELECT * FROM produktionsauftraege WHERE NOT status = 'Geplant'; -- Alternativ: -- WHERE status != 'Geplant';
Aufgabe 3: Sortierung und Begrenzung
- Sortiere alle Aufträge nach Lieferdatum (früheste zuerst)
- Zeige die 3 größten Aufträge (nach Menge)
- Sortiere alle Aufträge erst nach Kunde (A-Z), dann nach Menge (größte zuerst)
- Zeige die Aufträge 4-6, wenn nach Lieferdatum sortiert
💡 Tip anzeigen
ORDER BY&ASCORDER BY,DESC&LIMITORDER BY,ASC&DESCORDER BY,ASC,LIMIT&OFFSET
⚡Lösung anzeigen
-
Sortieren nach Lieferdatum (früheste zuerst)
SELECT * FROM produktionsauftraege ORDER BY lieferdatum ASC; -
Drei größten Aufträge
SELECT auftragsnummer, kunde, produkt, menge FROM produktionsauftraege ORDER BY menge DESC LIMIT 3; -
Sortiree nach Kunde (A-Z) und Menge (größte zuerst)
SELECT kunde, produkt, menge FROM produktionsauftraege ORDER BY kunde ASC, menge DESC; -
Zeige Aufträge 4-6 nach Lieferdatum
SELECT auftragsnummer, kunde, produkt, lieferdatum FROM produktionsauftraege ORDER BY lieferdatum ASC LIMIT 3 OFFSET 3;
Aufgabe 4: Aggregationen und Statistiken
- Wie viele Produktionsaufträge gibt es insgesamt?
- Was ist die Gesamtmenge aller Aufträge?
- Was ist die durchschnittliche Menge pro Auftrag?
- Was ist die kleinste und größte Menge in einem Auftrag?
💡 Tip anzeigen
COUNT&ASSUM&ASAVG&ASMIN,MAX&AS
⚡Lösung anzeigen
-
Anzahl Produktionsaufträge
SELECT COUNT(*) AS anzahl_auftraege FROM produktionsauftraege; -
Gesamtmenge
SELECT SUM(menge) AS gesamtmenge FROM produktionsauftraege; -
Durchschnitt
SELECT AVG(menge) AS durchschnittliche_menge FROM produktionsauftraege; -
Kleinste und größte Menge
SELECT MIN(menge) AS kleinste_menge, MAX(menge) AS groesste_menge FROM produktionsauftraege;
Aufgabe 5: Gruppierungen und Analysen
- Wie viele Aufträge gibt es pro Kunde?
- Was ist die Gesamtmenge pro Produkt?
- Wie viele Aufträge gibt es pro Status?
- Was ist die durchschnittliche Menge pro Kunde?
💡 Tip anzeigen
COUNT,GROUP BY&ORDER BYSUM,GROUP BY&ORDER BYCOUNT,GROUP BY&ORDER BYAVG,COUNT,GROUP BY&ORDER BY
⚡Lösung anzeigen
-
Aufträge pro Kunde
SELECT kunde, COUNT(*) AS anzahl_auftraege FROM produktionsauftraege GROUP BY kunde ORDER BY anzahl_auftraege DESC; -
Gesamtmenge pro Produkt
SELECT produkt, SUM(menge) AS gesamtmenge FROM produktionsauftraege GROUP BY produkt ORDER BY gesamtmenge DESC; -
Anzahl Aufträge pro Status
SELECT status, COUNT(*) AS anzahl FROM produktionsauftraege GROUP BY status ORDER BY anzahl DESC; -
Durchschnittliche Menge pro Kunde
SELECT kunde, AVG(menge) AS durchschnittliche_menge, COUNT(*) AS anzahl_auftraege FROM produktionsauftraege GROUP BY kunde ORDER BY durchschnittliche_menge DESC;
Aufgabe 6: Erweiterte Analysen mit HAVING
- Welche Kunden haben mehr als 2 Aufträge?
- Welche Produkte haben eine Gesamtmenge über 600?
- Welche Kunden haben eine durchschnittliche Menge über 250?
- Welche Produkte haben mehr als 1 Auftrag UND eine Gesamtmenge über 700?
💡 Tip anzeigen
GROUP BY,HAVING&COUNTGROUP BY,HAVING&SUMGROUP BY,HAVING,AVG&ORDER BYGROUP BY,HAVING,COUNT,SUM&ORDER BY
⚡Lösung anzeigen
-
Kunde mit mehr als 2 Aufträge
SELECT kunde, COUNT(*) AS anzahl_auftraege FROM produktionsauftraege GROUP BY kunde HAVING COUNT(*) > 2; -
Produkte mit Gesamtmenge > 600
SELECT produkt, SUM(menge) AS gesamtmenge FROM produktionsauftraege GROUP BY produkt HAVING SUM(menge) > 600; -
Kunden mit durchschnittlicher Menge > 250
SELECT kunde, AVG(menge) AS durchschnittliche_menge, COUNT(*) AS anzahl_auftraege FROM produktionsauftraege GROUP BY kunde HAVING AVG(menge) > 250 ORDER BY durchschnittliche_menge DESC; -
Produkte mit > 1 Auftrag UND Gesamtmenge > 700
SELECT produkt, COUNT(*) AS anzahl_auftraege, SUM(menge) AS gesamtmenge FROM produktionsauftraege GROUP BY produkt HAVING COUNT(*) > 1 AND SUM(menge) > 700 ORDER BY gesamtmenge DESC;
Aufgabe 7: Komplexe Abfragen (Kombinationen)
- Zeige alle BMW AG Aufträge, die in Produktion sind, sortiert nach Lieferdatum
- Zeige die Top 3 Kunden nach Gesamtmenge (absteigend)
- Wie viele Aufträge haben Status "In Produktion" pro Produkt?
💡 Tip anzeigen
WHERE&ORDER BYSUM,GROUP BY,ORDER BY&LIMITCOUNT,WHERE,GROUP BY,ORDER BYCOUNT,WHERE,GROUP BY
⚡Lösung anzeigen
-
BMW AG Aufträge in Produktion, nach Lieferdatum
SELECT auftragsnummer, produkt, menge, lieferdatum FROM produktionsauftraege WHERE kunde = 'BMW AG' AND status = 'In Produktion' ORDER BY lieferdatum ASC; -
Top 3 Kunden nach Gesamtmenge
SELECT kunde, SUM(menge) AS gesamtmenge FROM produktionsauftraege GROUP BY kunde ORDER BY gesamtmenge DESC LIMIT 3; -
Aufträge "In Produktion" pro Produkt
SELECT produkt, COUNT(*) AS anzahl_in_produktion FROM produktionsauftraege WHERE status = 'In Produktion' GROUP BY produkt ORDER BY anzahl_in_produktion DESC; -
Kunden mit nur abgeschlossenen Aufträgen
SELECT kunde, COUNT(*) AS anzahl_abgeschlossen FROM produktionsauftraege WHERE status = 'Abgeschlossen' GROUP BY kunde;
Zusammenfassung 📌
- WHERE filtert Zeilen nach Bedingungen (
=,!=,>,<,>=,<=) - AND, OR, NOT kombinieren Bedingungen
- BETWEEN prüft Wertebereiche, IN prüft gegen eine Liste
- LIKE ermöglicht Mustersuche (
%und_als Platzhalter) - ORDER BY sortiert Ergebnisse (ASC aufsteigend, DESC absteigend)
- LIMIT begrenzt die Anzahl der Ergebnisse, OFFSET überspringt Zeilen
- Aggregatfunktionen (COUNT, SUM, AVG, MIN, MAX) fassen Daten zusammen
- GROUP BY gruppiert Zeilen für Aggregationen
- HAVING filtert Gruppen (ähnlich wie WHERE, aber für Gruppen)
- DISTINCT entfernt Duplikate
Im nächsten Kapitel werden wir lernen, wie man Daten verändert (INSERT, UPDATE, DELETE) und unser Produktionsplanungssystem weiter ausbauen!