Stap 1: Diagnose — waar zit het probleem?
Voor je begint met optimaliseren, moet je weten wat je moet optimaliseren. Willekeurig indexen toevoegen of queries herschrijven zonder te begrijpen waar de bottleneck zit, leidt zelden tot de gewenste verbetering.
Begin met het identificeren van de trage queries. In SQL Server gebruik je de Dynamic Management Views (DMV's):
-- Vind de 10 traagste queries in SQL Server
SELECT TOP 10
qs.total_elapsed_time / qs.execution_count AS gem_duur_ms,
qs.execution_count,
qs.total_logical_reads / qs.execution_count AS gem_reads,
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset END
- qs.statement_start_offset)/2)+1) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY gem_duur_ms DESC;
In PostgreSQL gebruik je de pg_stat_statements extensie:
-- Vind trage queries in PostgreSQL
SELECT
round(mean_exec_time::numeric, 2) AS gem_ms,
calls,
round(total_exec_time::numeric, 2) AS totaal_ms,
query
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 20;
Zodra je de trage queries hebt geïdentificeerd, analyseer je het execution plan van elke query. Dat is de volgende stap.
Execution plans lezen
Een execution plan toont hoe de database query-optimizer van plan is (of was) de query uit te voeren: welke indexen worden gebruikt, hoe tabellen worden gescand, hoe JOINs worden uitgevoerd en hoeveel rijen de optimizer verwacht te verwerken vs. hoeveel er werkelijk verwerkt werden.
In SQL Server bekijk je het execution plan via SET STATISTICS IO ON en het grafische execution plan in SQL Server Management Studio. In PostgreSQL gebruik je EXPLAIN ANALYZE:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT
o.order_id,
c.naam,
SUM(ol.bedrag) AS totaal
FROM orders o
JOIN klanten c ON c.klant_id = o.klant_id
JOIN order_regels ol ON ol.order_id = o.order_id
WHERE o.datum >= '2024-01-01'
AND o.status = 'VOLTOOID'
GROUP BY o.order_id, c.naam;
De rode vlaggen in een execution plan:
- Sequential Scan / Table Scan op een grote tabel: de database scant elke rij. Dit is het teken dat een index ontbreekt of niet wordt gebruikt.
- Hash Join met grote tabellen: kan wijzen op ontbrekende indexen op JOIN-kolommen.
- Estimated vs Actual rows sterk afwijkend: de statistieken zijn verouderd. Run
ANALYZE(PostgreSQL) ofUPDATE STATISTICS(SQL Server). - Sort operaties op grote datasets zonder index op de ORDER BY kolom.
- Nested Loop met grote tabellen: efficiënt voor kleine resultaatsets, maar kan exponentieel vertragen bij grotere datasets.
Indexen: de krachtigste optimalisatietool
Een goed geplaatste index is de meest effectieve manier om query-performance te verbeteren. Een index stelt de database in staat om direct naar specifieke rijen te navigeren in plaats van de hele tabel te scannen — vergelijkbaar met de index in een boek.
B-tree indexen (standaard)
De standaard index in vrijwel alle databases is de B-tree (Balanced Tree) index. Ideaal voor:
- Equality conditions:
WHERE klant_id = 42 - Range conditions:
WHERE datum BETWEEN '2024-01-01' AND '2024-12-31' - ORDER BY en GROUP BY op de geïndexeerde kolom
- JOIN conditions
Covering indexen
Een covering index bevat alle kolommen die een query nodig heeft, zodat de database de tabel helemaal niet hoeft te lezen ("index-only scan"). Bijzonder krachtig voor high-frequency queries:
-- Query die we willen optimaliseren:
SELECT order_id, status, totaal_bedrag
FROM orders
WHERE klant_id = 42
AND datum >= '2024-01-01';
-- Covering index: bevat alle benodigde kolommen
CREATE INDEX idx_orders_klant_datum_covering
ON orders (klant_id, datum)
INCLUDE (order_id, status, totaal_bedrag);
Wanneer GEEN index toevoegen
Indexen zijn niet gratis: elke index vertraagt INSERT, UPDATE en DELETE operaties omdat de index bijgewerkt moet worden. Voeg geen index toe op:
- Kleine tabellen (< 1000 rijen) — een full scan is hier sneller.
- Kolommen met lage kardinaliteit (bijv. een boolean of een status met 3 waarden) — de selectiviteit is te laag.
- Kolommen die zelden in WHERE- of JOIN-clausules voorkomen.
- Tabellen met veel writes en weinig reads.
Query herschrijven
Soms is de query zelf het probleem. Veelvoorkomende anti-patterns die queries vertragen:
Functies op geïndexeerde kolommen in WHERE
-- Slecht: functie op geïndexeerde kolom verhindert index gebruik
SELECT * FROM orders WHERE YEAR(datum) = 2024;
-- Goed: range condition gebruikt de index wel
SELECT * FROM orders
WHERE datum >= '2024-01-01' AND datum < '2025-01-01';
SELECT * vermijden
SELECT * leest alle kolommen, ook als je er maar twee nodig hebt. Dit verhindert covering index gebruik en verhoogt network overhead. Specificeer altijd de benodigde kolommen.
N+1 queries
Het N+1 probleem: één query om een lijst van orders te ophalen, dan voor elke order een aparte query om de klantgegevens te laden. Oplossing: gebruik een JOIN of IN-clausule om alle benodigde data in één query op te halen.
CTEs vs subqueries vs tijdelijke tabellen
CTEs zijn leesbaar maar niet altijd sneller. In PostgreSQL materialiseert een CTE de resultaten soms terwijl een subquery inline wordt uitgevoerd — en soms andersom. Test altijd het execution plan van beide varianten. Als een CTE meerdere keren gerefereerd wordt, overweeg dan een tijdelijke tabel of materialized CTE.
Partitionering
Partitionering verdeelt een grote tabel in kleinere, beheersbare stukken op basis van een partitie-sleutel. Een query met een filter op de partitie-sleutel hoeft dan alleen de relevante partitie(s) te scannen — "partition pruning".
-- PostgreSQL: range partitionering op datum
CREATE TABLE events (
event_id BIGINT,
event_type VARCHAR(50),
datum DATE,
payload JSONB
) PARTITION BY RANGE (datum);
-- Maandelijkse partities
CREATE TABLE events_2024_01 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE events_2024_02 PARTITION OF events
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
-- Query gebruikt alleen de januaripartitie (partition pruning)
SELECT * FROM events
WHERE datum BETWEEN '2024-01-01' AND '2024-01-31';
Partitionering is effectief bij tabellen groter dan een paar honderd miljoen rijen. Voor kleinere tabellen is een goede index vaak voldoende.
Let op: kies de partitie-sleutel zorgvuldig. Vrijwel alle queries moeten een filter bevatten op de partitie-sleutel. Is dat niet het geval, dan heb je niets aan partitionering — de database scant alle partities (full partition scan).
Statistieken en vacuüm (PostgreSQL)
PostgreSQL's query-optimizer neemt beslissingen op basis van statistieken: schatting van het aantal rijen, de distributie van waarden en de grootte van tabellen. Verouderde statistieken leiden tot slechte query-plannen.
-- Update statistieken voor een specifieke tabel
ANALYZE orders;
-- Vacuüm: verwijder dead tuples en update statistieken
VACUUM ANALYZE orders;
-- Controleer wanneer een tabel voor het laatst gevacuümd is
SELECT
relname,
last_vacuum,
last_autovacuum,
last_analyze,
n_dead_tup,
n_live_tup
FROM pg_stat_user_tables
WHERE relname = 'orders';
Autovacuum doet dit automatisch in de achtergrond, maar bij tables met veel updates of deletes (bijv. een queue-tabel) kan het nodig zijn dit handmatig te triggeren of autovacuum agressiever in te stellen.
Caching strategieën
Caching vermindert de load op de database door veelgevraagde data in snel geheugen te bewaren. Meerdere cache-niveaus zijn mogelijk:
- Database buffer cache: de database zelf cached actief gebruikte pagina's in geheugen. Zorg dat de server voldoende RAM heeft voor de buffer cache. In PostgreSQL zie je de hit rate via
pg_stat_bgwriter. - Materialized views: precomputed resultaten van complexe queries die periodiek worden ververst. Ideaal voor dashboards die dezelfde zware berekening herhalen.
- Application-level cache: Redis of Memcached voor query-resultaten die meerdere keren per seconde worden opgevraagd en relatief stabiel zijn (bijv. een productcatalogus).
- CDN-caching: voor publieke rapport-pagina's en dashboards kan een CDN de HTML/JSON-output cachen zodat de database helemaal niet meer belast wordt voor veelgevraagde views.
Cloud-specifieke optimalisaties
Snowflake
- Clustering keys: vergelijkbaar met partitionering — definieer clustering keys op kolommen die in WHERE-clausules voorkomen.
- Result cache: Snowflake cached identieke query-resultaten automatisch 24 uur. Vermijd dynamische WHERE-clausules met functies als
CURRENT_TIMESTAMPdie caching verhinderen. - Warehouse grootte: kies de juiste warehouse grootte voor je workload. Groter is niet altijd sneller — sommige queries zijn beperkt door I/O, niet door compute.
BigQuery
- Partitionering en clustering: BigQuery rekent per verwerkte byte. Partitioneer op datum en cluster op veelgefiltreerde kolommen om kosten en latency te reduceren.
- BI Engine: in-memory analytische engine voor Power BI en Looker connecties. Reduceert latency drastisch voor dashboards.
- Materialized views: automatisch bijgewerkt en gebruikt door de optimizer indien relevant.
Azure Synapse / Databricks SQL
- Delta Lake OPTIMIZE: consolideert small files en maakt Z-ordering mogelijk. Run
OPTIMIZE tabel_naam ZORDER BY (kolom1, kolom2)periodiek op actieve tabellen. - Photon engine: Databricks' native vectorized query engine. Automatisch gebruikt, maar sommige query-patronen profiteren meer dan andere.
Continue monitoring
Performance-optimalisatie is geen eenmalige activiteit. Stel monitoring in op:
- Query duur P95/P99: het 95e of 99e percentiel van query-uitvoertijden. Gemiddelden maskeren uitschieters die gebruikers wél merken.
- Cache hit ratio: een buffer cache hit ratio onder de 95% is een signaal dat meer RAM nodig is.
- Index usage stats: controleer regelmatig welke indexen niet gebruikt worden (
pg_stat_user_indexesin PostgreSQL). Ongebruikte indexen zijn pure overhead. - Bloat: in PostgreSQL accumuleren dead tuples en index bloat. Monitor en plan regelmatige VACUUM-operaties.
- Connection pool utilization: als je connection pool bijna vol zit, worden queries geblokkeerd. Tune de pool grootte of kijk naar connection poolers als PgBouncer.
Conclusie
Database-optimalisatie is een systematisch proces, geen willekeurige serie tweaks. Begin altijd met meten: identificeer de trage queries, analyseer de execution plans, en dan pas intervenieer je gericht. De meest impactvolle ingrepen — in volgorde van effectiviteit — zijn: juiste indexen toevoegen, queries herschrijven om anti-patterns te elimineren, statistieken up-to-date houden, en partitioneren voor heel grote tabellen.
Voor cloud databases als Snowflake en BigQuery komen kostenoptimalisatie en performance-optimalisatie samen: een goed gepartitioneerde tabel is zowel sneller als goedkoper. Investeer in het begrip van hoe je specifieke cloud-database werkt — de documentatie van elke provider geeft waardevolle inzichten over de specifieke query-optimizer en optimalisatiemechanismen.
Database performance verbeteringen nodig?
DataPartner365 voert database performance-audits uit en implementeert optimalisaties voor SQL Server, PostgreSQL, Snowflake en Databricks.