SQL voor Data-Analisten: Vensterfuncties, CTE's en Geavanceerde Query's

Een uitgebreide gids over SQL-vensterfuncties, Common Table Expressions (CTE's) en geavanceerde querypatronen voor data-analisten. Met praktische codevoorbeelden van ROW_NUMBER, RANK, LAG, LEAD, NTILE en recursieve CTE's.

SQL-vensterfuncties en CTE's voor data-analyse met dashboards en queryresultaten

SQL-vensterfuncties, CTE's (Common Table Expressions) en geavanceerde querypatronen vormen de kern van analytische SQL. Of het nu gaat om de voorbereiding op een sollicitatiegesprek als data-analist of het schrijven van complexe rapportagequery's — deze technieken transformeren onleesbare, diep geneste subquery's in overzichtelijke en performante SQL.

Beknopt Overzicht

Vensterfuncties voeren berekeningen uit over een reeks rijen die gerelateerd zijn aan de huidige rij — zonder deze samen te vouwen tot één uitvoerrij zoals GROUP BY doet. In combinatie met CTE's worden complexe analytische query's leesbaar en onderhoudbaar.

SQL-Vensterfuncties en de OVER-Clausule

Een vensterfunctie past een berekening toe over een gedefinieerd "venster" van rijen. De OVER-clausule bepaalt welke rijen binnen dat venster vallen en hoe ze geordend worden. In tegenstelling tot aggregatiefuncties met GROUP BY behoudt een vensterfunctie elke individuele rij in de resultaatset.

De algemene syntaxis volgt een consistent patroon in alle grote databases — PostgreSQL, MySQL 8+, BigQuery, SQL Server en Snowflake.

sql
-- window_function_syntax.sql
SELECT
  employee_id,
  department,
  salary,
  RANK() OVER (
    PARTITION BY department
    ORDER BY salary DESC
  ) AS dept_salary_rank,
  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 verdeelt de data in groepen (vergelijkbaar met GROUP BY, maar zonder rijen samen te vouwen). ORDER BY bepaalt de volgorde binnen elke partitie. De optionele frame-clausule (ROWS BETWEEN ...) beperkt het venster tot een specifiek bereik van rijen.

Dit onderscheid is essentieel: waar GROUP BY het resultaat comprimeert tot één rij per groep, behoudt een vensterfunctie de volledige granulariteit van de dataset. Dat maakt het mogelijk om aggregatiewaarden naast individuele rijwaarden weer te geven — een patroon dat in vrijwel elke analytische rapportage terugkomt.

ROW_NUMBER, RANK en DENSE_RANK Vergeleken

Deze drie rangschikkingsfuncties lijken op het eerste gezicht identiek, maar gedragen zich anders wanneer er gelijke waarden (ties) voorkomen. Het kiezen van de verkeerde functie is een veelvoorkomende foutbron in analytische query's.

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 kent altijd een uniek oplopend geheel getal toe — zelfs bij gelijke waarden krijgt één rij willekeurig het lagere nummer. RANK kent hetzelfde nummer toe aan gelijke waarden, maar slaat de volgende nummers over (1, 1, 3). DENSE_RANK verwerkt gelijke waarden eveneens, maar slaat nooit nummers over (1, 1, 2). Voor top-N-query's waarin duplicaten dezelfde positie moeten delen, is DENSE_RANK doorgaans de juiste keuze.

In de praktijk wordt ROW_NUMBER vaak gebruikt voor paginering en deduplicatie, terwijl RANK en DENSE_RANK beter geschikt zijn voor rapportages waarin de exacte positie van belang is.

LAG en LEAD voor Periode-over-Periode Analyse

LAG en LEAD bieden toegang tot data van voorgaande of volgende rijen zonder self-joins. Deze functies zijn onmisbaar voor het berekenen van periode-over-periode groei, het detecteren van trends en het identificeren van veranderingen in tijdreeksen.

sql
-- period_over_period.sql
SELECT
  month,
  revenue,
  LAG(revenue, 1) OVER (ORDER BY month) AS prev_month_revenue,
  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,
  LEAD(revenue, 1) OVER (ORDER BY month) AS next_month_revenue
FROM monthly_sales
ORDER BY month;

Het tweede argument van LAG/LEAD specificeert de offset (standaard is 1). Een derde optioneel argument biedt een standaardwaarde wanneer er geen rij op die offset bestaat — handig om NULL-waarden in de eerste of laatste rijen te vermijden. De NULLIF in de groeiberekening voorkomt deling-door-nul-fouten.

Dit patroon wordt intensief gebruikt in financiële rapportages, waar maand-over-maand en kwartaal-over-kwartaal vergelijkingen standaard onderdeel zijn van dashboards. Het elimineert de noodzaak voor complexe self-joins die in oudere SQL-stijlen nodig waren.

NTILE en Percentiel-Bucketing

NTILE verdeelt rijen in een opgegeven aantal min of meer gelijke groepen. Data-analisten zetten het in voor kwartielanalyse, decielscoring en klantsegmentatie.

sql
-- customer_segmentation.sql
SELECT
  customer_id,
  total_spend,
  NTILE(4) OVER (ORDER BY total_spend DESC) AS spend_quartile,
  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;

Kwartiel 1 bevat de klanten met de hoogste uitgaven, kwartiel 4 de laagste. Dit patroon sluit direct aan op RFM-segmentatie (Recency, Frequency, Monetary) die in marketinganalyse wordt toegepast. Door NTILE te combineren met meerdere dimensies ontstaan gedetailleerde klantprofielen die als basis dienen voor gerichte campagnes.

Klaar om je Data Analytics gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Common Table Expressions: Geneste Subquery's Vervangen

CTE's (WITH-clausules) splitsen complexe query's op in benoemde, leesbare stappen. Elke CTE fungeert als een tijdelijke benoemde resultaatset die alleen bestaat voor de duur van de query. Naast leesbaarheid vereenvoudigen CTE's het debugproces — elke stap kan afzonderlijk getest worden.

sql
-- cte_sales_analysis.sql
WITH monthly_revenue AS (
  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 (
  SELECT
    month,
    product_category,
    revenue,
    unique_customers,
    RANK() OVER (
      PARTITION BY month
      ORDER BY revenue DESC
    ) AS category_rank
  FROM monthly_revenue
)
SELECT
  month,
  product_category,
  revenue,
  unique_customers,
  category_rank
FROM ranked_categories
WHERE category_rank <= 3
ORDER BY month, category_rank;

Het bovenstaande voorbeeld illustreert hoe een CTE de logica opdeelt in twee heldere stappen: eerst de maandelijkse omzet per categorie berekenen, vervolgens een rangschikking toepassen. Zonder CTE's zou dezelfde logica als diep geneste subquery resulteren — aanzienlijk moeilijker te lezen en te onderhouden.

Een bijkomend voordeel is dat CTE's meerdere keren in dezelfde query kunnen worden gerefereerd, wat codeduplicatie voorkomt wanneer dezelfde tussentabel in meerdere delen van de eindquery nodig is.

Recursieve CTE's voor Hiërarchische Data

Recursieve CTE's bieden oplossingen voor vraagstukken met hiërarchische of grafachtige data — organogrammen, categoriebomen, stuklijsten en padzoekquery's.

sql
-- recursive_org_chart.sql
WITH RECURSIVE org_hierarchy AS (
  SELECT
    employee_id,
    employee_name,
    manager_id,
    1 AS depth,
    employee_name AS management_chain
  FROM employees
  WHERE manager_id IS NULL

  UNION ALL

  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;

De recursieve CTE begint met een ankerquery die de topniveaurijen selecteert (in dit geval medewerkers zonder manager). Het recursieve deel voegt vervolgens telkens het volgende niveau toe door te joinen op de eerder gevonden resultaten. Het resultaat is een complete hiërarchie met diepte-informatie en een aaneengeschakelde managementketen.

Prestaties van Recursieve CTE's

Recursieve CTE's kunnen traag zijn op grote datasets. Voeg altijd een dieptelimiet toe (WHERE depth < 10) en zorg ervoor dat de joinkolom (manager_id) geïndexeerd is. Voor zeer diepe hiërarchieën zijn gematerialiseerde paden of nested set-patronen een beter alternatief.

Geavanceerde Analytische Patronen: Gaps, Islands en Lopende Totalen

Het gaps-and-islands-patroon identificeert opeenvolgende reeksen in data — actieve abonnementsperioden, opeenvolgende inlogdagen of ononderbroken productieruns. Dit patroon combineert ROW_NUMBER met datumberekeningen om groepen van aaneengesloten rijen te detecteren.

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,
    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;

De kern van deze techniek berust op een wiskundig inzicht: wanneer een oplopende datum wordt verminderd met een oplopend rijnummer, levert een aaneengesloten reeks steeds dezelfde waarde op. Een onderbreking resulteert in een andere waarde, waardoor de groepen automatisch worden gescheiden. Dit patroon is bijzonder waardevol voor productanalyse (gebruikersretentie), financiele audits en operationele monitoring.

Vensterfuncties Combineren met CASE voor Conditionele Analyse

Door vensterfuncties te combineren met CASE-expressies ontstaan krachtige conditionele analyses. Dit maakt het mogelijk om voortschrijdende gemiddelden, cumulatieve kwartaaltotalen en uitbijterdetectie in één enkele query samen te voegen.

sql
-- conditional_analytics.sql
SELECT
  order_date,
  product_category,
  amount,
  AVG(amount) OVER (
    PARTITION BY product_category
    ORDER BY order_date
    ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
  ) AS moving_avg_7d,
  SUM(amount) OVER (
    PARTITION BY product_category, DATE_TRUNC('quarter', order_date)
    ORDER BY order_date
  ) AS qtd_cumulative,
  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;

Het voortschrijdende 7-daags gemiddelde onthult kortetermijntrends binnen elke productcategorie. Het cumulatieve kwartaaltotaal toont de voortgang richting kwartaaldoelstellingen. De uitbijtervlag markeert transacties die meer dan het dubbele van het categorie-gemiddelde bedragen — een veelgebruikte methode voor fraudedetectie en datakwaliteitscontrole.

Uitvoeringsvolgorde van Vensterfuncties

Vensterfuncties worden uitgevoerd na WHERE, GROUP BY en HAVING — maar vóór ORDER BY en LIMIT. Dit betekent dat vensterfuncties niet rechtstreeks in WHERE-clausules gebruikt kunnen worden. Om te filteren op het resultaat van een vensterfunctie, dient de query in een CTE of subquery te worden gewrapped.

Prestatie-Optimalisatie voor Analytische Query's

Bij het werken met grote datasets is het essentieel om vensterfuncties efficient in te zetten. Benoemde vensters (WINDOW-clausule) verminderen codeduplicatie en kunnen de query-optimalisatie verbeteren doordat de database-engine hetzelfde venster hergebruikt.

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
);

Door het venster eenmaal te definiëren en meerdere keren te hergebruiken, wordt de query niet alleen korter maar ook beter onderhoudbaar. Wanneer de partitielogica gewijzigd moet worden, hoeft dit slechts op één plek te gebeuren.

Daarnaast zijn er enkele algemene prestatie-aanbevelingen voor analytische SQL:

  • Filter vroeg: pas WHERE-clausules toe voordat vensterfuncties worden geëvalueerd om het aantal te verwerken rijen te beperken.
  • Indexeer partitiekolommen: PARTITION BY-kolommen en ORDER BY-kolommen profiteren sterk van B-tree-indexen.
  • Beperk het vensterbereik: gebruik ROWS BETWEEN in plaats van het standaard onbegrensde bereik wanneer alleen een subset van rijen relevant is.
  • Vermijd onnodige sorteringen: elke unieke combinatie van PARTITION BY en ORDER BY kan een extra sorteerstap in het queryplan veroorzaken.

Klaar om je Data Analytics gesprekken te halen?

Oefen met onze interactieve simulatoren, flashcards en technische tests.

Conclusie

  • Vensterfuncties (ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, NTILE) opereren over rijensets zonder deze samen te vouwen — in tegenstelling tot GROUP BY-aggregaties
  • CTE's splitsen complexe query's op in benoemde, testbare stappen en vervangen diep geneste subquery's
  • Recursieve CTE's verwerken hiërarchische data zoals organogrammen en categoriebomen, maar vereisen dieptelimieten en geïndexeerde joinkolommen
  • Het gaps-and-islands-patroon (ROW_NUMBER + datumberekeningen) identificeert aaneengesloten reeksen in tijdreeksdata
  • Benoemde vensters (WINDOW-clausule) verminderen codeduplicatie en kunnen de query-optimalisatie verbeteren
  • Filter data in WHERE-clausules voordat vensterfuncties worden geëvalueerd om de prestaties op grote tabellen te behouden
  • Deze patronen komen frequent voor in sollicitatiegesprekken voor data-analyse en zijn direct toepasbaar op dagelijkse rapportages, segmentatie en trendanalyse

Begin met oefenen!

Test je kennis met onze gespreksimulatoren en technische tests.

Tags

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

Delen

Gerelateerde artikelen