Spark SQL: De Kracht van SQL op Big Data

Spark SQL maakt het mogelijk om met vertrouwde SQL-syntax te werken op gedistribueerde datasets. In Databricks is Spark SQL geïntegreerd in notebooks, DLT-pipelines en SQL Warehouses. Na deze module begrijp je de volledige SQL-laag van Databricks.

~3 uur
Leertijd
Module 4
van 8 modules
5
Praktijkoefeningen
Intermediate
Niveau

Wat is Spark SQL?

~3 uur Intermediate SQL + Python

Spark SQL is de module binnen Apache Spark die het mogelijk maakt om met SQL-queries te werken op gedistribueerde datasets. Databricks bouwt hierbovenop en voegt extra functies toe zoals Delta Lake-integratie, Unity Catalog support en een geoptimaliseerde query-engine genaamd Photon.

In tegenstelling tot een traditionele database draait Spark SQL op een cluster van meerdere machines. Queries worden automatisch verdeeld over de worker nodes, waardoor je met dezelfde SQL-syntax terabytes aan data kunt verwerken. Databricks maakt Spark SQL toegankelijk via drie interfaces:

Spark SQL vs. Traditioneel SQL

Spark SQL is grotendeels ANSI SQL compliant, maar er zijn enkele verschillen. Spark SQL heeft uitgebreide ondersteuning voor semi-structured data (JSON, arrays, structs), higher-order functies en streaming queries. Sommige database-specifieke functies zoals stored procedures en triggers bestaan niet in Spark SQL.

Tabellen Aanmaken: Managed vs External

In Databricks zijn er twee soorten tabellen: managed tables en external tables. Het verschil zit in wie verantwoordelijk is voor de onderliggende databestanden.

Eigenschap Managed Table External Table
Dataopslag Databricks beheert de locatie (Unity Catalog managed storage) Jij bepaalt de locatie (bijv. ADLS Gen2)
DROP TABLE Verwijdert ook de onderliggende databestanden Verwijdert alleen de metadata; data blijft behouden
Gebruik Standaardkeuze voor interne tabellen Wanneer data al bestaat op een externe locatie
Governance Volledig onder Unity Catalog Vereist externe locatie credentials
SQL - Managed tabel aanmaken

-- Managed Delta tabel aanmaken
CREATE TABLE IF NOT EXISTS silver.klanten (
    klant_id        BIGINT      NOT NULL,
    voornaam        STRING      NOT NULL,
    achternaam      STRING      NOT NULL,
    email           STRING,
    geboortedatum   DATE,
    stad            STRING,
    aanmaakdatum    TIMESTAMP   DEFAULT current_timestamp()
)
USING DELTA
COMMENT 'Gecleansde klantgegevens — Silver laag';

-- Partitioneer op stad voor snellere regionale queries
CREATE TABLE IF NOT EXISTS silver.orders (
    order_id        BIGINT      NOT NULL,
    klant_id        BIGINT      NOT NULL,
    order_datum     DATE        NOT NULL,
    totaal_bedrag   DECIMAL(10,2),
    status          STRING
)
USING DELTA
PARTITIONED BY (order_datum)
COMMENT 'Order transacties — Silver laag';
      
SQL - External tabel aanmaken

-- External tabel: data staat al in Azure Data Lake
CREATE TABLE IF NOT EXISTS bronze.raw_events
USING DELTA
LOCATION 'abfss://raw@mijnstorageaccount.dfs.core.windows.net/events/'
COMMENT 'Ruwe event data — Bronze laag (external)';

-- Tabel eigenschappen bekijken
DESCRIBE EXTENDED silver.klanten;

-- Tabel statistieken ophalen voor query planner
ANALYZE TABLE silver.klanten COMPUTE STATISTICS FOR ALL COLUMNS;
      

SELECT Queries met Praktijkvoorbeelden

Spark SQL ondersteunt de volledige SELECT-syntax inclusief subqueries, CTEs en lateral views. Hier volgen praktijkgerichte voorbeelden die je tegenkomt in productie-omgevingen.

SQL - SELECT met aggregaties en CTE

