ToolBox Hub
Nahaufnahme von farbenfrohem Programmiercode auf einem Bildschirm.

SQL Spickzettel fuer Entwickler: Von den Grundlagen bis zu fortgeschrittenen Abfragen in 2026

đŸ“· Myburgh Roux / Pexels

SQL Spickzettel fuer Entwickler: Von den Grundlagen bis zu fortgeschrittenen Abfragen in 2026

Ein umfassendes SQL-Nachschlagewerk zu SELECT, JOINs, Unterabfragen, Window Functions, CTEs, Indizierung und Optimierungstipps fuer Entwickler.

19. MĂ€rz 202613 Min. Lesezeit

SQL bleibt auch 2026 eine der wichtigsten Faehigkeiten, die ein Entwickler haben kann. Ob Sie REST APIs erstellen, mit Analytics-Pipelines arbeiten oder Produktionsdaten debuggen -- Sie werden unweigerlich SQL schreiben und verstehen muessen. Dieser Spickzettel geht weit ueber die Grundlagen hinaus und bietet Ihnen eine praxisnahe Referenz, die alles von einfachen SELECT-Anweisungen bis hin zu fortgeschrittenen Window Functions und Strategien zur Abfrageoptimierung abdeckt.

Grundlegende SELECT-Abfragen

Jede SQL-Reise beginnt mit der SELECT-Anweisung. Sie ist die Grundlage aller Datenabrufoperationen.

Spalten auswaehlen

-- Bestimmte Spalten auswaehlen
SELECT first_name, last_name, email
FROM users;

-- Alle Spalten auswaehlen (in Produktionscode vermeiden)
SELECT *
FROM users;

-- Spalten mit Aliasen fuer bessere Lesbarkeit
SELECT
  first_name AS "First Name",
  last_name AS "Last Name",
  created_at AS "Registration Date"
FROM users;

-- Eindeutige Werte auswaehlen
SELECT DISTINCT department
FROM employees;

Filtern mit WHERE

Die WHERE-Klausel ermoeglicht es Ihnen, Zeilen anhand von Bedingungen zu filtern. Das Verstaendnis der Operatorprioritaet und der NULL-Behandlung ist entscheidend fuer korrekte Abfragen.

-- Einfacher Vergleich
SELECT * FROM products
WHERE price > 50.00;

-- Mehrere Bedingungen mit AND / OR
SELECT * FROM orders
WHERE status = 'shipped'
  AND total_amount > 100
  AND created_at >= '2026-01-01';

-- IN-Operator fuer mehrere Werte
SELECT * FROM users
WHERE country IN ('US', 'CA', 'GB', 'DE');

-- BETWEEN fuer Bereiche (inklusive)
SELECT * FROM events
WHERE event_date BETWEEN '2026-01-01' AND '2026-12-31';

-- Musterabgleich mit LIKE
SELECT * FROM products
WHERE name LIKE '%wireless%';  -- enthaelt 'wireless'

-- Umgang mit NULLs (= funktioniert NICHT mit NULL)
SELECT * FROM users
WHERE phone IS NULL;

SELECT * FROM users
WHERE phone IS NOT NULL;

Ein haeufiger Fehler ist WHERE column = NULL anstelle von WHERE column IS NULL zu schreiben. Ersteres ergibt immer UNKNOWN und liefert keine Zeilen zurueck.

Sortieren mit ORDER BY

-- Sortierung nach einer Spalte
SELECT * FROM products
ORDER BY price DESC;

-- Sortierung nach mehreren Spalten
SELECT * FROM employees
ORDER BY department ASC, salary DESC;

-- Sortierung nach Spaltenposition (weniger lesbar, aber gueltig)
SELECT first_name, last_name, hire_date
FROM employees
ORDER BY 3 DESC;

-- NULLS FIRST / NULLS LAST (PostgreSQL, Oracle)
SELECT * FROM tasks
ORDER BY due_date ASC NULLS LAST;

Ergebnisse begrenzen

-- PostgreSQL / MySQL
SELECT * FROM logs
ORDER BY created_at DESC
LIMIT 100;

-- Mit Offset fuer Paginierung
SELECT * FROM products
ORDER BY id
LIMIT 20 OFFSET 40;  -- Seite 3 (20 Eintraege pro Seite)

-- SQL Server
SELECT TOP 100 * FROM logs
ORDER BY created_at DESC;

-- Standard-SQL (FETCH FIRST)
SELECT * FROM logs
ORDER BY created_at DESC
FETCH FIRST 100 ROWS ONLY;

