FISI // SQL mit XAMPP: Abfragen auf einer Tabelle (Teil 1)

In der Abschlussprüfung bekommst du bei der Datenbankaufgabe meist ein fertiges Tabellenschema und sollst dazu SQL-Abfragen schreiben. Auf Papier, ohne Datenbank, die dir Fehler meldet. Genau deshalb lohnt es sich, vorher an einer echten Datenbank zu üben: Du siehst sofort, ob deine Abfrage das liefert, was du erwartest, und merkst dir die Syntax viel schneller. Eine kostenlose und schnell eingerichtete Übungsumgebung dafür ist XAMPP.
Das ist der erste Teil einer kleinen Serie. Hier geht es um die Umgebung und um Abfragen auf einer einzelnen Tabelle. Teil 2 behandelt Abfragen über mehrere Tabellen (JOINs und Unterabfragen), Teil 3 das Anlegen und Ändern von Tabellen, Transaktionen und Benutzerrechte. Wie man vom Datenmodell zu den Tabellen kommt, steht im Beitrag ERM und relationales Datenmodell.
Ziel
Nach dem Artikel kannst du
- erklären, aus welchen Komponenten XAMPP besteht und wofür es gedacht ist,
- eine Übungsdatenbank in XAMPP anlegen und Abfragen in phpMyAdmin oder auf der Kommandozeile ausführen,
- mit
SELECT,WHEREundORDER BYgezielt Spalten und Zeilen auswählen, - die Operatoren
AND,OR,IN,BETWEEN,LIKEundIS NULLsicher einsetzen, - mit Aggregatfunktionen,
GROUP BYundHAVINGAuswertungen erstellen, - den Unterschied zwischen
WHEREundHAVINGbegründen.
Was ist XAMPP?
XAMPP ist ein Softwarepaket der Apache Friends, das einen kompletten Webserver-Stack mit einem einzigen Installer auf den Rechner bringt. Der Name setzt sich aus den Bestandteilen zusammen:
| Buchstabe | Komponente | Aufgabe |
|---|---|---|
| X | plattformübergreifend | läuft unter Windows, Linux und macOS |
| A | Apache | Webserver, liefert Webseiten aus |
| M | MariaDB (früher MySQL) | relationales Datenbanksystem |
| P | PHP | Skriptsprache für dynamische Webseiten |
| P | Perl | weitere Skriptsprache |
Seit 2015 steckt MariaDB statt MySQL im Paket. MariaDB ist ein Fork von MySQL und für unsere Zwecke praktisch gleich zu bedienen. Im Control Panel heißt der Dienst deshalb weiterhin „MySQL“. Dazu kommt phpMyAdmin, eine Weboberfläche, über die du Datenbanken anlegst, Tabellen ansiehst und SQL-Befehle ausführst.
Wichtig für die Einordnung: XAMPP ist eine Entwicklungs- und Lernumgebung. Der Datenbankbenutzer root hat nach der Installation kein Passwort, und viele Einstellungen sind auf Bequemlichkeit statt Sicherheit ausgelegt. Einen XAMPP-Server stellt man nicht ins Internet und auch nicht ungeschützt ins Firmennetz.
Installation in Kürze
- Installer von apachefriends.org herunterladen und ausführen.
- Als Ziel
C:\xampplassen. UnterC:\Program Filesgibt es wegen der Benutzerkontensteuerung gern Probleme mit Schreibrechten. - XAMPP Control Panel starten und bei Apache und MySQL auf „Start“ klicken.
- Im Browser
http://localhost/phpmyadminaufrufen.
Apache lauscht standardmäßig auf den Ports 80 und 443, MariaDB auf Port 3306. Startet Apache nicht, belegt meist ein anderes Programm Port 80, unter Windows häufig der IIS. Wie du den Übeltäter findest, zeigt der Praxis-Abschnitt weiter unten. Wer bei den Portnummern unsicher ist, findet die Grundlagen im Beitrag zu Ports.
Die Übungsdatenbank
Für alle Beispiele nutzen wir eine kleine Inventartabelle mit Geräten eines Betriebs. Lege in phpMyAdmin über „Neu“ eine Datenbank inventar an, öffne den Reiter „SQL“ und führe das folgende Skript aus. Wie CREATE TABLE und INSERT im Detail funktionieren, kommt in Teil 3. Hier reicht es, das Skript einmal laufen zu lassen.
CREATE TABLE geraet (
inv_nr VARCHAR(10) PRIMARY KEY,
typ VARCHAR(20) NOT NULL,
hersteller VARCHAR(30) NOT NULL,
modell VARCHAR(40) NOT NULL,
preis DECIMAL(8,2) NOT NULL,
kaufdatum DATE NOT NULL,
abt_nr INT NOT NULL, -- 10 = IT, 20 = Vertrieb, 30 = Buchhaltung
pers_nr INT NULL -- NULL = keinem Mitarbeiter zugeordnet
);
INSERT INTO geraet VALUES
('NB-101','Notebook','Lenovo','ThinkPad T14',1149.00,'2024-03-12',10,1001),
('NB-102','Notebook','Lenovo','ThinkPad T14',1149.00,'2024-03-12',20,1002),
('NB-103','Notebook','Dell','Latitude 5440',989.00,'2025-01-20',20,1003),
('NB-104','Notebook','HP','EliteBook 840',1299.00,'2025-06-02',30,NULL),
('PC-201','Desktop','Dell','OptiPlex 7010',749.00,'2023-09-05',10,1004),
('PC-202','Desktop','Dell','OptiPlex 7010',749.00,'2023-09-05',30,1005),
('PC-203','Desktop','HP','Elite Mini 800',689.00,'2024-11-18',20,NULL),
('MO-301','Monitor','Dell','P2723DE',329.00,'2024-03-12',10,1001),
('MO-302','Monitor','Dell','P2723DE',329.00,'2024-03-12',20,1002),
('MO-303','Monitor','LG','27BQ75QB',279.00,'2025-01-20',30,NULL),
('DR-401','Drucker','Brother','HL-L6410DN',549.00,'2023-02-14',10,NULL),
('DR-402','Drucker','HP','LaserJet Pro 4002dn',389.00,'2025-06-02',20,NULL);
Zwölf Geräte, vier Typen, drei Abteilungen und fünf Geräte ohne festen Benutzer. Das reicht, um alle Abfragen dieses Teils nachzuvollziehen.
Der Aufbau einer SELECT-Abfrage
Eine Abfrage besteht aus Klauseln, die immer in derselben Reihenfolge stehen müssen. Pflicht sind nur SELECT und FROM, alle anderen sind optional:
SELECT spalten -- was soll angezeigt werden?
FROM tabelle -- woher kommen die Daten?
WHERE bedingung -- welche Zeilen?
GROUP BY spalten -- wie gruppieren?
HAVING bedingung -- welche Gruppen?
ORDER BY spalten -- wie sortieren?
LIMIT anzahl; -- wie viele Zeilen? (MySQL/MariaDB)
Das DBMS arbeitet die Klauseln aber nicht in der Reihenfolge ab, in der du sie schreibst. Es holt zuerst die Tabelle, filtert die Zeilen, bildet Gruppen, filtert die Gruppen und wählt erst danach die Spalten aus. Diese Reihenfolge erklärt die meisten Fehlermeldungen, die dir beim Üben begegnen werden.