-- Common Table Expression (CTE) voor leesbaarheid
WITH maandelijkse_omzet AS (
    SELECT
        date_trunc('month', order_datum)    AS maand,
        stad,
        SUM(totaal_bedrag)                  AS omzet,
        COUNT(DISTINCT order_id)            AS aantal_orders,
        COUNT(DISTINCT klant_id)            AS unieke_klanten,
        AVG(totaal_bedrag)                  AS gemiddeld_orderbedrag
    FROM silver.orders o
    JOIN silver.klanten k ON o.klant_id = k.klant_id
    WHERE o.status = 'COMPLETED'
      AND o.order_datum >= '2025-01-01'
    GROUP BY 1, 2
),
top_steden AS (
    SELECT stad, SUM(omzet) AS totaal_omzet
    FROM maandelijkse_omzet
    GROUP BY stad
    ORDER BY totaal_omzet DESC
    LIMIT 10
)
SELECT
    m.maand,
    m.stad,
    m.omzet,
    m.aantal_orders,
    m.unieke_klanten,
    ROUND(m.gemiddeld_orderbedrag, 2) AS gem_orderbedrag,
    ROUND(m.omzet / SUM(m.omzet) OVER (PARTITION BY m.maand) * 100, 1) AS marktaandeel_pct
FROM maandelijkse_omzet m
INNER JOIN top_steden t ON m.stad = t.stad
ORDER BY m.maand, m.omzet DESC;
      
Python - spark.sql() en DataFrame API combineren

# Spark SQL uitvoeren vanuit Python
resultaat = spark.sql("""
    SELECT
        klant_id,
        COUNT(*) AS aantal_orders,
        SUM(totaal_bedrag) AS lifetime_value
    FROM silver.orders
    WHERE status = 'COMPLETED'
    GROUP BY klant_id
    HAVING COUNT(*) >= 5
""")

# Resultaat als DataFrame opslaan
resultaat.write.format("delta").mode("overwrite").saveAsTable("gold.klant_lifetime_value")

# Alternatief: registereer als tijdelijke view en gebruik in volgende query
resultaat.createOrReplaceTempView("ltv_berekening")

# Nu kun je de tijdelijke view gebruiken in SQL
spark.sql("""
    SELECT
        k.voornaam,
        k.achternaam,
        k.stad,
        l.lifetime_value,
        CASE
            WHEN l.lifetime_value >= 10000 THEN 'Platinum'
            WHEN l.lifetime_value >= 5000  THEN 'Gold'
            WHEN l.lifetime_value >= 1000  THEN 'Silver'
            ELSE 'Bronze'
        END AS klant_segment
    FROM ltv_berekening l
    JOIN silver.klanten k ON l.klant_id = k.klant_id
    ORDER BY l.lifetime_value DESC
""").display()
      

Views in Spark SQL

Spark SQL ondersteunt drie soorten views. Het kiezen van de juiste view-soort is belangrijk voor je architectuur en data governance.

View Type Scope Persistentie Gebruik
Temporary View Huidige SparkSession Verdwijnt bij einde sessie Tussenresultaten in notebooks
Global Temp View Alle sessies op dezelfde cluster Verdwijnt bij cluster restart Data delen tussen notebooks op zelfde cluster
Permanent View Metastore / Unity Catalog Persistent tot expliciet verwijderd Herbruikbare logische laag voor BI-tools
SQL - Views aanmaken

-- Tijdelijke view (huidige sessie)
CREATE OR REPLACE TEMPORARY VIEW actieve_klanten AS
SELECT *
FROM silver.klanten
WHERE aanmaakdatum >= date_sub(current_date(), 365);

-- Globale tijdelijke view (andere notebooks op zelfde cluster)
CREATE OR REPLACE GLOBAL TEMPORARY VIEW dashboard_metrics AS
SELECT
    date_trunc('week', order_datum) AS week,
    COUNT(*)                         AS orders,
    SUM(totaal_bedrag)               AS omzet
FROM silver.orders
WHERE status = 'COMPLETED'
GROUP BY 1;

-- Gebruik global temp view (altijd via global_temp schema)
SELECT * FROM global_temp.dashboard_metrics;

-- Permanente view in Unity Catalog
CREATE OR REPLACE VIEW gold.v_klant_overzicht AS
SELECT
    k.klant_id,
    k.voornaam || ' ' || k.achternaam AS volledige_naam,
    k.stad,
    COUNT(o.order_id)                  AS totaal_orders,
    SUM(o.totaal_bedrag)               AS totaal_besteed,
    MAX(o.order_datum)                 AS laatste_aankoop