Aggregatfunktionen und GROUP BY

Aggregatfunktionen fassen mehrere Zeilen zu einem einzelnen Zusammenfassungswert zusammen. Sie werden fast immer zusammen mit GROUP BY verwendet.

-- Gaengige Aggregatfunktionen
SELECT
  department,
  COUNT(*) AS employee_count,
  AVG(salary) AS avg_salary,
  MIN(salary) AS min_salary,
  MAX(salary) AS max_salary,
  SUM(salary) AS total_payroll
FROM employees
GROUP BY department;

-- Gruppen filtern mit HAVING
SELECT
  category,
  COUNT(*) AS product_count,
  AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 10
ORDER BY avg_price DESC;

-- COUNT-Varianten
SELECT
  COUNT(*) AS total_rows,         -- zaehlt alle Zeilen einschliesslich NULLs
  COUNT(email) AS has_email,      -- zaehlt nicht-NULL E-Mails
  COUNT(DISTINCT city) AS cities  -- zaehlt eindeutige Staedte
FROM users;

Der wesentliche Unterschied: WHERE filtert einzelne Zeilen vor der Aggregation, waehrend HAVING Gruppen nach der Aggregation filtert. Sie koennen in einer WHERE-Klausel keine Aggregatfunktionen referenzieren.

JOINs: Tabellen kombinieren

JOINs sind der Punkt, an dem SQL wirklich maechtig wird. Das Verstaendnis der verschiedenen Typen ist essenziell fuer die Arbeit mit relationalen Daten.

INNER JOIN

Gibt nur Zeilen zurueck, die in beiden Tabellen uebereinstimmende Werte haben.

SELECT
  o.id AS order_id,
  o.order_date,
  c.name AS customer_name,
  c.email
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id;

LEFT JOIN (LEFT OUTER JOIN)

Gibt alle Zeilen der linken Tabelle und uebereinstimmende Zeilen der rechten Tabelle zurueck. Nicht uebereinstimmende Spalten der rechten Seite liefern NULL.

-- Alle Kunden und ihre Bestellungen finden (einschliesslich Kunden ohne Bestellungen)
SELECT
  c.name,
  c.email,
  o.id AS order_id,
  o.total_amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;

-- Kunden finden, die noch nie eine Bestellung aufgegeben haben
SELECT c.name, c.email
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.id IS NULL;

RIGHT JOIN und FULL OUTER JOIN

-- RIGHT JOIN: alle Zeilen der rechten Tabelle
SELECT
  e.name AS employee,
  d.name AS department
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.id;

-- FULL OUTER JOIN: alle Zeilen aus beiden Tabellen
SELECT
  s.name AS student,
  c.title AS course
FROM students s
FULL OUTER JOIN enrollments e ON s.id = e.student_id
FULL OUTER JOIN courses c ON e.course_id = c.id;

CROSS JOIN

Erzeugt das kartesische Produkt zweier Tabellen. Jede Zeile der ersten Tabelle wird mit jeder Zeile der zweiten Tabelle kombiniert.

-- Alle moeglichen Groessen-Farb-Kombinationen erzeugen
SELECT
  s.size_name,
  c.color_name
FROM sizes s
CROSS JOIN colors c;

Self JOIN

Eine Tabelle, die mit sich selbst verbunden wird -- nuetzlich fuer hierarchische oder vergleichende Daten.

-- Mitarbeiter und ihre Vorgesetzten finden
SELECT
  e.name AS employee,
  m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;

Unterabfragen

Unterabfragen sind Abfragen, die in andere Abfragen verschachtelt sind. Sie koennen in SELECT-, FROM-, WHERE- und HAVING-Klauseln vorkommen.

Unterabfrage in WHERE

-- Produkte finden, die ueber dem Durchschnittspreis liegen
SELECT name, price
FROM products
WHERE price > (SELECT AVG(price) FROM products);

-- Kunden finden, die in den letzten 30 Tagen bestellt haben
SELECT name, email
FROM customers
WHERE id IN (
  SELECT DISTINCT customer_id
  FROM orders
  WHERE order_date >= CURRENT_DATE - INTERVAL '30 days'
);

Korrelierte Unterabfragen

Eine korrelierte Unterabfrage referenziert Spalten der aeusseren Abfrage und wird einmal pro aeusserer Zeile ausgefuehrt. Sie sind maechtig, koennen aber bei grossen Datensaetzen langsamer als JOINs sein.

