SQL fuer Datenanalysten: Fensterfunktionen, CTEs und fortgeschrittene Abfragen

Umfassender Leitfaden zu SQL-Fensterfunktionen (ROW_NUMBER, RANK, LAG, LEAD, NTILE), Common Table Expressions und fortgeschrittenen Abfragemustern wie Gaps-and-Islands. Mit vollstaendigen Codebeispielen fuer technische Interviews und die taegliche Analysearbeit.

SQL-Fensterfunktionen und CTEs fuer Datenanalyse mit Dashboards und Abfrageergebnissen

SQL-Fensterfunktionen, CTEs (Common Table Expressions) und fortgeschrittene Abfragemuster bilden das Rueckgrat analytischer Datenbankarbeit. Ob bei der Vorbereitung auf ein technisches Interview als Datenanalyst oder beim Aufbau komplexer Reporting-Abfragen — diese Techniken verwandeln verschachtelte, schwer lesbare Subqueries in sauberes, performantes SQL. Wer diese Konzepte sicher beherrscht, loest Analyseaufgaben in einem Bruchteil der Zeit und liefert dabei wartbaren, nachvollziehbaren Code.

Kurzreferenz

Fensterfunktionen fuehren Berechnungen ueber eine definierte Menge von Zeilen durch, die mit der aktuellen Zeile in Beziehung stehen — ohne die Ergebnisse wie bei GROUP BY zu einer einzigen Zeile zusammenzufassen. In Kombination mit CTEs entstehen analytische Abfragen, die sowohl lesbar als auch wartbar bleiben.

SQL-Fensterfunktionen und die OVER-Klausel verstehen

Eine Fensterfunktion wendet eine Berechnung auf ein definiertes "Fenster" von Zeilen an. Die OVER-Klausel bestimmt, welche Zeilen zum Fenster gehoeren und in welcher Reihenfolge sie verarbeitet werden. Im Gegensatz zu Aggregatfunktionen mit GROUP BY bleiben bei Fensterfunktionen alle einzelnen Zeilen im Ergebnis erhalten.

Die allgemeine Syntax folgt einem einheitlichen Muster, das in allen gaengigen Datenbanksystemen funktioniert — PostgreSQL, MySQL 8+, BigQuery, SQL Server und Snowflake.

sql
-- window_function_syntax.sql
SELECT
  employee_id,
  department,
  salary,
  -- Ranks employees within each department by salary
  RANK() OVER (
    PARTITION BY department
    ORDER BY salary DESC
  ) AS dept_salary_rank,
  -- Running total of salaries within department
  SUM(salary) OVER (
    PARTITION BY department
    ORDER BY salary DESC
    ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
  ) AS running_salary_total
FROM employees;

PARTITION BY teilt die Daten in Gruppen auf — aehnlich wie GROUP BY, aber ohne Zeilen zusammenzufassen. ORDER BY legt die Reihenfolge innerhalb jeder Partition fest. Die optionale Frame-Klausel (ROWS BETWEEN ...) grenzt das Fenster auf einen bestimmten Zeilenbereich ein. Dieses Zusammenspiel aus Partitionierung, Sortierung und Frame-Definition bildet die Grundlage fuer alle weiteren Fensterfunktionen.

ROW_NUMBER, RANK und DENSE_RANK im Vergleich

Diese drei Ranking-Funktionen sehen auf den ersten Blick aehnlich aus, verhalten sich aber bei Gleichstaenden (Ties) unterschiedlich. Die Wahl der falschen Funktion ist eine haeufige Fehlerquelle in analytischen Abfragen und ein beliebtes Thema in SQL-Interviews.

sql
-- ranking_comparison.sql
SELECT
  product_name,
  category,
  revenue,
  ROW_NUMBER() OVER (ORDER BY revenue DESC) AS row_num,
  RANK()       OVER (ORDER BY revenue DESC) AS rank_val,
  DENSE_RANK() OVER (ORDER BY revenue DESC) AS dense_rank_val
FROM products;

| product_name | revenue | row_num | rank_val | dense_rank_val | |-------------|---------|---------|----------|----------------| | Widget A | 50000 | 1 | 1 | 1 | | Widget B | 50000 | 2 | 1 | 1 | | Widget C | 40000 | 3 | 3 | 2 | | Widget D | 30000 | 4 | 4 | 3 |