FROM silver.klanten k
LEFT JOIN silver.orders o ON k.klant_id = o.klant_id
GROUP BY k.klant_id, k.voornaam, k.achternaam, k.stad;

-- View verwijderen
DROP VIEW IF EXISTS gold.v_klant_overzicht;
      

Ingebouwde Functies

Spark SQL heeft honderden ingebouwde functies. Hier behandelen we de meest gebruikte categorieën die je tegenkomt bij data engineering werk.

String Functies

SQL - String functies

SELECT
    -- Basis string manipulatie
    UPPER(voornaam)                             AS voornaam_upper,
    LOWER(email)                                AS email_lower,
    TRIM(BOTH ' ' FROM naam)                    AS naam_getrimd,
    LENGTH(achternaam)                          AS naam_lengte,
    SUBSTRING(postcode, 1, 4)                   AS pc_cijfers,

    -- Samenvoegen
    CONCAT(voornaam, ' ', achternaam)           AS volledige_naam,
    voornaam || ' ' || achternaam               AS volledige_naam_alt,

    -- Vervangen en zoeken
    REPLACE(email, '@gmail.com', '@bedrijf.nl') AS nieuw_email,
    REGEXP_REPLACE(telefoon, '[^0-9]', '')      AS tel_alleen_cijfers,
    LOCATE('@', email)                          AS at_positie,
    INSTR(email, '@')                           AS at_positie_alt,

    -- Splitten
    SPLIT(email, '@')[0]                        AS email_gebruiker,
    SPLIT(email, '@')[1]                        AS email_domein

FROM silver.klanten
WHERE email IS NOT NULL;
      

Datum Functies

SQL - Datum functies

SELECT
    order_datum,

    -- Datum extractie
    YEAR(order_datum)                           AS jaar,
    MONTH(order_datum)                          AS maand,
    DAY(order_datum)                            AS dag,
    DAYOFWEEK(order_datum)                      AS dag_van_week,  -- 1=Zondag
    WEEKOFYEAR(order_datum)                     AS weeknummer,
    QUARTER(order_datum)                        AS kwartaal,

    -- Datum berekeningen
    DATEDIFF(current_date(), order_datum)       AS dagen_geleden,
    DATE_ADD(order_datum, 30)                   AS betaaldatum,
    DATE_SUB(order_datum, 7)                    AS een_week_eerder,
    ADD_MONTHS(order_datum, 3)                  AS drie_maanden_later,

    -- Afkappen
    DATE_TRUNC('month', order_datum)            AS begin_maand,
    DATE_TRUNC('year', order_datum)             AS begin_jaar,

    -- Formatteren
    DATE_FORMAT(order_datum, 'dd-MM-yyyy')      AS datum_nl_formaat,
    DATE_FORMAT(order_datum, 'MMMM yyyy')       AS maand_jaar,

    -- Timestamp conversie
    TO_TIMESTAMP(order_datum)                   AS als_timestamp,
    UNIX_TIMESTAMP(order_datum)                 AS epoch_seconden

FROM silver.orders
ORDER BY order_datum DESC;
      

Aggregatie Functies

SQL - Aggregatie functies

SELECT
    stad,
    COUNT(*)                            AS totaal_klanten,
    COUNT(DISTINCT klant_id)            AS unieke_klanten,
    COUNT(email)                        AS klanten_met_email,  -- NULL's worden niet geteld
    SUM(totaal_besteed)                 AS totale_omzet,
    AVG(totaal_besteed)                 AS gemiddeld_besteed,
    MIN(totaal_besteed)                 AS laagste_besteding,
    MAX(totaal_besteed)                 AS hoogste_besteding,
    PERCENTILE_APPROX(totaal_besteed, 0.5)  AS mediaan_besteding,
    PERCENTILE_APPROX(totaal_besteed, 0.9)  AS p90_besteding,
    STDDEV(totaal_besteed)              AS std_afwijking,
    COLLECT_LIST(klant_id)              AS klant_ids_lijst,     -- Lijst van alle waarden
    COLLECT_SET(klant_id)               AS klant_ids_uniek      -- Lijst van unieke waarden

FROM gold.v_klant_overzicht
GROUP BY stad
HAVING COUNT(*) >= 10
ORDER BY totale_omzet DESC;
      

Window Functies