-- Mitarbeiter finden, die mehr als den Durchschnitt ihrer Abteilung verdienen
SELECT e.name, e.salary, e.department_id
FROM employees e
WHERE e.salary > (
  SELECT AVG(e2.salary)
  FROM employees e2
  WHERE e2.department_id = e.department_id
);

-- EXISTS-Pruefung
SELECT c.name
FROM customers c
WHERE EXISTS (
  SELECT 1
  FROM orders o
  WHERE o.customer_id = c.id
    AND o.total_amount > 1000
);

Unterabfrage in FROM (Abgeleitete Tabellen)

SELECT
  dept_stats.department,
  dept_stats.avg_salary,
  dept_stats.employee_count
FROM (
  SELECT
    department,
    AVG(salary) AS avg_salary,
    COUNT(*) AS employee_count
  FROM employees
  GROUP BY department
) AS dept_stats
WHERE dept_stats.employee_count > 5
ORDER BY dept_stats.avg_salary DESC;

Common Table Expressions (CTEs)

CTEs bieten eine Moeglichkeit, modulares und lesbares SQL zu schreiben. Sie definieren temporaere benannte Ergebnismengen, die fuer die Dauer einer einzelnen Abfrage existieren.

Einfacher CTE

WITH active_customers AS (
  SELECT
    c.id,
    c.name,
    c.email,
    COUNT(o.id) AS order_count,
    SUM(o.total_amount) AS total_spent
  FROM customers c
  JOIN orders o ON c.id = o.customer_id
  WHERE o.order_date >= '2026-01-01'
  GROUP BY c.id, c.name, c.email
)
SELECT *
FROM active_customers
WHERE total_spent > 500
ORDER BY total_spent DESC;

Mehrere CTEs

WITH
monthly_revenue AS (
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    SUM(total_amount) AS revenue
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY DATE_TRUNC('month', order_date)
),
revenue_with_growth AS (
  SELECT
    month,
    revenue,
    LAG(revenue) OVER (ORDER BY month) AS prev_month_revenue,
    ROUND(
      (revenue - LAG(revenue) OVER (ORDER BY month))
      / LAG(revenue) OVER (ORDER BY month) * 100, 2
    ) AS growth_pct
  FROM monthly_revenue
)
SELECT *
FROM revenue_with_growth
ORDER BY month;

Rekursive CTEs

Rekursive CTEs eignen sich ideal fuer hierarchische Daten wie Organigramme, Kategoriebaeume oder Graph-Traversierung.