ROW_NUMBER vergibt immer eine eindeutige, aufsteigende Ganzzahl — auch bei identischen Werten erhaelt eine Zeile willkuerlich die niedrigere Nummer. RANK vergibt bei Gleichstaenden dieselbe Nummer, ueberspringt aber die nachfolgenden Plaetze (1, 1, 3). DENSE_RANK behandelt Gleichstaende ebenfalls, ueberspringt jedoch keine Nummern (1, 1, 2). Fuer Top-N-Abfragen, bei denen Duplikate denselben Rang teilen sollen, ist DENSE_RANK in der Regel die richtige Wahl.

LAG und LEAD fuer Periodenvergleiche

LAG und LEAD greifen auf Daten vorheriger oder nachfolgender Zeilen zu — ohne Self-Joins. Diese Funktionen sind unverzichtbar fuer die Berechnung von Wachstumsraten, die Erkennung von Trends und die Identifikation von Veraenderungen zwischen Zeitraeumen.

sql
-- period_over_period.sql
SELECT
  month,
  revenue,
  -- Previous month revenue
  LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
  -- Month-over-month growth rate
  ROUND(
    (revenue - LAG(revenue, 1) OVER (ORDER BY month)) * 100.0
    / NULLIF(LAG(revenue, 1) OVER (ORDER BY month), 0),
    2
  ) AS mom_growth_pct,
  -- Next month revenue (forward-looking)
  LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_sales
ORDER BY month;

Das zweite Argument von LAG/LEAD gibt den Offset an (Standard ist 1). Ein optionales drittes Argument definiert einen Standardwert, wenn an der gewuenschten Position keine Zeile existiert — das ist nuetzlich, um NULL-Werte in der ersten bzw. letzten Zeile zu vermeiden. NULLIF in der Wachstumsberechnung verhindert Division-durch-Null-Fehler, die andernfalls das gesamte Ergebnis verfaelschen wuerden.

NTILE und Perzentil-Bucketing

NTILE verteilt Zeilen in eine festgelegte Anzahl moeglichst gleich grosser Gruppen. Datenanalysten verwenden diese Funktion fuer Quartilanalysen, Dezil-Scoring und Kundensegmentierung — Anwendungsfaelle, die in der Praxis staendig vorkommen.

sql
-- customer_segmentation.sql
SELECT
  customer_id,
  total_spend,
  -- Split customers into 4 quartiles by spend
  NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile,
  -- Decile scoring for finer granularity
  NTILE(10) OVER (ORDER BY total_spend DESC) AS spend_decile
FROM (
  SELECT
    customer_id,
    SUM(order_total) AS total_spend
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY customer_id
) customer_totals;

Quartil 1 enthaelt die umsatzstaerksten Kunden, Quartil 4 die umsatzschwachsten. Dieses Muster laesst sich direkt auf die RFM-Analyse (Recency, Frequency, Monetary) uebertragen, die im Marketing-Analytics zur Kundensegmentierung eingesetzt wird.

Bereit für deine Data Analytics-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Common Table Expressions: Verschachtelte Subqueries ersetzen

CTEs (WITH-Klauseln) zerlegen komplexe Abfragen in benannte, lesbare Schritte. Jede CTE wirkt als temporaere, benannte Ergebnismenge, die nur fuer die Dauer der Abfrage existiert. Neben der verbesserten Lesbarkeit erleichtern CTEs auch das Debugging erheblich — jeder Schritt laesst sich einzeln testen und verifizieren.

sql
-- cte_sales_analysis.sql
WITH monthly_revenue AS (
  -- Step 1: Aggregate raw orders into monthly totals
  SELECT
    DATE_TRUNC('month', order_date) AS month,
    product_category,
    SUM(amount) AS revenue,
    COUNT(DISTINCT customer_id) AS unique_customers
  FROM orders
  WHERE order_date >= '2025-01-01'
  GROUP BY DATE_TRUNC('month', order_date), product_category
),
ranked_categories AS (
  -- Step 2: Rank categories within each month
  SELECT
    month,
    product_category,
    revenue,
    unique_customers,
    RANK() OVER (
      PARTITION BY month
      ORDER BY revenue DESC
    ) AS category_rank
  FROM monthly_revenue
)
-- Step 3: Final output — top 3 categories per month
SELECT
  month,
  product_category,
  revenue,
  unique_customers,
  category_rank