Window functies zijn een van de krachtigste SQL-features. Ze berekenen waarden over een "venster" van rijen zonder die rijen samen te voegen. Dit maakt rankings, lopende totalen en vergelijkingen met vorige perioden mogelijk.

SQL - ROW_NUMBER en RANK

-- ROW_NUMBER: uniek volgnummer per partitie
-- Gebruik: deduplicatie, eerste/laatste record per groep
SELECT
    klant_id,
    order_datum,
    totaal_bedrag,
    ROW_NUMBER() OVER (
        PARTITION BY klant_id
        ORDER BY order_datum DESC
    ) AS rij_nummer
FROM silver.orders;

-- Eerste order per klant selecteren (deduplicatie patroon)
WITH genummerd AS (
    SELECT *,
        ROW_NUMBER() OVER (
            PARTITION BY klant_id
            ORDER BY order_datum ASC
        ) AS rn
    FROM silver.orders
)
SELECT * FROM genummerd WHERE rn = 1;

-- RANK vs DENSE_RANK: verschil bij gelijke waarden
SELECT
    klant_id,
    totaal_bedrag,
    RANK()       OVER (ORDER BY totaal_bedrag DESC) AS rank_met_gaten,      -- 1,2,2,4
    DENSE_RANK() OVER (ORDER BY totaal_bedrag DESC) AS rank_zonder_gaten,   -- 1,2,2,3
    PERCENT_RANK() OVER (ORDER BY totaal_bedrag DESC) AS percent_rank       -- 0.0 t/m 1.0
FROM silver.orders
WHERE status = 'COMPLETED';
      
SQL - LAG en LEAD: vergelijking met vorige/volgende rij

-- LAG: waarde van vorige rij ophalen
-- LEAD: waarde van volgende rij ophalen
SELECT
    order_datum,
    totaal_bedrag,

    -- Omzet vorige maand
    LAG(totaal_bedrag, 1, 0) OVER (
        ORDER BY order_datum
    ) AS vorige_order_bedrag,

    -- Procentuele groei tov vorige order
    ROUND(
        (totaal_bedrag - LAG(totaal_bedrag, 1) OVER (ORDER BY order_datum))
        / NULLIF(LAG(totaal_bedrag, 1) OVER (ORDER BY order_datum), 0) * 100,
        2
    ) AS groei_pct,

    -- Volgende order datum (verwachte terugkeer)
    LEAD(order_datum, 1) OVER (
        PARTITION BY klant_id
        ORDER BY order_datum
    ) AS volgende_order_datum,

    -- Dagen tot volgende order
    DATEDIFF(
        LEAD(order_datum, 1) OVER (PARTITION BY klant_id ORDER BY order_datum),
        order_datum
    ) AS dagen_tot_volgende_order

FROM silver.orders
WHERE klant_id = 12345
ORDER BY order_datum;