WITH RECURSIVE org_chart AS (
  -- Basisfall: Top-Level-Manager (kein Vorgesetzter)
  SELECT id, name, manager_id, 1 AS level
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Rekursiver Fall: Mitarbeiter mit einem Vorgesetzten, der bereits im Ergebnis ist
  SELECT e.id, e.name, e.manager_id, oc.level + 1
  FROM employees e
  JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT
  REPEAT('  ', level - 1) || name AS org_tree,
  level
FROM org_chart
ORDER BY level, name;

Window Functions

Window Functions fuehren Berechnungen ueber eine Menge von Zeilen durch, die mit der aktuellen Zeile in Beziehung stehen, ohne sie zu einer einzelnen Ausgabezeile zusammenzufassen. Sie gehoeren zu den maechtigsten Funktionen in modernem SQL.

ROW_NUMBER, RANK und DENSE_RANK

SELECT
  name,
  department,
  salary,
  ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
  RANK() OVER (ORDER BY salary DESC) AS rank,
  DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;

Der Unterschied: Wenn zwei Mitarbeiter auf Rang 2 gleichauf liegen, weist ROW_NUMBER ihnen willkuerlich 2 und 3 zu, RANK weist beiden 2 zu und springt dann auf 4, und DENSE_RANK weist beiden 2 zu und faehrt mit 3 fort.

Partitionierte Window Functions

-- Top 3 Verdiener pro Abteilung
WITH ranked AS (
  SELECT
    name,
    department,
    salary,
    ROW_NUMBER() OVER (
      PARTITION BY department
      ORDER BY salary DESC
    ) AS dept_rank
  FROM employees
)
SELECT * FROM ranked
WHERE dept_rank <= 3;

LAG und LEAD

Diese Funktionen ermoeglichen den Zugriff auf Werte aus vorhergehenden oder nachfolgenden Zeilen.

SELECT
  order_date,
  total_amount,
  LAG(total_amount, 1) OVER (ORDER BY order_date) AS prev_day_amount,
  LEAD(total_amount, 1) OVER (ORDER BY order_date) AS next_day_amount,
  total_amount - LAG(total_amount, 1) OVER (ORDER BY order_date) AS daily_change
FROM daily_sales;

Laufende Summen und gleitende Durchschnitte

SELECT
  order_date,
  total_amount,
  SUM(total_amount) OVER (
    ORDER BY order_date
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_total,
  AVG(total_amount) OVER (
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS seven_day_avg
FROM daily_sales;

NTILE fuer Gruppierung

-- Kunden in 4 Quartile nach Ausgaben einteilen
SELECT
  name,
  total_spent,
  NTILE(4) OVER (ORDER BY total_spent DESC) AS spending_quartile
FROM customer_summary;

Indizierungsstrategien

Effizientes SQL zu schreiben bedeutet nicht nur korrekte Abfragesyntax -- es geht auch darum sicherzustellen, dass die Datenbank Ihre Abfragen schnell ausfuehren kann. Indizes sind der primaere Mechanismus dafuer.

Arten von Indizes

-- B-tree Index (Standard, gut fuer Gleichheits- und Bereichsabfragen)
CREATE INDEX idx_users_email ON users (email);

-- Zusammengesetzter Index (Spaltenreihenfolge ist wichtig)
CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date DESC);

-- Unique Index (erzwingt auch Eindeutigkeit)
CREATE UNIQUE INDEX idx_users_email_unique ON users (email);

-- Partial Index (PostgreSQL - indiziert nur eine Teilmenge der Zeilen)
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';

-- GIN Index fuer Volltextsuche (PostgreSQL)
CREATE INDEX idx_articles_search
ON articles USING GIN (to_tsvector('english', title || ' ' || body));

Wann Indizes erstellt werden sollten

Indizes beschleunigen Lesevorgaenge, verlangsamen aber Schreibvorgaenge. Erstellen Sie Indizes auf Spalten, die haeufig in WHERE-Klauseln, JOIN-Bedingungen und ORDER BY-Klauseln vorkommen. Vermeiden Sie die Indizierung von Spalten mit sehr geringer Kardinalitaet (wie eine boolesche Spalte), es sei denn, Sie verwenden einen Partial Index.

Ein zusammengesetzter Index auf (a, b, c) kann Abfragen befriedigen, die nach a, nach a AND b oder nach a AND b AND c filtern, aber nicht nach b allein oder c allein. Die Spaltenreihenfolge ist entscheidend.

Abfrageleistung mit EXPLAIN pruefen

-- PostgreSQL
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE customer_id = 42
  AND order_date >= '2026-01-01';

-- MySQL
EXPLAIN
SELECT * FROM orders
WHERE customer_id = 42
  AND order_date >= '2026-01-01';

Achten Sie auf sequenzielle Scans (Seq Scan) bei grossen Tabellen, die auf einen fehlenden Index hinweisen. Ein Index Scan oder Index Only Scan ist das gewuenschte Ergebnis.

Tipps zur Abfrageoptimierung

1. Vermeiden Sie SELECT *

Geben Sie immer die benoetigten Spalten an. Dies reduziert I/O, Speicherverbrauch und kann Index-Only-Scans ermoeglichen.

-- Schlecht
SELECT * FROM users WHERE id = 42;

-- Gut
SELECT id, name, email FROM users WHERE id = 42;

2. Verwenden Sie EXISTS statt IN fuer grosse Unterabfragen

-- Langsamer bei grossen Unterabfrageergebnissen
SELECT * FROM customers
WHERE id IN (SELECT customer_id FROM orders);

-- Schneller: stoppt beim ersten Treffer
SELECT * FROM customers c
WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

3. Vermeiden Sie Funktionen auf indizierten Spalten

-- Schlecht: Index auf created_at wird nicht verwendet
SELECT * FROM orders
WHERE YEAR(created_at) = 2026;

-- Gut: indexfreundlicher Bereichsscan
SELECT * FROM orders
WHERE created_at >= '2026-01-01'
  AND created_at < '2027-01-01';

4. Verwenden Sie UNION ALL statt UNION wenn moeglich

UNION entfernt Duplikate (erfordert eine Sortierung), waehrend UNION ALL dies nicht tut. Wenn Sie wissen, dass die Ergebnismengen bereits eindeutig sind, bevorzugen Sie immer UNION ALL.

-- Langsamer (dedupliziert)
SELECT name FROM employees
UNION
SELECT name FROM contractors;

-- Schneller (keine Deduplizierung)
SELECT name FROM employees
UNION ALL
SELECT name FROM contractors;

5. Batch-Operationen fuer grosse Datenmengen

-- Anstatt Millionen von Zeilen auf einmal zu loeschen
-- In Batches loeschen, um die Tabelle nicht zu sperren
DELETE FROM logs
WHERE created_at < '2025-01-01'
LIMIT 10000;
-- Wiederholen, bis 0 Zeilen betroffen sind

6. Verwenden Sie geeignete Datentypen

Kleinere Datentypen bedeuten, dass mehr Zeilen in den Speicher und in Indexseiten passen. Verwenden Sie INTEGER statt BIGINT, wenn der Wertebereich es erlaubt. Verwenden Sie VARCHAR mit angemessenen Laengen statt TEXT fuer indizierte Spalten.

Nuetzliche SQL-Muster

UPSERT (INSERT ON CONFLICT)

-- PostgreSQL
INSERT INTO user_settings (user_id, theme, language)
VALUES (42, 'dark', 'en')
ON CONFLICT (user_id)
DO UPDATE SET
  theme = EXCLUDED.theme,
  language = EXCLUDED.language;

-- MySQL
INSERT INTO user_settings (user_id, theme, language)
VALUES (42, 'dark', 'en')
ON DUPLICATE KEY UPDATE
  theme = VALUES(theme),
  language = VALUES(language);

Daten pivotieren mit CASE

SELECT
  product_id,
  SUM(CASE WHEN month = 1 THEN revenue ELSE 0 END) AS jan,
  SUM(CASE WHEN month = 2 THEN revenue ELSE 0 END) AS feb,
  SUM(CASE WHEN month = 3 THEN revenue ELSE 0 END) AS mar
FROM monthly_revenue
GROUP BY product_id;

Datumsreihen generieren

-- PostgreSQL: eine Reihe von Daten generieren
SELECT generate_series(
  '2026-01-01'::date,
  '2026-12-31'::date,
  '1 day'::interval
)::date AS date;

-- Mit LEFT JOIN verwenden, um Luecken in Zeitreihendaten zu fuellen
SELECT
  d.date,
  COALESCE(s.total, 0) AS daily_total
FROM generate_series('2026-01-01'::date, '2026-03-31'::date, '1 day') AS d(date)
LEFT JOIN daily_sales s ON s.sale_date = d.date
ORDER BY d.date;

Kurzreferenz-Tabelle

OperationSyntax
Zeilen filternWHERE condition
Ergebnisse sortierenORDER BY column ASC/DESC
Zeilen begrenzenLIMIT n oder FETCH FIRST n ROWS ONLY
Duplikate entfernenSELECT DISTINCT
Gruppieren und aggregierenGROUP BY ... HAVING
Tabellen kombinierenJOIN ... ON
Temporaere ErgebnismengeWITH cte AS (...)
ZeilennummerierungROW_NUMBER() OVER (...)
Vorherige/naechste ZeileLAG() / LEAD() OVER (...)
Laufende SummeSUM() OVER (ORDER BY ... ROWS ...)
Einfuegen oder aktualisierenINSERT ... ON CONFLICT DO UPDATE

Fazit

SQL ist eine umfangreiche Sprache, aber die Beherrschung der Muster in diesem Spickzettel deckt die ueberwiegende Mehrheit der realen Szenarien ab, die Ihnen als Entwickler begegnen werden. Beginnen Sie mit soliden SELECT- und JOIN-Grundlagen und erweitern Sie dann schrittweise Ihr Repertoire um CTEs und Window Functions. Behalten Sie stets die Indizierung und Abfrageleistung im Auge, besonders wenn Ihre Datenmengen wachsen.

Probieren Sie unseren SQL Formatter aus, um Ihre Abfragen zu formatieren.

Setzen Sie ein Lesezeichen fuer diese Seite und kommen Sie zurueck, wann immer Sie eine schnelle Referenz benoetigen. Der beste Weg, SQL zu verinnerlichen, ist regelmaessiges Ueben -- sei es durch Abfragen gegen Ihre eigenen Datenbanken, die Nutzung von Plattformen wie SQLite-In-Memory-Sandboxes oder das Erkunden offener Datensaetze. Die Muster, die Sie heute aufbauen, werden Ihnen Ihre gesamte Karriere lang dienen.

Verwandte BeitrÀge