FROM ranked_categories
WHERE category_rank <= 3
ORDER BY month, category_rank;

Der entscheidende Vorteil gegenueber verschachtelten Subqueries liegt in der linearen Struktur: Die Abfrage liest sich von oben nach unten wie eine Schritt-fuer-Schritt-Anleitung. Jede CTE baut auf den Ergebnissen der vorherigen auf, was die Fehlersuche drastisch vereinfacht.

Rekursive CTEs fuer hierarchische Daten

Rekursive CTEs loesen Probleme mit hierarchischen oder graphaehnlichen Datenstrukturen — Organigramme, Kategoriebaeume, Stuecklisten und Pfadsuch-Abfragen. Sie bestehen aus einem Basisfall (Ankerbedingung) und einem rekursiven Teil, der sich selbst referenziert.

sql
-- recursive_org_chart.sql
WITH RECURSIVE org_hierarchy AS (
  -- Base case: top-level managers (no manager above them)
  SELECT
    employee_id,
    employee_name,
    manager_id,
    1 AS depth,
    employee_name AS management_chain
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  -- Recursive case: join employees to their managers
  SELECT
    e.employee_id,
    e.employee_name,
    e.manager_id,
    oh.depth + 1,
    oh.management_chain || ' > ' || e.employee_name
  FROM employees e
  INNER JOIN org_hierarchy oh ON e.manager_id = oh.employee_id
)
SELECT
  employee_id,
  employee_name,
  depth,
  management_chain
FROM org_hierarchy
ORDER BY management_chain;
Performance rekursiver CTEs

Rekursive CTEs koennen bei grossen Datensaetzen langsam werden. Es empfiehlt sich, immer ein Tiefenlimit einzubauen (WHERE depth < 10) und sicherzustellen, dass die Join-Spalte (manager_id) indiziert ist. Fuer sehr tiefe Hierarchien sind Materialized-Path- oder Nested-Set-Muster die performantere Alternative.

Fortgeschrittene Analysemuster: Gaps, Islands und laufende Summen

Das Gaps-and-Islands-Muster identifiziert aufeinanderfolgende Sequenzen in Daten — aktive Abonnementzeitraeume, aufeinanderfolgende Login-Tage oder ununterbrochene Produktionslaeufe. Diese Technik kombiniert ROW_NUMBER mit Datumsarithmetik und gehoert zum Standardrepertoire fortgeschrittener SQL-Analysten.

sql
-- consecutive_login_streaks.sql
WITH login_days AS (
  SELECT DISTINCT
    user_id,
    DATE(login_timestamp) AS login_date
  FROM user_sessions
),
streaks AS (
  SELECT
    user_id,
    login_date,
    -- Subtracting row_number from date creates a constant for consecutive days
    login_date - (ROW_NUMBER() OVER (
      PARTITION BY user_id
      ORDER BY login_date
    ))::int AS streak_group
  FROM login_days
)
SELECT
  user_id,
  MIN(login_date) AS streak_start,
  MAX(login_date) AS streak_end,
  COUNT(*) AS streak_length
FROM streaks
GROUP BY user_id, streak_group
HAVING COUNT(*) >= 3
ORDER BY streak_length DESC;

Der Trick besteht darin, dass bei aufeinanderfolgenden Tagen die Differenz zwischen Datum und ROW_NUMBER konstant bleibt. Sobald eine Luecke auftritt, aendert sich dieser Wert, wodurch eine neue Gruppe entsteht. Dieses Muster wird in Interviews regelmaessig abgefragt und hat direkten Praxisbezug in der Nutzeranalyse und Retentionsmessung.

Fensterfunktionen mit CASE fuer bedingte Analysen kombinieren

Die Kombination von Fensterfunktionen mit CASE-Ausdruecken ermoeglicht komplexe bedingte Berechnungen innerhalb einer einzigen Abfrage. Typische Anwendungsfaelle sind gleitende Durchschnitte, quartalsbezogene kumulative Summen und die automatische Erkennung von Ausreissern.