-- Lopend totaal (running total)
SELECT
    order_datum,
    totaal_bedrag,
    SUM(totaal_bedrag) OVER (
        ORDER BY order_datum
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulatieve_omzet,

    -- Voortschrijdend gemiddelde (7-daags)
    AVG(totaal_bedrag) OVER (
        ORDER BY order_datum
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) AS gem_7_dagen

FROM silver.orders
WHERE status = 'COMPLETED'
ORDER BY order_datum;
      

Query Optimalisatie

Goede SQL-queries schrijven is één ding; ze ook snel laten draaien op grote datasets is een ander. Databricks biedt verschillende tools om queries te analyseren en te versnellen.

EXPLAIN: Query Plan Begrijpen

SQL - EXPLAIN voor query analyse

-- Bekijk het logical + physical query plan
EXPLAIN EXTENDED
SELECT k.stad, SUM(o.totaal_bedrag) AS omzet
FROM silver.orders o
JOIN silver.klanten k ON o.klant_id = k.klant_id
WHERE o.order_datum >= '2025-01-01'
GROUP BY k.stad;

-- EXPLAIN COST toont geschatte kardinaliteit per stap
EXPLAIN COST
SELECT * FROM silver.orders WHERE totaal_bedrag > 1000;

-- In Databricks UI: klik op 'Spark Jobs' voor visueel query plan
      

Caching

SQL - Tabellen cachen

-- Cache een tabel in geheugen (nuttig bij herhaald gebruik in zelfde notebook)
CACHE TABLE silver.klanten;

-- Cache met selectieve kolommen
CACHE SELECT klant_id, stad, totaal_besteed FROM gold.v_klant_overzicht;

-- Cache verwijderen
UNCACHE TABLE silver.klanten;

-- Controleer welke tabellen gecached zijn
SHOW TABLES IN silver;
      
Python - DataFrame caching

from pyspark.storagelevel import StorageLevel

# Lees grote tabel
df_orders = spark.table("silver.orders")

# Cache in geheugen (standaard)
df_orders.cache()

# Of specifiek storage level kiezen
df_orders.persist(StorageLevel.MEMORY_AND_DISK)

# Forceer de cache door een actie uit te voeren
df_orders.count()

# Later: cache vrijgeven om geheugen te besparen
df_orders.unpersist()
      

Partitionering

SQL - Partitionering en partition pruning

-- Tabel aanmaken met partitionering
CREATE TABLE IF NOT EXISTS silver.orders_gepartitioneerd
USING DELTA
PARTITIONED BY (jaar INT, maand INT)
AS
SELECT
    *,
    YEAR(order_datum)  AS jaar,
    MONTH(order_datum) AS maand
FROM silver.orders;

-- Query met partition pruning (Spark leest alleen relevante partities)
SELECT *
FROM silver.orders_gepartitioneerd
WHERE jaar = 2025 AND maand = 3;  -- Leest alleen partitie jaar=2025/maand=3

-- Partities bekijken
SHOW PARTITIONS silver.orders_gepartitioneerd;

-- Specifieke partitie repareren na handmatige data toevoeging
MSCK REPAIR TABLE silver.orders_gepartitioneerd;

-- Z-ORDER voor non-partitie kolommen (data skipping)
OPTIMIZE silver.orders
ZORDER BY (klant_id, status);
      

Performance Tips

  • Partitioneer op hoge kardinaliteitskolommen niet — gebruik Z-ORDER in plaats van partitionering voor kolommen met meer dan 10.000 unieke waarden.
  • Gebruik ANALYZE TABLE — geeft de query planner betere statistieken voor join-strategie keuzes (broadcast vs. sort-merge join).
  • Vermijd SELECT * — selecteer alleen de kolommen die je nodig hebt. Bij kolomopslag (parquet/delta) leest Spark anders alle kolommen in.
  • Filter vroeg — voeg WHERE-clauses toe zo vroeg mogelijk in je query, liefst voor joins.

Praktijkoefeningen

Oefening 1: Tabel en View Aanmaken

Scenario: Je werkt voor een e-commerce bedrijf en moet een Gold-laag inrichten voor rapportage.

  1. Maak een managed Delta tabel gold.product_performance aan met kolommen: product_id, naam, categorie, totaal_verkopen, totale_omzet, gemiddelde_rating.
  2. Vul de tabel met data uit silver.orders en silver.producten via een INSERT OVERWRITE.
  3. Maak een permanente view gold.v_top_producten die de top 20 producten per categorie toont op basis van omzet.

Oefening 2: Window Functies in Praktijk

Scenario: De marketing afdeling wil weten welke klanten in de top 10% van bestedingen vallen per stad, en hoelang de gemiddelde tijd is tussen twee aankopen.

  1. Schrijf een query die klanten rankt op totale besteding per stad (DENSE_RANK).
  2. Bereken de gemiddelde dagen tussen opeenvolgende orders per klant (gebruik LAG).
  3. Identificeer klanten die meer dan 90 dagen geen aankoop hebben gedaan (churn risk).

Oefening 3: Query Optimalisatie

Scenario: Een query op de orders tabel duurt 45 seconden. Gebruik EXPLAIN om de bottleneck te vinden.

  1. Voer EXPLAIN EXTENDED uit op de trage query en identificeer de meest kostbare stap.
  2. Voeg de juiste ZORDER BY toe op de meest gefilterde kolom.
  3. Voer ANALYZE TABLE uit en kijk of de query sneller wordt.
  4. Vergelijk de uitvoeringstijd voor en na de optimalisatie.

Spark SQL Skills in de Markt

Spark SQL is de meest gevraagde technische vaardigheid voor data engineers in Nederland. Bedrijven zoeken naar professionals die SQL kunnen combineren met het Databricks-ecosysteem.

SQL + Spark
Meest gevraagde combinatie
+15-20%
Salarisverschil met alleen SQL
500+
Vacatures per kwartaal NL
€65K-€90K
Gemiddeld salaris Spark engineer