Snowflake in zes lessen
Van de architectuur tot het beheersen van je rekening. Virtual warehouses, data laden, micro-partities, Time Travel, zero-copy cloning en toegangsbeheer, met SQL die je direct kunt draaien.
Les 1: Wat is Snowflake?
Snowflake Data Cloud: De Cloud-Native Data Warehouse
Snowflake is een volledig beheerde cloud data platform dat gebouwd is voor de cloud. Het combineert een data warehouse, data lake, en data sharing platform in één.
Key Insight
Traditionele data warehouses zijn duur en moeilijk te schalen. Snowflake's architectuur scheidt storage en compute, waardoor je onafhankelijk kunt schalen en alleen betaalt voor wat je gebruikt.
Snowflake's Multi-Cluster Architecture
Cloud Storage
Gecentraliseerde data opslag (S3, Azure Blob, GCS)
Virtual Warehouses
Elastische compute clusters voor query processing
Cloud Services
Beheer, security, metadata en query optimization
Data Sharing
Live data delen zonder kopiëren
Snowflake vs Traditionele Data Warehouses
| Feature | Snowflake | Traditioneel Data Warehouse | On-premise Warehouse |
|---|---|---|---|
| Architectuur | Storage & Compute gescheiden | Gekoppelde storage/compute | Fysieke servers |
| Schaling | ✅ Instant scaling (elastic) | ❌ Manueel schalen | ❌ Hardware upgrade nodig |
| Kosten Model | Pay-per-use (secunde) | Capaciteit/licentie gebaseerd | CapEx + onderhoud |
| Maintenance | ✅ Volledig beheerd | ❌ Gedeeltelijk beheerd | ❌ Volledig zelf |
| Data Sharing | ✅ Live sharing (zero-copy) | ❌ ETL/Export nodig | ❌ Complexe integratie |
| Time Travel | ✅ Tot 90 dagen historie | ❌ Backup nodig | ❌ Complex backup |
Pro Tip: Wanneer Snowflake Kiezen?
Kies Snowflake wanneer je: (1) Onvoorspelbare workloads hebt, (2) Met meerdere cloud providers werkt, (3) Data wilt delen met externe partijen, (4) Geen database administrators wilt beheren, (5) Snelle time-to-value nodig hebt.
SnowSQL Voorbeeld: Eerste Verbinding met Snowflake
-- Installeer SnowSQL: https://docs.snowflake.com/en/user-guide/snowsql-install-config
-- Verbind met Snowflake
!connect account=yz12345.eu-west-1
user=jansmit
password=************
warehouse=COMPUTE_WH
database=DEMO_DB
schema=PUBLIC
role=SYSADMIN;
-- Maak je eerste database
CREATE OR REPLACE DATABASE SALES_DB
COMMENT = 'Productie database voor sales data';
-- Maak een nieuw schema
CREATE OR REPLACE SCHEMA SALES_DB.RAW
COMMENT = 'Raw landing zone voor bron systemen';
-- Maak je eerste tabel
CREATE OR REPLACE TABLE SALES_DB.RAW.CUSTOMERS (
CUSTOMER_ID NUMBER(38,0) AUTOINCREMENT START 1 INCREMENT 1,
FIRST_NAME VARCHAR(100),
LAST_NAME VARCHAR(100),
EMAIL VARCHAR(255) UNIQUE,
PHONE VARCHAR(20),
ADDRESS VARCHAR(500),
CITY VARCHAR(100),
COUNTRY VARCHAR(100),
CREATED_AT TIMESTAMP_NTZ(9) DEFAULT CURRENT_TIMESTAMP(),
UPDATED_AT TIMESTAMP_NTZ(9) DEFAULT CURRENT_TIMESTAMP()
)
COMMENT = 'Klant master data tabel';
-- Maak een stage voor data loading
CREATE OR REPLACE STAGE SALES_DB.RAW.CUSTOMER_STAGE
URL = 's3://your-bucket/customer-data/'
STORAGE_INTEGRATION = YOUR_S3_INTEGRATION
FILE_FORMAT = (TYPE = 'CSV' SKIP_HEADER = 1);
-- Load data van S3
COPY INTO SALES_DB.RAW.CUSTOMERS
FROM @SALES_DB.RAW.CUSTOMER_STAGE
PATTERN = '.*customers.*\.csv'
ON_ERROR = 'CONTINUE';
-- Query data met Time Travel
SELECT * FROM SALES_DB.RAW.CUSTOMERS
AT(OFFSET => -60*5); -- Data van 5 minuten geleden
-- Toon alle databases
SHOW DATABASES;
-- Toon kosten van vandaag
SELECT
WAREHOUSE_NAME,
SUM(CREDITS_USED) AS TOTAL_CREDITS,
SUM(CREDITS_USED_COMPUTE) AS COMPUTE_CREDITS,
SUM(CREDITS_USED_CLOUD_SERVICES) AS SERVICES_CREDITS
FROM INFORMATION_SCHEMA.WAREHOUSE_METERING_HISTORY
WHERE START_TIME >= CURRENT_DATE()
GROUP BY WAREHOUSE_NAME
ORDER BY TOTAL_CREDITS DESC;
Python Voorbeeld: Snowflake Connector met Snowpark
# Installeer: pip install snowflake-connector-python snowflake-snowpark-python
from snowflake.snowpark import Session
from snowflake.snowpark.functions import col, sum as sum_, avg, count
import pandas as pd
import json
def create_snowflake_session():
"""Maak een Snowflake Snowpark sessie"""
connection_params = {
"account": "yz12345.eu-west-1",
"user": "DATAPARTNER_USER",
"password": "your_secure_password",
"role": "DATA_ENGINEER",
"warehouse": "COMPUTE_WH",
"database": "ANALYTICS_DB",
"schema": "PRODUCTION"
}
session = Session.builder.configs(connection_params).create()
print("✅ Snowflake Snowpark sessie succesvol aangemaakt")
return session
def demonstrate_snowpark_features(session):
"""Demonstreer Snowpark functionaliteiten"""
# 1. Laad data in Snowpark DataFrame (lazy evaluation)
sales_df = session.table("SALES_DB.RAW.SALES_TRANSACTIONS")
print("Aantal rijen in sales:", sales_df.count())
print("Schema van sales:", sales_df.schema)
# 2. Data transformaties
daily_sales = sales_df \
.group_by("TRANSACTION_DATE") \
.agg(
sum_("AMOUNT").alias("TOTAL_SALES"),
avg("AMOUNT").alias("AVG_TRANSACTION"),
count("*").alias("TRANSACTION_COUNT")
) \
.filter(col("TOTAL_SALES") > 10000) \
.sort(col("TRANSACTION_DATE").desc())
# 3. Toon SQL die gegenereerd wordt
print("Generated SQL:")
print(daily_sales.queries['queries'][0])
# 4. Materialiseer de resultaten
results = daily_sales.limit(10).collect()
print("\nTop 10 verkoopdagen:")
for row in results:
print(f"{row.TRANSACTION_DATE}: €{row.TOTAL_SALES:,.2f}")
# 5. Pandas integratie
pandas_df = daily_sales.to_pandas()
print(f"\nPandas DataFrame shape: {pandas_df.shape}")
return pandas_df
def create_udf_for_calculations(session):
"""Maak een User Defined Function in Snowflake"""
# Register een Python UDF
@udf(name="calculate_discount",
input_types=[FloatType(), FloatType()],
return_type=FloatType(),
is_permanent=True,
stage_location="@PYTHON_UDFS",
packages=["pandas"])
def calculate_discount(price, discount_percentage):
"""Bereken de prijs na korting"""
if discount_percentage > 50:
discount_percentage = 50 # Max 50% korting
return price * (1 - discount_percentage/100)
print("✅ UDF succesvol geregistreerd in Snowflake")
# Gebruik de UDF in een query
result = session.sql("""
SELECT
PRODUCT_ID,
PRICE,
DISCOUNT_PERCENTAGE,
calculate_discount(PRICE, DISCOUNT_PERCENTAGE) as FINAL_PRICE
FROM SALES_DB.RAW.PRODUCTS
WHERE DISCOUNT_PERCENTAGE > 0
LIMIT 10
""").collect()
return result
if __name__ == "__main__":
# Maak Snowpark sessie
session = create_snowflake_session()
try:
# Demo Snowpark features
sales_data = demonstrate_snowpark_features(session)
# Maak en gebruik UDF
udf_results = create_udf_for_calculations(session)
# Zero-copy cloning demo
print("\n--- Zero-Copy Cloning Demo ---")
session.sql("CREATE OR REPLACE DATABASE SALES_DB_CLONE CLONE SALES_DB").collect()
print("Database succesvol gekloond zonder extra storage kosten!")
# Time Travel demo
print("\n--- Time Travel Demo ---")
history = session.sql("""
SELECT
MIN(CREATED) AS OLDEST_DATA,
MAX(CREATED) AS NEWEST_DATA
FROM SALES_DB.RAW.SALES_TRANSACTIONS
AT(OFFSET => -3600) -- 1 uur geleden
""").collect()
print(f"Data van 1 uur geleden: {history[0]}")
finally:
session.close()
print("\n✅ Sessie gesloten")
Oefening: Snowflake Architecture
Scenario: Je werkt voor een Nederlandse e-commerce bedrijf met:
- 10TB aan historische order data in AWS S3
- Real-time clickstream data via Kafka
- Analytics team van 15 personen
- Data science team die Python gebruikt
- Noodzaak om data te delen met externe logistics partners
Opdracht: Ontwerp een Snowflake architectuur voor dit bedrijf:
- Hoeveel virtual warehouses heb je nodig?
- Welke size (XS, S, M, L, XL, XXL, XXXL)?
- Hoe organiseer je databases en schemas?
- Hoe implementeer je data sharing?
- Welke security features gebruik je?
Real-World Case: Bol.com
Uitdaging: Bol.com had data verspreid over meerdere on-premise data warehouses. Black Friday pieken zorgden voor performance issues.
Oplossing met Snowflake:
- Geconsolideerde data in Snowflake Data Cloud
- Auto-suspendeer warehouses tijdens daluren
- Multi-cluster warehouses voor concurrente queries
- Data Sharing met leveranciers voor real-time inventory
- Snowpark voor Python/R machine learning
Resultaat: 60% kostenbesparing tov on-premise, 10x snellere queries tijdens piekmomenten, real-time data sharing met 200+ leveranciers.
Quick Knowledge Check
Vraag 1: Wat is het belangrijkste voordeel van Snowflake's architectuur?
Vraag 2: Wat is Zero-Copy Cloning in Snowflake?
Vraag 3: Hoe lang kun je Time Travel gebruiken in Snowflake?
Les 2: Virtual Warehouses & Scaling
Compute los van opslag
Een virtual warehouse is in Snowflake niets anders dan een cluster rekenkracht. Het bevat geen data: dat staat in de opslaglaag die alle warehouses delen. Zet je een warehouse uit, dan blijft je data gewoon staan en betaal je alleen nog opslag.
Dat is het punt waar Snowflake zich onderscheidt van een klassiek datawarehouse. Je financiële team kan een zware maandafsluiting draaien zonder dat de dashboards van de rest van het bedrijf trager worden, omdat beide groepen hun eigen warehouse gebruiken op dezelfde data.
Onthoud dit
Opslag reken je af per terabyte per maand. Compute reken je af per seconde dat een warehouse draait, met een minimum van 60 seconden per keer dat hij aangaat. Een warehouse die niets doet maar wel aanstaat, kost dus geld.
Maten en wat ze kosten
Elke stap omhoog verdubbelt het aantal servers én het verbruik per uur. Een Medium kost precies twee keer zoveel per uur als een Small — maar doet een zware query vaak ook in de helft van de tijd. Dan betaal je hetzelfde en ben je eerder klaar.
| Maat | Credits per uur | Waarvoor |
|---|---|---|
| X-Small | 1 | Ontwikkelen, kleine transformaties, ad-hocvragen |
| Small | 2 | Dagelijkse laadprocessen op enkele miljoenen rijen |
| Medium | 4 | Dashboards met meerdere gebruikers tegelijk |
| Large | 8 | Zware joins, grote aggregaties, maandafsluiting |
| X-Large en groter | 16+ | Terabytes verwerken, verdubbelt per stap |
Groter is niet altijd duurder
Duurt een query 40 minuten op een Small, probeer hem dan eens op een Medium. Loopt hij in 20 minuten, dan is je rekening identiek en heb je je antwoord twee keer zo snel. Schaalt hij niet mee, dan zit het knelpunt ergens anders — meestal in de query zelf, niet in de hoeveelheid rekenkracht.
Een warehouse aanmaken
-- Warehouse voor de dagelijkse laadprocessen
CREATE WAREHOUSE laden_wh WITH
WAREHOUSE_SIZE = 'SMALL'
AUTO_SUSPEND = 60 -- na 60 seconden niksdoen vanzelf uit
AUTO_RESUME = TRUE -- gaat weer aan zodra er een query komt
INITIALLY_SUSPENDED = TRUE; -- start uit, niet aan
-- Warehouse voor de dashboards, met ruimte voor gelijktijdige gebruikers
CREATE WAREHOUSE bi_wh WITH
WAREHOUSE_SIZE = 'MEDIUM'
AUTO_SUSPEND = 300
AUTO_RESUME = TRUE
MIN_CLUSTER_COUNT = 1
MAX_CLUSTER_COUNT = 3 -- meer clusters zodra de wachtrij oploopt
SCALING_POLICY = 'STANDARD';
-- Maat aanpassen kan terwijl hij draait; lopende queries gaan gewoon door
ALTER WAREHOUSE laden_wh SET WAREHOUSE_SIZE = 'MEDIUM';
-- Handmatig uitzetten
ALTER WAREHOUSE laden_wh SUSPEND;
Twee manieren van schalen
Verwar deze twee niet, want ze lossen een ander probleem op.
Groter maken (van Small naar Medium) helpt tegen één zware query. Meer servers in hetzelfde cluster betekent meer geheugen en meer parallelle verwerking voor dat ene zware werkstuk.
Meer clusters (van 1 naar 3) helpt tegen veel gebruikers tegelijk. Zodra er queries in de wachtrij komen te staan, start Snowflake een extra cluster van dezelfde maat. Elke query wordt er niet sneller van, maar niemand staat meer te wachten.
Hoe je het herkent
Staan queries lang in de wachtrij maar lopen ze daarna snel? Dan heb je meer clusters nodig. Starten ze meteen maar duren ze lang? Dan is een grotere maat de juiste knop — of je query deugt niet.
Multi-cluster in de praktijk
-- ECONOMY wacht langer voor hij een cluster bijzet: goedkoper,
-- maar gebruikers staan soms even in de rij.
ALTER WAREHOUSE bi_wh SET
MAX_CLUSTER_COUNT = 4
SCALING_POLICY = 'ECONOMY';
-- Kijken of er wachtrijen ontstaan in het afgelopen etmaal
SELECT
warehouse_name,
SUM(avg_running) AS gemiddeld_actief,
SUM(avg_queued_load) AS gemiddeld_in_wachtrij
FROM snowflake.account_usage.warehouse_load_history
WHERE start_time >= DATEADD(day, -1, CURRENT_TIMESTAMP())
GROUP BY warehouse_name
ORDER BY gemiddeld_in_wachtrij DESC;
De resultaatcache: gratis antwoorden
Draait iemand exact dezelfde query nog een keer en is de onderliggende data niet gewijzigd, dan geeft Snowflake het antwoord uit de cache. Dat kost nul credits en gaat vrijwel direct — het warehouse hoeft er niet eens voor aan te staan. De cache houdt een resultaat 24 uur vast, en bij hergebruik schuift die termijn op tot maximaal 31 dagen.
Dit is waarom een dashboard dat tien mensen achter elkaar openen maar één keer echt geld kost. Het is ook waarom je bij het meten van queryprestaties de cache moet uitzetten, anders meet je niets.
-- Alleen voor het meten: zet de resultaatcache uit in deze sessie
ALTER SESSION SET USE_CACHED_RESULT = FALSE;
-- Terugzetten als je klaar bent met meten
ALTER SESSION SET USE_CACHED_RESULT = TRUE;
-- Welke queries kwamen uit de cache?
SELECT
query_text,
warehouse_name,
execution_status,
total_elapsed_time / 1000 AS seconden,
credits_used_cloud_services
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD(hour, -2, CURRENT_TIMESTAMP())
AND warehouse_name IS NULL -- geen warehouse gebruikt = uit de cache
ORDER BY start_time DESC
LIMIT 20;
Oefening: warehouses indelen
Situatie: een groothandel met drie soorten gebruik:
- Elke nacht om 02:00 een laadproces van ongeveer 40 minuten op een Small
- Overdag 25 mensen op Power BI-dashboards, vooral tussen 09:00 en 10:00
- Twee analisten die onregelmatig zware ad-hocquery's draaien
Opdracht:
- Hoeveel warehouses maak je aan, en waarom niet één?
- Welke maat geef je elk, en welke zet je op multi-cluster?
- Welke AUTO_SUSPEND kies je per warehouse? Let op het verschil tussen een nachtelijk batchproces en een dashboard waar de hele dag door iemand op klikt.
- De analisten klagen dat hun query's traag zijn tijdens de ochtendpiek. Wat is er aan de hand als ze hun eigen warehouse hebben?
Richting: drie warehouses. Een Small voor het laden met AUTO_SUSPEND op 60. Een Medium voor BI met maximaal drie clusters en AUTO_SUSPEND rond 300, zodat hij tussen twee dashboardklikken niet steeds afslaat. Een X-Small voor de analisten die ze zelf tijdelijk opschalen. Zijn hun query's traag terwijl ze een eigen warehouse hebben, dan ligt het niet aan drukte van anderen: dan scant hun query te veel data. Dat is het onderwerp van les 4.
Les 3: Data laden in Snowflake
Stages: waar je bestanden landen
Voordat data in een tabel komt, staat hij op een stage. Dat is een plek waar Snowflake bestanden kan lezen. Er zijn er drie soorten, en het verschil zit in wie de opslag bezit.
- Gebruikersstage (
@~) — privé per account, handig om snel iets te testen. - Tabelstage (
@%tabelnaam) — hoort bij één tabel, geen aparte configuratie nodig. - Externe stage — wijst naar jouw eigen S3-bucket, Azure-container of GCS-bucket. Dit gebruik je in productie, omdat de bestanden dan blijven staan waar je ze al beheert.
-- Vertel Snowflake hoe je bestanden eruitzien
CREATE OR REPLACE FILE FORMAT csv_formaat
TYPE = 'CSV'
FIELD_DELIMITER = ';'
SKIP_HEADER = 1
FIELD_OPTIONALLY_ENCLOSED_BY = '"'
NULL_IF = ('', 'NULL', 'null')
EMPTY_FIELD_AS_NULL = TRUE
DATE_FORMAT = 'DD-MM-YYYY';
-- Externe stage op S3, met een storage integration voor de rechten
CREATE OR REPLACE STAGE orders_stage
URL = 's3://mijn-bucket/orders/'
STORAGE_INTEGRATION = s3_integratie
FILE_FORMAT = csv_formaat;
-- Kijken wat er klaarstaat zonder iets te laden
LIST @orders_stage;
-- En even in een bestand gluren
SELECT $1, $2, $3
FROM @orders_stage/orders_2026_01.csv
LIMIT 5;
COPY INTO: de werkpaard-opdracht
COPY INTO leest bestanden van een stage en schrijft ze naar een tabel. Snowflake onthoudt welke bestanden al geladen zijn, dus dezelfde opdracht twee keer draaien levert geen dubbele rijen op. Dat gedrag is standaard en bespaart je een hoop ellende.
CREATE OR REPLACE TABLE orders_bronze (
order_id NUMBER,
klant_id NUMBER,
besteldatum DATE,
bedrag NUMBER(12,2),
status VARCHAR,
geladen_op TIMESTAMP_NTZ DEFAULT CURRENT_TIMESTAMP()
);
-- Eerst droogzwemmen: valideren zonder te schrijven
COPY INTO orders_bronze
FROM @orders_stage
FILE_FORMAT = (FORMAT_NAME = csv_formaat)
VALIDATION_MODE = 'RETURN_ERRORS';
-- Daadwerkelijk laden
COPY INTO orders_bronze (order_id, klant_id, besteldatum, bedrag, status)
FROM (
SELECT $1::NUMBER, $2::NUMBER, $3::DATE, $4::NUMBER(12,2), $5::VARCHAR
FROM @orders_stage
)
FILE_FORMAT = (FORMAT_NAME = csv_formaat)
ON_ERROR = 'CONTINUE'; -- foute rijen overslaan, de rest wel laden
-- Wat ging er mis bij de laatste lading?
SELECT * FROM TABLE(VALIDATE(orders_bronze, JOB_ID => '_last'));
Let op ON_ERROR
De standaard is ABORT_STATEMENT: één foute rij en de hele lading gaat niet door. Dat is vaak precies wat je wilt in productie, want half geladen data is erger dan geen data. Zet je het op CONTINUE, controleer dan altijd achteraf met VALIDATE wat je hebt overgeslagen — anders mis je stilletjes rijen.
Bestandsgrootte bepaalt je snelheid
Snowflake verdeelt het laden over de servers in je warehouse. Eén bestand van 10 GB wordt door één thread verwerkt; honderd bestanden van 100 MB gaan parallel. Mik op bestanden van 100 tot 250 MB gecomprimeerd. Veel kleinere bestandjes is ook niet goed: dan gaat de overhead per bestand overheersen.
Snowpipe: laden zodra het bestand er is
Met COPY INTO bepaal jij wanneer er geladen wordt. Met Snowpipe doet Snowflake dat zelf, binnen een minuut nadat er een bestand in je bucket verschijnt. Je betaalt per verwerkt bestand in plaats van per draaiend warehouse, wat gunstig is bij een gestage stroom kleine bestanden en juist ongunstig bij één grote lading per nacht.
CREATE OR REPLACE PIPE orders_pipe
AUTO_INGEST = TRUE
AS
COPY INTO orders_bronze (order_id, klant_id, besteldatum, bedrag, status)
FROM (
SELECT $1::NUMBER, $2::NUMBER, $3::DATE, $4::NUMBER(12,2), $5::VARCHAR
FROM @orders_stage
)
FILE_FORMAT = (FORMAT_NAME = csv_formaat);
-- Dit ARN zet je als melding op je S3-bucket
SHOW PIPES;
-- Draait hij, en loopt er iets achter?
SELECT SYSTEM$PIPE_STATUS('orders_pipe');
-- Wat heeft de pipe de afgelopen dag gedaan?
SELECT file_name, status, row_count, first_error_message, last_load_time
FROM TABLE(snowflake.information_schema.copy_history(
TABLE_NAME => 'ORDERS_BRONZE',
START_TIME => DATEADD(day, -1, CURRENT_TIMESTAMP())
))
ORDER BY last_load_time DESC;
Halfgestructureerde data: JSON zonder gedoe
Het VARIANT-datatype slaat JSON op zoals het is. Je hoeft vooraf geen kolommen te bedenken, en je bevraagt het met puntnotatie. Verandert de structuur van je bron, dan breekt je laadproces niet.
CREATE OR REPLACE TABLE events_raw (payload VARIANT);
COPY INTO events_raw
FROM @events_stage
FILE_FORMAT = (TYPE = 'JSON');
-- Velden eruit halen met puntnotatie; :: zet het om naar een echt type
SELECT
payload:event_id::STRING AS event_id,
payload:gebruiker.naam::STRING AS gebruikersnaam,
payload:tijdstip::TIMESTAMP_NTZ AS tijdstip,
payload:bedrag::NUMBER(12,2) AS bedrag
FROM events_raw
WHERE payload:type::STRING = 'bestelling';
-- Een array uitklappen naar losse rijen
SELECT
e.payload:order_id::STRING AS order_id,
r.value:sku::STRING AS sku,
r.value:aantal::NUMBER AS aantal
FROM events_raw e,
LATERAL FLATTEN(input => e.payload:regels) r;
Oefening: een laadstraat kiezen
Situatie: je krijgt drie bronnen binnen:
- Elke nacht één CSV-export van 8 GB uit het ERP-systeem
- Doorlopend JSON-bestandjes van ongeveer 2 MB uit een webshop, een paar per minuut
- Eens per kwartaal een handmatige Excel-aanlevering van de afdeling inkoop
Opdracht:
- Kies per bron tussen COPY INTO en Snowpipe, en onderbouw waarom.
- Wat doe je met die 8 GB voordat je hem laadt?
- Welk datatype gebruik je voor de webshopdata, en waarom niet meteen losse kolommen?
- Welke ON_ERROR-instelling kies je voor het ERP-bestand, en welke voor de handmatige aanlevering?
Richting: het ERP-bestand splits je in stukken van 100 tot 250 MB en laad je met COPY INTO op een vast tijdstip; ON_ERROR = 'ABORT_STATEMENT', want een halve financiële lading wil je niet. De webshopdata is precies waar Snowpipe voor bedoeld is: klein, continu, en als VARIANT opgeslagen zodat een extra veld in de JSON niets breekt. De kwartaalaanlevering doe je met de hand, met VALIDATION_MODE vooraf — bij handwerk zijn fouten de regel, niet de uitzondering.
Les 4: Micro-partities en performance
Wat Snowflake onder water doet
Snowflake bewaart je tabellen in micro-partities: blokken van 50 tot 500 MB onverpakte data, kolomsgewijs opgeslagen. Je maakt ze niet zelf aan en je kunt ze niet aanpassen; Snowflake deelt je data automatisch op zodra je laadt.
Van elke micro-partitie houdt Snowflake bij wat de kleinste en grootste waarde per kolom is. Dat is het hele geheim achter de snelheid. Vraag je om bestellingen van januari 2026, dan kijkt Snowflake eerst in die metadata en slaat elke partitie over waarin geen enkele datum in die maand voorkomt. Dat heet pruning.
Onthoud dit
Een query wordt niet traag omdat je tabel groot is. Hij wordt traag omdat Snowflake te veel partities moet openen. Alle optimalisatie in Snowflake draait om één vraag: hoe zorg ik dat er minder gescand hoeft te worden?
Meten hoeveel je scant
Het queryprofiel vertelt je precies hoeveel partities er zijn gelezen van het totaal. Staat daar "3.200 van 3.200", dan is er niets weggeprund en scan je je hele tabel.
-- Hoeveel partities raakten je zwaarste query's aan?
SELECT
query_id,
LEFT(query_text, 80) AS query,
partitions_scanned,
partitions_total,
ROUND(100 * partitions_scanned / NULLIF(partitions_total, 0), 1) AS pct_gescand,
bytes_spilled_to_local_storage AS gemorst_lokaal,
total_elapsed_time / 1000 AS seconden
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD(day, -7, CURRENT_TIMESTAMP())
AND partitions_total > 0
ORDER BY partitions_scanned DESC
LIMIT 20;
De kolom gemorst_lokaal verdient aandacht. Morst een query naar schijf, dan paste de bewerking niet in het geheugen van je warehouse. Dat is het duidelijkste signaal dat een maat groter wél helpt — in tegenstelling tot de meeste andere gevallen.
Clustering: de volgorde van je data
Bij het laden belandt data in de volgorde waarin hij binnenkomt. Laad je elke nacht de bestellingen van die dag, dan staan ze vanzelf netjes op datum en prunet een datumfilter uitstekend. Filter je echter vooral op klant, dan staan die klantrijen verspreid over duizenden partities en helpt pruning je niets.
Een clustering key laat Snowflake je data op de achtergrond herordenen op de kolommen waarop je filtert.
-- Eerst kijken of het nodig is. Let op average_overlaps:
-- hoe hoger, hoe meer partities elkaar overlappen en hoe slechter het pruned.
SELECT SYSTEM$CLUSTERING_INFORMATION('orders', '(besteldatum, klant_id)');
-- Pas daarna instellen
ALTER TABLE orders CLUSTER BY (besteldatum, klant_id);
-- Wat kost het herordenen?
SELECT
table_name,
SUM(credits_used) AS credits,
SUM(num_rows_reclustered) AS rijen_herordend
FROM snowflake.account_usage.automatic_clustering_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY table_name
ORDER BY credits DESC;
-- Weer uitzetten als het meer kost dan het oplevert
ALTER TABLE orders DROP CLUSTERING KEY;
Clustering kost doorlopend geld
Snowflake herordent op de achtergrond, en dat verbruikt credits zolang je blijft laden. Zet het alleen aan op tabellen boven ongeveer een terabyte die je vaak bevraagt op een kolom waarop ze niet vanzelf geordend zijn. Op een tabel van tien miljoen rijen is het weggegooid geld. Zet de kolom met de minste verschillende waarden vooraan in de key.
Vier dingen die echt schelen
Selecteer geen kolommen die je niet gebruikt. Snowflake slaat kolomsgewijs op, dus SELECT * op een tabel met tachtig kolommen leest alle tachtig. Noem de vijf die je nodig hebt en je leest een fractie.
Filter op de kolom zelf, niet op een bewerking ervan. Zodra je een functie om een kolom heen zet, kan Snowflake de metadata niet meer gebruiken en vervalt het pruning.
Gebruik geen DISTINCT om dubbele rijen van een slechte join te verbergen. Dat verdoezelt het probleem en kost een volledige sortering.
Materialiseer wat je steeds opnieuw berekent. Een zware aggregatie die tien dashboards voeden moet, hoort één keer berekend te worden.
-- FOUT: de functie om besteldatum heen maakt pruning onmogelijk
SELECT klant_id, SUM(bedrag)
FROM orders
WHERE YEAR(besteldatum) = 2026
GROUP BY klant_id;
-- GOED: een bereik op de kolom zelf, metadata blijft bruikbaar
SELECT klant_id, SUM(bedrag)
FROM orders
WHERE besteldatum >= '2026-01-01'
AND besteldatum < '2027-01-01'
GROUP BY klant_id;
-- FOUT: hoofdletterongevoelig zoeken sloopt het pruning ook
SELECT * FROM klanten WHERE UPPER(email) = 'INFO@VOORBEELD.NL';
-- GOED: sla de genormaliseerde waarde op bij het laden en filter daarop
SELECT * FROM klanten WHERE email_lower = 'info@voorbeeld.nl';
Materialized views
Een materialized view bewaart het resultaat en houdt het automatisch bij als de brontabel verandert. Dat kost opslag en achtergrondcredits, maar bespaart rekenwerk bij elke bevraging. Ze kennen flinke beperkingen: geen joins, geen window-functies, één tabel.
Heb je wél een join of een window-functie nodig, kijk dan naar dynamic tables. Die zijn bedoeld voor hele transformatieketens met een doorlooptijd die je zelf bepaalt. Het verschil tussen beide staat uitgewerkt in het artikel over Dynamic Tables versus views, onderaan deze pagina.
CREATE OR REPLACE MATERIALIZED VIEW omzet_per_dag AS
SELECT
besteldatum,
COUNT(*) AS aantal_orders,
SUM(bedrag) AS omzet
FROM orders
GROUP BY besteldatum;
-- Wat kost het onderhoud ervan?
SELECT table_name, SUM(credits_used) AS credits
FROM snowflake.account_usage.materialized_view_refresh_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY table_name
ORDER BY credits DESC;
Oefening: een trage query ontleden
Situatie: een rapportage over klantgedrag draait elke ochtend en duurt veertig minuten op een Medium. De tabel orders telt 2 miljard rijen en wordt dagelijks bijgeladen. Uit het queryprofiel blijkt: 11.800 van 12.000 partities gescand, 40 GB gemorst naar lokale schijf.
Opdracht:
- Wat vertelt dat aantal gescande partities je over de filters in de query?
- Wat betekent het morsen naar schijf, en welke knop helpt daar wél tegen?
- De query filtert op
WHERE DATE_TRUNC('month', besteldatum) = '2026-01-01'. Wat is daar mis mee en hoe schrijf je het anders? - Zou je hier clustering aanzetten? Waarop let je bij die afweging?
Richting: bijna alles scannen betekent dat er effectief niet gefilterd wordt — en die DATE_TRUNC om de kolom heen is precies de reden. Herschrijf naar een bereik met >= en < en het pruning komt terug. Het morsen zegt dat de bewerking niet in het geheugen past; dat is het geval waarin een stap groter echt helpt. Clustering is hier te overwegen vanwege de omvang, maar probeer eerst de herschreven query: als de data al op laaddatum geordend staat, prunet een datumbereik vanzelf goed en bespaar je de doorlopende clusteringkosten.
Les 5: Time Travel en Zero-Copy Cloning
Terugkijken in de tijd
Omdat Snowflake micro-partities nooit aanpast maar vervangt, staat de oude versie er nog. Time Travel maakt die bereikbaar. Je kunt een tabel bevragen zoals hij er een uur geleden uitzag, of vlak voor een specifieke query.
Op Standard is de bewaartermijn één dag. Op Enterprise kun je per tabel tot negentig dagen instellen. Die termijn kost opslag, dus zet hem niet overal maximaal.
-- Drie manieren om terug te kijken
SELECT * FROM orders AT(OFFSET => -60 * 15); -- 15 minuten geleden
SELECT * FROM orders AT(TIMESTAMP => '2026-10-24 08:00:00'::TIMESTAMP_NTZ);
SELECT * FROM orders BEFORE(STATEMENT => '01b2c3d4-...'); -- vlak voor die query
-- Wat is er precies veranderd sinds vanochtend?
SELECT COUNT(*) AS nu FROM orders;
SELECT COUNT(*) AS vanochtend FROM orders AT(TIMESTAMP => '2026-10-24 06:00:00'::TIMESTAMP_NTZ);
-- Per tabel instellen hoe ver je terug wilt kunnen
ALTER TABLE orders SET DATA_RETENTION_TIME_IN_DAYS = 30;
-- Een verwijderde tabel terughalen
UNDROP TABLE orders;
Na een foute UPDATE
Heeft iemand een UPDATE zonder WHERE gedraaid, zoek dan het query-id op in query_history en herstel met BEFORE(STATEMENT => ...). Dat is nauwkeuriger dan een tijdstip gokken. Doe het wel snel: zodra de bewaartermijn verstrijkt, is de oude versie echt weg.
Herstellen in de praktijk
-- 1. Zoek de schuldige query op
SELECT query_id, query_text, user_name, start_time, rows_updated
FROM snowflake.account_usage.query_history
WHERE query_type = 'UPDATE'
AND start_time >= DATEADD(hour, -4, CURRENT_TIMESTAMP())
ORDER BY start_time DESC;
-- 2. Controleer eerst of de oude stand er goed uitziet
SELECT * FROM orders BEFORE(STATEMENT => '01b2c3d4-0000-abcd-0000-000000000001')
LIMIT 20;
-- 3. Zet een kopie van de oude stand apart
CREATE TABLE orders_herstel AS
SELECT * FROM orders BEFORE(STATEMENT => '01b2c3d4-0000-abcd-0000-000000000001');
-- 4. Pas daarna omwisselen; SWAP is atomair
ALTER TABLE orders SWAP WITH orders_herstel;
Zero-copy cloning
Een clone is een nieuwe tabel, schema of database die in eerste instantie naar dezelfde micro-partities wijst. Er wordt niets gekopieerd, dus het gaat vrijwel direct en kost in het begin geen extra opslag. Pas als je in de clone gaat schrijven, ontstaan er nieuwe partities en betaal je voor dat verschil.
Dit is waarom je in Snowflake zonder bezwaar een volledige acceptatieomgeving kunt neerzetten op productiedata, of een experiment kunt doen zonder iemand in de weg te zitten.
-- Hele database klonen voor acceptatie
CREATE DATABASE acc_db CLONE prod_db;
-- Klonen zoals het er gisteren uitzag: Time Travel en cloning combineren
CREATE TABLE orders_gisteren
CLONE orders AT(TIMESTAMP => DATEADD(day, -1, CURRENT_TIMESTAMP()));
-- Veilig experimenteren met een zware wijziging
CREATE TABLE orders_test CLONE orders;
ALTER TABLE orders_test CLUSTER BY (klant_id);
-- meten, vergelijken, en daarna opruimen
DROP TABLE orders_test;
Twee dingen om te weten
Een clone neemt geen toegangsrechten op de onderliggende objecten over; die ken je opnieuw toe. En hoewel klonen zelf gratis is, kost een clone van een tabel die je daarna volledig herschrijft wél de volle opslag. Ruim testclones dus op.
Oefening: herstel na een ongeluk
Situatie: om 14:20 heeft een collega een DELETE FROM klanten gedraaid zonder WHERE. Het is nu 15:05. De tabel staat op de standaardbewaartermijn van één dag en er zijn sindsdien alweer nieuwe klanten aangemaakt door de applicatie.
Opdracht:
- Hoe vind je het exacte moment terug in plaats van 14:20 te gokken?
- Waarom zet je de oude stand eerst in een aparte tabel in plaats van meteen terug te schrijven?
- Hoe zorg je dat de klanten die ná 14:20 zijn aangemaakt niet verdwijnen bij het herstel?
- Wat stel je achteraf in zodat dit minder pijn doet als het nog eens gebeurt?
Richting: zoek het query-id in query_history en gebruik BEFORE(STATEMENT => ...). Zet die stand in klanten_herstel zodat je kunt controleren voordat je iets omwisselt. Voor de nieuwe klanten combineer je: neem de herstelde rijen en voeg daar met MERGE de rijen uit de huidige tabel aan toe die na het incident zijn aangemaakt. En verhoog daarna DATA_RETENTION_TIME_IN_DAYS op je kerntabellen — één dag is krap als een fout pas de volgende ochtend opvalt.
Les 6: Toegangsbeheer en kosten
Rechten hangen aan rollen, niet aan mensen
In Snowflake krijgt een gebruiker nooit rechtstreeks rechten op een tabel. Rechten horen bij een rol, en die rol ken je toe aan een gebruiker. Rollen kunnen elkaar bovendien bevatten, zodat je een hiërarchie bouwt.
Die laatste eigenschap wordt vaak verkeerd begrepen. Geef je rol A aan rol B, dan krijgt B álles wat A mag. Je bouwt dus van beperkt naar ruim: de analistenrol gaat in de engineersrol, niet andersom.
USE ROLE SECURITYADMIN;
CREATE ROLE analist;
CREATE ROLE engineer;
-- Analisten mogen lezen
GRANT USAGE ON DATABASE prod_db TO ROLE analist;
GRANT USAGE ON SCHEMA prod_db.gold TO ROLE analist;
GRANT SELECT ON ALL TABLES IN SCHEMA prod_db.gold TO ROLE analist;
-- ook op tabellen die er later bij komen
GRANT SELECT ON FUTURE TABLES IN SCHEMA prod_db.gold TO ROLE analist;
GRANT USAGE ON WAREHOUSE bi_wh TO ROLE analist;
-- Engineers mogen alles wat analisten mogen, plus schrijven
GRANT ROLE analist TO ROLE engineer;
GRANT ALL ON SCHEMA prod_db.silver TO ROLE engineer;
GRANT USAGE ON WAREHOUSE laden_wh TO ROLE engineer;
-- Pas daarna aan mensen toekennen
GRANT ROLE analist TO USER jansen;
-- Controleren wat een rol werkelijk mag
SHOW GRANTS TO ROLE analist;
Vergeet FUTURE GRANTS niet
Zonder ON FUTURE TABLES ziet je analistenrol elke tabel die je morgen aanmaakt niet. Dat is de meest voorkomende klacht na een nieuwe inrichting: "ik zie de tabel wel in de lijst maar krijg geen rechten". En USAGE op database én schema is altijd nodig voordat SELECT op een tabel iets doet.
Niet iedereen mag alles zien
Twee mechanismen beperken wat een rol binnen een tabel te zien krijgt. Een masking policy verbergt de inhoud van een kolom; een row access policy beperkt welke rijen zichtbaar zijn.
-- E-mailadressen alleen zichtbaar voor wie dat nodig heeft
CREATE OR REPLACE MASKING POLICY email_masker AS (val STRING) RETURNS STRING ->
CASE
WHEN CURRENT_ROLE() IN ('ENGINEER', 'ACCOUNTADMIN') THEN val
ELSE REGEXP_REPLACE(val, '.+@', '*****@')
END;
ALTER TABLE klanten MODIFY COLUMN email
SET MASKING POLICY email_masker;
-- Accountmanagers zien alleen hun eigen regio
CREATE OR REPLACE ROW ACCESS POLICY regio_filter AS (regio STRING) RETURNS BOOLEAN ->
CURRENT_ROLE() = 'ENGINEER'
OR regio = CURRENT_USER(); -- in de praktijk via een koppeltabel
ALTER TABLE klanten ADD ROW ACCESS POLICY regio_filter ON (regio);
Grip op je rekening
Snowflake rekent af in credits. Je grootste kostenpost is bijna altijd compute, en de meestvoorkomende verspilling is een warehouse dat aanstaat zonder dat er iemand op werkt. AUTO_SUSPEND uit les 2 is daarom je belangrijkste knop.
Daarnaast zet je resource monitors in. Die waarschuwen je bij een percentage van je budget en kunnen een warehouse desnoods stilleggen.
USE ROLE ACCOUNTADMIN;
CREATE OR REPLACE RESOURCE MONITOR maandbudget WITH
CREDIT_QUOTA = 500
FREQUENCY = MONTHLY
START_TIMESTAMP = IMMEDIATELY
TRIGGERS
ON 75 PERCENT DO NOTIFY
ON 90 PERCENT DO NOTIFY
ON 100 PERCENT DO SUSPEND -- lopende query's mogen afmaken
ON 110 PERCENT DO SUSPEND_IMMEDIATE; -- hard stoppen
ALTER WAREHOUSE bi_wh SET RESOURCE_MONITOR = maandbudget;
-- Waar gaan de credits heen?
SELECT
warehouse_name,
ROUND(SUM(credits_used), 1) AS credits,
ROUND(SUM(credits_used) * 2.50, 0) AS indicatie_euro -- pas aan naar je eigen tarief
FROM snowflake.account_usage.warehouse_metering_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY warehouse_name
ORDER BY credits DESC;
-- Welke gebruikers verbruiken het meest?
SELECT
user_name,
COUNT(*) AS aantal_queries,
ROUND(SUM(total_elapsed_time) / 3600000, 1) AS draaiuren
FROM snowflake.account_usage.query_history
WHERE start_time >= DATEADD(day, -30, CURRENT_TIMESTAMP())
GROUP BY user_name
ORDER BY draaiuren DESC
LIMIT 15;
Waar je het eerst naar kijkt
Staat AUTO_SUSPEND overal aan en op een verstandige waarde? Draait er een warehouse groter dan nodig? Scant een terugkerende query bijna de hele tabel? Die drie vragen verklaren in de praktijk het leeuwendeel van een rekening die hoger uitvalt dan verwacht.
Oefening: inrichting voor een nieuw team
Situatie: er komt een team van drie externe analisten bij. Ze moeten rapportages kunnen maken op de gold-laag, maar mogen geen e-mailadressen of geboortedata van klanten zien, en niets kunnen wijzigen. Je verwacht dat ze vooral 's ochtends werken.
Opdracht:
- Welke rol maak je, en welke rechten geef je precies? Vergeet de twee niveaus USAGE niet.
- Hoe zorg je dat ze ook de tabellen zien die volgende maand worden toegevoegd?
- Welk mechanisme gebruik je voor de afgeschermde kolommen, en waarom niet gewoon een aparte view?
- Geef je ze een eigen warehouse of laat je ze meedraaien op het bestaande BI-warehouse? Welke overweging geeft de doorslag?
- Welke resource monitor hang je eraan?
Richting: een rol analist_extern met USAGE op database en schema, SELECT op alle huidige én toekomstige tabellen in gold, en USAGE op een warehouse. Masking policies verdienen de voorkeur boven een aparte view, omdat de afscherming dan aan de kolom hangt en niet te omzeilen is door de onderliggende tabel te bevragen. Een eigen X-Small warehouse is verdedigbaar omdat je dan precies ziet wat dit team kost en het de bestaande dashboards niet raakt tijdens de ochtendpiek — hang er dan wel een resource monitor aan met een bescheiden quotum en een SUSPEND op 100 procent.
Snowflake Carrière Kansen in Nederland
De vraag naar Snowflake experts groeit met 300% per jaar in Nederland. Gemiddelde salarissen (2024):
Tip: Snowflake certificeringen verhogen je salaris met 20-30%. De SnowPro Core certificering is de beste start.
Snowflake Certificering Pad
Na het voltooien van deze cursus ben je klaar voor:
SnowPro Core
Fundamentele kennis van Snowflake Data Cloud
SnowPro Advanced: Architect
Geavanceerde architectuur en implementatie
SnowPro Advanced: Data Engineer
Data engineering en pipelines op Snowflake
SnowPro Advanced: Administrator
Security, governance en beheer
Snowflake Learning Resources
Snowflake als dataplatform
Hoe Snowflake in een moderne data-architectuur past: opslag, compute, kosten en beheer.
Lees de platformpaginaWat is Snowflake?
De basis uitgelegd: architectuur, virtual warehouses, micro-partities en waar het zich onderscheidt.
Lees het artikelDynamic Tables vs Views
Verschil in verversing, kosten, latency en onderhoud, met SQL-voorbeelden en een beslisboom.
Lees het artikelQuery Optimizer
Plak een trage query en krijg concrete suggesties voor clustering, pruning en warehouse-grootte.
Open de toolJouw Snowflake Learning Journey
Na deze complete guide kun je door naar:
- Advanced Snowflake Performance Tuning - Query optimization, clustering keys, materialized views
- Snowflake for Data Science - Snowpark ML, time series analysis, geospatial data
- Snowflake Security & Governance - RBAC, data masking, tagging, compliance
- Snowflake Data Engineering - Streams, tasks, Snowpipe, orchestration
- Snowflake Ecosystem Integration - dbt, Airflow, Fivetran, Tableau
Actuele Snowflake Vacatures
Bekijk onze Snowflake vacatures voor de nieuwste kansen bij top bedrijven in Nederland zoals ING, ABN AMRO, KLM, en Philips.
Snowflake vs Andere Cloud Data Platforms
| Platform | Sterke Punten | Zwakke Punten | Best Voor | Prijzen (Nederland) |
|---|---|---|---|---|
| Snowflake | Zero-copy cloning, Data Sharing, Multi-cloud | Kostbaar voor kleine workloads | Enterprise, Data Sharing, Multi-cloud | €2-€4 per credit |
| Databricks | Machine Learning, Delta Lake, Spark | Complexer voor pure SQL | Data Science, ML, Streaming | €0.40-€0.70 per DBU |
| Google BigQuery | Serverless, Machine Learning, Google Cloud | Vendor lock-in, minder multi-cloud | Google Cloud ecosystem | €4.80 per TB (on-demand) |
| Azure Synapse | Azure integratie, T-SQL compatible | Minder mature data sharing | Microsoft ecosystem | €1.20 per hour (DWU100) |
| Amazon Redshift | AWS integratie, goed voor grote datasets | Minder flexibel schalen | AWS native workloads | €0.85 per hour (dc2.large) |
Let op: prijzen zijn indicatief en kunnen variëren per regio en gebruik.