Ein Beispiel: Einen Spaltenalias, den du in SELECT vergibst, kannst du in ORDER BY verwenden, weil ORDER BY danach kommt. In WHERE kennt das DBMS den Alias noch nicht.
Spalten auswählen
Mit SELECT * bekommst du alle Spalten. Für Abfragen in der Prüfung ist das fast immer falsch, denn dort steht meist ausdrücklich, welche Spalten gesucht sind. Nenne sie dann einzeln:
SELECT inv_nr, modell, preis
FROM geraet;
Spalten lassen sich mit AS umbenennen und berechnen. So zeigst du zum Beispiel den Bruttopreis mit 19 % Umsatzsteuer an:
SELECT inv_nr, preis AS netto, preis * 1.19 AS brutto
FROM geraet;
Die Tabelle selbst ändert sich dabei nicht. Die Spalte brutto existiert nur im Ergebnis.
Zeilen filtern mit WHERE
WHERE lässt nur die Zeilen durch, für die die Bedingung wahr ist. Zum Vergleichen gibt es =, <> (ungleich, in MySQL auch !=), <, >, <= und >=. Texte und Datumswerte stehen in einfachen Anführungszeichen, Zahlen nicht.
SELECT inv_nr, modell, preis
FROM geraet
WHERE typ = 'Notebook' AND preis < 1200;
| inv_nr | modell | preis |
|---|---|---|
| NB-101 | ThinkPad T14 | 1149.00 |
| NB-102 | ThinkPad T14 | 1149.00 |
| NB-103 | Latitude 5440 | 989.00 |
AND, OR und Klammern
AND bindet stärker als OR, so wie Punkt- vor Strichrechnung. Das führt zu einem der häufigsten Fehler. Gesucht sind Notebooks und Desktops, die weniger als 700 Euro gekostet haben:
-- falsch
WHERE typ = 'Notebook' OR typ = 'Desktop' AND preis < 700
-- richtig
WHERE (typ = 'Notebook' OR typ = 'Desktop') AND preis < 700
Die erste Variante liest das DBMS als „alle Notebooks, dazu Desktops unter 700 Euro“ und liefert fünf Zeilen. Die zweite liefert wie gewünscht nur PC-203. Wenn AND und OR in einer Bedingung vorkommen, setze immer Klammern.
IN, BETWEEN, LIKE und IS NULL
Für häufige Bedingungen gibt es kürzere Schreibweisen:
| Operator | Bedeutung | Beispiel |
|---|---|---|
IN (...) | Wert steht in der Liste | hersteller IN ('HP', 'LG') |
BETWEEN a AND b | Wert liegt zwischen a und b, beide Grenzen eingeschlossen | preis BETWEEN 300 AND 750 |
LIKE | Textmuster, % = beliebig viele Zeichen, _ = genau ein Zeichen | modell LIKE 'Elite%' |
IS NULL / IS NOT NULL | Feld ist leer bzw. gefüllt | pers_nr IS NULL |
NOT | verneint eine Bedingung | typ NOT IN ('Monitor') |
IN ersetzt eine Kette von OR-Vergleichen auf dieselbe Spalte. Die Abfrage „alle Geräte von HP oder LG“ liefert NB-104, PC-203, MO-303 und DR-402.
BETWEEN funktioniert auch mit Datumswerten. Alle Geräte, die 2024 gekauft wurden:
SELECT inv_nr, kaufdatum
FROM geraet
WHERE kaufdatum BETWEEN '2024-01-01' AND '2024-12-31';
Das ergibt NB-101, NB-102, PC-203, MO-301 und MO-302.
Bei LIKE beschreibst du ein Muster. 'Elite%' passt auf alles, was mit „Elite“ beginnt (EliteBook 840 und Elite Mini 800). '%dn' passt auf alles, was auf „dn“ endet. 'NB-10_' passt auf NB-101 bis NB-109, aber nicht auf NB-1010. Willst du Namen finden, die mit A oder B beginnen, brauchst du zwei LIKE-Bedingungen mit OR, denn LIKE kennt keine Liste.
NULL steht für „kein Wert vorhanden“. Das ist etwas anderes als 0 oder ein leerer Text. Deshalb funktioniert pers_nr = NULL nicht: Ein Vergleich mit NULL ergibt weder wahr noch falsch, sondern „unbekannt“, und die Zeile fällt raus. Richtig ist:
SELECT inv_nr, typ
FROM geraet
WHERE pers_nr IS NULL;
Das liefert die fünf Geräte ohne festen Benutzer, also NB-104, PC-203, MO-303, DR-401 und DR-402.
Doppelte Werte entfernen und sortieren
DISTINCT entfernt doppelte Zeilen aus dem Ergebnis. Welche Hersteller gibt es im Bestand?
SELECT DISTINCT hersteller
FROM geraet
ORDER BY hersteller;
Ergebnis: Brother, Dell, HP, LG, Lenovo. Ohne DISTINCT stünde Dell fünfmal in der Liste. DISTINCT bezieht sich immer auf die ganze Zeile. SELECT DISTINCT hersteller, typ liefert also jede Kombination aus Hersteller und Typ einmal.
ORDER BY sortiert aufsteigend (ASC, Standard) oder absteigend (DESC). Bei mehreren Spalten gilt die zweite nur, wenn die erste gleich ist. Die Richtung musst du für jede Spalte einzeln angeben:
SELECT inv_nr, modell, preis
FROM geraet
ORDER BY preis DESC, inv_nr ASC
LIMIT 3;
| inv_nr | modell | preis |
|---|---|---|
| NB-104 | EliteBook 840 | 1299.00 |
| NB-101 | ThinkPad T14 | 1149.00 |
| NB-102 | ThinkPad T14 | 1149.00 |
LIMIT gibt es in MySQL und MariaDB. Microsoft SQL Server schreibt stattdessen SELECT TOP 3 .... Ohne ORDER BY ist die Reihenfolge der Zeilen übrigens nicht festgelegt, auch wenn sie beim Testen immer gleich aussieht.
Aggregatfunktionen
Aggregatfunktionen fassen viele Zeilen zu einem einzigen Wert zusammen:
| Funktion | Ergebnis | Auf unsere Tabelle angewendet |
|---|---|---|
COUNT(*) | Anzahl der Zeilen | 12 |
COUNT(pers_nr) | Anzahl der Zeilen, in denen pers_nr nicht NULL ist | 7 |
SUM(preis) | Summe | 8648.00 |
AVG(preis) | Durchschnitt | 720.67 (gerundet) |
MIN(preis) | kleinster Wert | 279.00 |
MAX(preis) | größter Wert | 1299.00 |
Der Unterschied zwischen COUNT(*) und COUNT(spalte) ist prüfungsrelevant: Alle Aggregatfunktionen außer COUNT(*) ignorieren NULL-Werte. Das gilt auch für AVG, das nur über die gefüllten Felder mittelt.
SELECT COUNT(*) AS geraete,
ROUND(AVG(preis), 2) AS durchschnitt,
MAX(preis) AS teuerstes
FROM geraet
WHERE abt_nr = 20;
Für den Vertrieb ergibt das 5 Geräte, einen Durchschnitt von 709.00 Euro und 1149.00 Euro für das teuerste Gerät. ROUND sorgt dafür, dass der Durchschnitt mit zwei Nachkommastellen erscheint, sonst zeigt MariaDB hier deutlich mehr Stellen an.
Gruppieren mit GROUP BY
Aggregatfunktionen ohne GROUP BY liefern genau eine Ergebniszeile für die ganze (gefilterte) Tabelle. Mit GROUP BY bekommst du eine Zeile pro Gruppe. Wie viele Geräte gibt es pro Typ, und was haben sie gekostet?
SELECT typ,
COUNT(*) AS anzahl,
SUM(preis) AS summe,
ROUND(AVG(preis), 2) AS schnitt
FROM geraet
GROUP BY typ
ORDER BY anzahl DESC, typ;
| typ | anzahl | summe | schnitt |
|---|---|---|---|
| Notebook | 4 | 4586.00 | 1146.50 |
| Desktop | 3 | 2187.00 | 729.00 |
| Monitor | 3 | 937.00 | 312.33 |
| Drucker | 2 | 938.00 | 469.00 |
Desktop und Monitor haben beide drei Geräte. Weil die erste Sortierspalte hier gleich ist, entscheidet die zweite (typ) über die Reihenfolge.
Die wichtigste Regel bei GROUP BY: Jede Spalte in SELECT, die nicht in einer Aggregatfunktion steht, muss auch in GROUP BY stehen. SELECT typ, modell, COUNT(*) ... GROUP BY typ ist falsch, weil es pro Typ mehrere Modelle gibt und das DBMS nicht wissen kann, welches es anzeigen soll. Strenge Datenbanksysteme wie SQL Server oder MySQL ab Version 5.7 brechen hier mit einer Fehlermeldung ab. MariaDB in XAMPP lässt die Abfrage in der Standardeinstellung durch und zeigt irgendein Modell an. Dass es läuft, heißt also nicht, dass es richtig ist.
Gruppen filtern mit HAVING
WHERE filtert Zeilen, bevor gruppiert wird. HAVING filtert Gruppen, nachdem gruppiert wurde. Deshalb kannst du Aggregatfunktionen nur in HAVING verwenden, nicht in WHERE.