sql
-- conditional_analytics.sql
SELECT
  order_date,
  product_category,
  amount,
  -- 7-day moving average
  AVG(amount) OVER (
    PARTITION BY product_category
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7d,
  -- Cumulative sum resetting each quarter
  SUM(amount) OVER (
    PARTITION BY product_category, DATE_TRUNC('quarter', order_date)
    ORDER BY order_date
  ) AS qtd_cumulative,
  -- Flag if current row exceeds 2x the category average
  CASE
    WHEN amount > 2 * AVG(amount) OVER (PARTITION BY product_category)
    THEN 'OUTLIER'
    ELSE 'NORMAL'
  END AS outlier_flag
FROM daily_sales
ORDER BY product_category, order_date;

Der gleitende 7-Tage-Durchschnitt verwendet eine explizite Frame-Klausel (ROWS BETWEEN 6 PRECEDING AND CURRENT ROW), um genau die letzten sieben Tage einzubeziehen. Die quartalsweise kumulative Summe nutzt DATE_TRUNC in der PARTITION-BY-Klausel, sodass sich die Summe zu Quartalsbeginn automatisch zuruecksetzt. Der Ausreisser-Flag vergleicht jeden einzelnen Wert mit dem Gesamtdurchschnitt der Kategorie.

Ausfuehrungsreihenfolge von Fensterfunktionen

Fensterfunktionen werden nach WHERE, GROUP BY und HAVING ausgefuehrt — aber vor ORDER BY und LIMIT. Das bedeutet, dass Fensterfunktionen nicht direkt in WHERE-Klauseln verwendet werden koennen. Um auf das Ergebnis einer Fensterfunktion zu filtern, muss die Abfrage in eine CTE oder eine Subquery eingebettet werden.

Performance-Optimierung fuer analytische Abfragen

Bei umfangreichen Datensaetzen ist die Performance von Fensterfunktionen ein entscheidender Faktor. Eine besonders nuetzliche Technik ist die Verwendung benannter Fenster (WINDOW-Klausel), die Codeduplizierung reduziert und dem Abfrageoptimierer zusaetzliche Hinweise gibt.

sql
-- named_window.sql
SELECT
  department,
  employee_id,
  salary,
  RANK() OVER dept_window AS dept_rank,
  SUM(salary) OVER dept_window AS dept_total,
  AVG(salary) OVER dept_window AS dept_avg
FROM employees
WINDOW dept_window AS (
  PARTITION BY department
  ORDER BY salary DESC
);

Statt die OVER-Klausel dreimal identisch zu wiederholen, wird das Fenster einmal definiert und mehrfach referenziert. Dies verbessert nicht nur die Lesbarkeit, sondern ermoeglicht es dem Datenbank-Optimizer in vielen Faellen, die Daten nur einmal zu sortieren statt mehrfach. Zusaetzlich sollten Daten immer so frueh wie moeglich in der WHERE-Klausel gefiltert werden, bevor Fensterfunktionen greifen — das reduziert die Menge der zu verarbeitenden Zeilen erheblich.

Weitere bewaeahrte Optimierungsstrategien umfassen die Indizierung von Spalten, die in PARTITION BY und ORDER BY verwendet werden, sowie die Vermeidung unnoetig breiter Fenster-Frames. Bei sehr grossen Tabellen kann es sinnvoll sein, die Daten vorab in einer CTE zu aggregieren oder zu filtern, bevor Fensterfunktionen angewendet werden.

Bereit für deine Data Analytics-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Fazit

  • Fensterfunktionen (ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE) operieren ueber Zeilenmengen, ohne diese zusammenzufassen — im Gegensatz zu GROUP-BY-Aggregaten
  • CTEs zerlegen komplexe Abfragen in benannte, einzeln testbare Schritte und ersetzen tief verschachtelte Subqueries
  • Rekursive CTEs verarbeiten hierarchische Daten wie Organigramme und Kategoriebaeume, erfordern aber Tiefenlimits und indizierte Join-Spalten
  • Das Gaps-and-Islands-Muster (ROW_NUMBER + Datumsarithmetik) identifiziert aufeinanderfolgende Sequenzen in Zeitreihendaten
  • Benannte Fenster (WINDOW-Klausel) reduzieren Codeduplizierung und koennen die Optimierung des Abfrageplans verbessern
  • Daten sollten in WHERE-Klauseln gefiltert werden, bevor Fensterfunktionen ausgewertet werden, um die Performance bei grossen Tabellen zu erhalten
  • Diese Muster tauchen regelmaessig in Interviews fuer Datenanalysten auf und sind direkt anwendbar auf taegliche Reporting-, Segmentierungs- und Trendanalysen

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#sql
#data-analytics
#window-functions
#cte
#interview

Teilen

Verwandte Artikel