Ein Beispiel, das beide Filter braucht: Welche Abteilungen haben seit Anfang 2024 Geräte für mehr als 1500 Euro gekauft?
SELECT abt_nr,
COUNT(*) AS anzahl,
SUM(preis) AS summe
FROM geraet
WHERE kaufdatum >= '2024-01-01' -- 1. Zeilen: nur Käufe ab 2024
GROUP BY abt_nr -- 2. pro Abteilung eine Gruppe
HAVING SUM(preis) > 1500 -- 3. Gruppen: nur Summe über 1500
ORDER BY summe DESC;
| abt_nr | anzahl | summe |
|---|---|---|
| 20 | 5 | 3545.00 |
| 30 | 2 | 1578.00 |
Die IT (Abteilung 10) hat seit 2024 nur ein Notebook und einen Monitor für zusammen 1478 Euro gekauft und fällt im dritten Schritt heraus. Ihre älteren Geräte von 2023 hat schon WHERE aussortiert.
Als Faustregel für die Prüfung: Bezieht sich die Bedingung auf einzelne Datensätze („Geräte ab 2024“, „Abteilung 20“), gehört sie in WHERE. Bezieht sie sich auf ein Ergebnis pro Gruppe („mehr als 10 Mitarbeiter“, „Summe über 1500“), gehört sie in HAVING.
Praxis: XAMPP und SQL aus der PowerShell
Zwei Situationen kommen beim Arbeiten mit XAMPP immer wieder vor: Apache startet nicht, weil Port 80 belegt ist, und du willst ein Abfrageergebnis als CSV weitergeben. Beides lässt sich mit der PowerShell erledigen.
# 1. Wer belegt Port 80 oder 3306? Get-NetTCPConnection zeigt die Prozess-ID,
# Get-Process macht daraus einen lesbaren Namen.
Get-NetTCPConnection -LocalPort 80, 3306 -State Listen -ErrorAction SilentlyContinue |
Select-Object LocalPort,
OwningProcess,
@{ Name = 'Prozess'; Expression = { (Get-Process -Id $_.OwningProcess).ProcessName } }
# 2. SQL-Abfrage mit dem mitgelieferten Kommandozeilen-Client ausführen.
# --batch liefert tabulatorgetrennte Ausgabe mit Kopfzeile, ideal für ConvertFrom-Csv.
$mysql = 'C:\xampp\mysql\bin\mysql.exe'
$sql = 'SELECT typ, COUNT(*) AS anzahl, SUM(preis) AS summe FROM geraet GROUP BY typ ORDER BY typ;'
$ergebnis = & $mysql --user=root --batch --execute=$sql inventar |
ConvertFrom-Csv -Delimiter "`t"
# 3. Ergebnis anzeigen und als CSV für Excel speichern (Semikolon für deutsche Excel-Versionen)
$ergebnis | Format-Table
$ergebnis | Export-Csv -Path "$env:USERPROFILE\Desktop\geraete_nach_typ.csv" -Delimiter ';' -NoTypeInformation -Encoding UTF8
Taucht in Schritt 1 bei Port 80 der Prozess System auf, steckt meist der IIS oder ein anderer Windows-Dienst dahinter. Dann änderst du entweder den Port von Apache in C:\xampp\apache\conf\httpd.conf (Zeile Listen 80) oder beendest den anderen Dienst, wenn du ihn nicht brauchst. In Schritt 2 funktioniert der Aufruf ohne Passwort nur, solange root noch keins hat, was in XAMPP der Auslieferungszustand ist. Hast du eins gesetzt, ergänzt du --password und gibst es bei der Abfrage ein. Mehr zu Pipeline und berechneten Eigenschaften findest du in den PowerShell-Grundlagen.
Stolperfallen
Aggregatfunktion in WHERE. WHERE COUNT(*) > 10 oder WHERE preis > AVG(preis) geht nicht, weil WHERE vor der Gruppierung ausgewertet wird. Bedingungen auf Gruppenergebnisse gehören in HAVING. Für „teurer als der Durchschnitt aller Geräte“ brauchst du eine Unterabfrage, die kommt in Teil 2.
Spalte fehlt in GROUP BY. Jede nicht aggregierte Spalte in SELECT muss in GROUP BY stehen. Dass MariaDB die Abfrage trotzdem ausführt, rettet dich in der Prüfung nicht.
Vergleich mit NULL. = NULL und <> NULL liefern nie ein Ergebnis. Es heißt IS NULL und IS NOT NULL.
AND und OR ohne Klammern. AND wird zuerst ausgewertet. Sobald beides in einer Bedingung steht, setze Klammern, auch wenn es ohne zufällig stimmen würde.
BETWEEN falsch eingeschätzt. Beide Grenzen gehören dazu. BETWEEN 3000 AND 4000 schließt 3000 und 4000 ein. Bei Datumsangaben mit Uhrzeit (DATETIME) kann BETWEEN '2024-01-01' AND '2024-12-31' den letzten Tag abschneiden, weil '2024-12-31' als Mitternacht gelesen wird.
LIKE mit Liste. LIKE ('H%', 'M%') gibt es nicht. Jedes Muster braucht eine eigene LIKE-Bedingung, verbunden mit OR. Achte dabei wieder auf Klammern, wenn noch ein AND dazukommt.
ORDER BY mit einer Richtung für alle Spalten. ORDER BY nachname, vorname DESC sortiert den Nachnamen aufsteigend und nur den Vornamen absteigend. Die Richtung gilt immer nur für die Spalte direkt davor.
Groß- und Kleinschreibung. Mit der Standard-Sortierung in MariaDB unterscheidet WHERE typ = 'notebook' nicht zwischen „notebook“ und „Notebook“. Andere Systeme oder Einstellungen tun das sehr wohl. Schreib Werte deshalb so, wie sie in der Tabelle stehen.
Anführungszeichen. Texte und Datumswerte in einfache Anführungszeichen, Zahlen ohne. WHERE abt_nr = '20' funktioniert in MariaDB zwar, weil automatisch umgewandelt wird, ist aber unsauber und wird in der Prüfung gern angestrichen.
Fazit
Mit XAMPP hast du in wenigen Minuten eine Datenbank auf dem eigenen Rechner, an der du jede SQL-Aufgabe ausprobieren kannst. Für Abfragen auf einer Tabelle brauchst du im Kern sechs Klauseln in fester Reihenfolge: SELECT und FROM als Pflicht, dazu WHERE für Zeilen, GROUP BY mit Aggregatfunktionen für Auswertungen, HAVING für Gruppen und ORDER BY für die Sortierung. Wer sich die Verarbeitungsreihenfolge merkt, versteht auch, warum ein Alias in WHERE unbekannt ist und warum Aggregatfunktionen dort nichts verloren haben. Nimm dir die Beispieltabelle, denk dir eigene Fragen aus und prüfe deine Vorhersage gegen das echte Ergebnis. In Teil 2 kommen weitere Tabellen dazu, und dann geht es um JOINs und Unterabfragen.


