Ruimtelijke Data··6 min read

Duikersgids: Hoe Ik Ruimtelijk Zoeken 55x Sneller Maakte

Leer hoe we PostgreSQL ruimtelijke queries optimaliseerden voor Duikersgids.nl, waardoor zoektijden daalden van 2.5 seconden naar 45ms met GiST indexen, partitioning en parallelle verwerkingstechnieken.

Categories

Database OptimalisatieRuimtelijke Data

Tags

PostgreSQLRuimtelijke QueriesGiST IndexenGeografische DataPerformance OptimalisatieDuikersgids

About the Author

Author avatar

Rob Schoenaker

Managing Partner bij UpstreamAds en Partner bij Ludulicious B.V. met meer dan 20 jaar ervaring in softwareontwikkeling, gespecialiseerd in .NET Core, ServiceStack, C# en database design.

Share:

Het Probleem: Ruimtelijke Queries Vermoorden Gebruikerservaring

In 2019 had Duikersgids.nl te maken met een kritiek performance probleem. Gebruikers die zochten naar duikstekken in de buurt van hun locatie wachtten 2.5 seconden op resultaten. Voor een locatie-gebaseerde applicatie was dit onacceptabel.

De Uitdaging:

  • 50.000+ duikstekken met geografische coördinaten
  • Complexe ruimtelijke queries met ST_DWithin en ST_Distance
  • Gebruikers die instant locatie-gebaseerde resultaten verwachten
  • Server die het moeilijk heeft onder belasting

De Cijfers:

-- Deze query deed er 2.5+ seconden over
SELECT ds.name, ds.location, ds.difficulty_level
FROM dive_sites ds
WHERE ST_DWithin(ds.location, ST_Point(4.5, 52.0), 10000)
  AND ds.status = 'active'
ORDER BY ST_Distance(ds.location, ST_Point(4.5, 52.0))
LIMIT 20;

De Oorzaak: Ontbrekende Ruimtelijke Indexen

Het probleem was duidelijk uit het execution plan:

EXPLAIN ANALYZE SELECT * FROM dive_sites 
WHERE ST_DWithin(location, ST_Point(4.5, 52.0), 10000);

-- Resultaat: Seq Scan on dive_sites (cost=0.00..1250.00 rows=5000 width=64)
-- Execution time: 2500.123 ms

Wat er gebeurde:

  • PostgreSQL deed een volledige tabel scan op 50.000+ records
  • Er bestonden geen ruimtelijke indexen voor geografische queries
  • Elke ruimtelijke operatie werd vanaf nul berekend
  • Geen optimalisatie voor veelgebruikte query patronen

De Oplossing: Strategische Ruimtelijke Indexing

Stap 1: Maak GiST Ruimtelijke Index

De eerste doorbraak kwam met een juiste ruimtelijke index:

-- De GiST index die alles veranderde
CREATE INDEX CONCURRENTLY idx_dive_sites_location 
ON dive_sites USING GIST (location);

Waarom Dit Werkt:

  • GIST (location): GiST indexen zijn specifiek geoptimaliseerd voor geometrische datatypes
  • Maakt snelle ruimtelijke operaties mogelijk zoals ST_DWithin, ST_Distance, en ST_Contains
  • Gebruikt R-tree structuur intern voor efficiënte ruimtelijke range queries
  • CONCURRENTLY maakt index creatie mogelijk zonder writes te blokkeren

Direct Resultaat: Query tijd daalde van 2.5 seconden naar 800ms (3x verbetering)

Stap 2: Voeg Partiële Index Toe voor Actieve Sites

De meeste queries hadden alleen actieve duikstekken nodig, dus maakten we een partiële index:

-- Partiële index voor actieve sites (enorme win!)
CREATE INDEX CONCURRENTLY idx_dive_sites_active 
ON dive_sites (created_at) 
WHERE status = 'active';

Waarom Dit Werkt:

  • WHERE status = 'active': Indexeert alleen rijen die aan de conditie voldoen
  • Dramatisch kleinere index grootte (alleen ~40.000 actieve sites vs 50.000+ totaal)
  • Snellere index scans en betere cache benutting
  • De meeste queries filteren toch op actieve status

Resultaat: Query tijd verbeterde naar 400ms (6x verbetering van origineel)

Stap 3: Maak Covering Index voor Veelgebruikte Queries

Ons meest voorkomende query patroon had locatie, duiktype en moeilijkheidsgraad nodig:

-- Samengestelde index voor ons meest voorkomende query patroon
CREATE INDEX CONCURRENTLY idx_dive_sites_location_type 
ON dive_sites USING GIST (location) 
INCLUDE (dive_type, difficulty_level);

Waarom Dit Werkt:

  • INCLUDE (dive_type, difficulty_level): Covering index bevat extra kolommen
  • Elimineert tabel lookups - alle benodigde data komt uit de index
  • GiST index handelt ruimtelijke operaties af, included kolommen leveren extra data
  • Perfect voor queries die locatie + metadata nodig hebben

Resultaat: Query tijd daalde naar 200ms (12x verbetering van origineel)

De Game Changer: PostgreSQL Partitioning

Het Probleem: Tijd-Gebaseerde Queries Nog Altijd Traag

Zelfs met ruimtelijke indexen waren queries die filterden op datum nog steeds traag:

-- Deze query scantte nog steeds alles
SELECT * FROM dive_sites 
WHERE ST_DWithin(location, ST_Point(4.5, 52.0), 10000)
  AND created_at > '2023-01-01';  -- Scantte nog steeds alle partitions!

De Oplossing: Native Partitioning

PostgreSQL 10+ native partitioning loste dit op:

-- PostgreSQL 10+: Native partitioning
CREATE TABLE dive_sites_partitioned (
    id SERIAL,
    name VARCHAR(255),
    location GEOGRAPHY(POINT, 4326),
    created_at TIMESTAMP WITH TIME ZONE
) PARTITION BY RANGE (created_at);

CREATE TABLE dive_sites_2023 PARTITION OF dive_sites_partitioned
FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');

CREATE TABLE dive_sites_2024 PARTITION OF dive_sites_partitioned
FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

Waarom Dit Werkt:

  • PARTITION BY RANGE (created_at): Splitst tabel op datum ranges
  • Elke partition is een aparte fysieke tabel
  • PostgreSQL bepaalt automatisch welke partitions te scannen
  • Queries met datum filters scannen alleen relevante partitions

Resultaat: Tijd-gefilterde queries daalden naar 100ms (25x verbetering)

De Finale Optimalisatie: Parallelle Verwerking

Het Probleem: Complexe Ruimtelijke Joins CPU-Gebonden

Complexe queries die nabijgelegen duikstekken vonden gebruikten maar één CPU core:

-- Deze query gebruikte maar één CPU core
SELECT ds1.name, ds2.name, 
       ST_Distance(ds1.location, ds2.location) as distance
FROM dive_sites ds1, dive_sites ds2 
WHERE ds1.id < ds2.id 
  AND ST_DWithin(ds1.location, ds2.location, 1000)
ORDER BY distance;

De Oplossing: Verbeterde Parallelle Verwerking

PostgreSQL 16+ parallelle verwerking configuratie:

-- PostgreSQL 16+: Betere parallelle uitvoering
SET max_parallel_workers_per_gather = 8;
SET parallel_leader_participation = off;
SET parallel_tuple_cost = 0.05;  -- Lagere kostendrempel

-- Dezelfde query gebruikt nu alle CPU cores
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT ds1.name, ds2.name, 
       ST_Distance(ds1.location, ds2.location) as distance
FROM dive_sites ds1, dive_sites ds2 
WHERE ds1.id < ds2.id 
  AND ST_DWithin(ds1.location, ds2.location, 1000)
ORDER BY distance;

Waarom Dit Werkt:

  • max_parallel_workers_per_gather = 8: Staat tot 8 worker processen toe
  • parallel_leader_participation = off: Vermindert coördinatie overhead
  • parallel_tuple_cost = 0.05: Maakt PostgreSQL meer geneigd parallelle plannen te kiezen
  • Complexe ruimtelijke joins worden gesplitst over meerdere CPU cores

Resultaat: Complexe queries verbeterden naar 45ms (55x verbetering van origineel)

Performance Resultaten Samenvatting

Optimalisatie StapQuery TijdVerbetering
Origineel (Geen Indexen)2.500msBaseline
GiST Ruimtelijke Index800ms3x sneller
Partiële Index (Actieve Sites)400ms6x sneller
Covering Index200ms12x sneller
Native Partitioning100ms25x sneller
Parallelle Verwerking45ms55x sneller

Belangrijke Lessen Geleerd

1. Ruimtelijke Data Vereist Speciale Indexen

  • Gewone B-tree indexen werken niet voor geografische data
  • GiST indexen zijn essentieel voor ruimtelijke operaties
  • Overweeg de specifieke ruimtelijke operaties die je nodig hebt

2. Partiële Indexen Zijn Krachtig

  • Indexeer alleen data die je daadwerkelijk queryt
  • Dramatisch kleinere index grootte en betere performance
  • Perfect voor gefilterde datasets

3. Covering Indexen Elimineren Tabel Lookups

  • Voeg veelgebruikte kolommen toe aan indexen
  • Vermindert I/O operaties significant
  • Vooral waardevol voor read-heavy workloads

4. Partitioning Oplost Tijd-Gebaseerde Queries

  • Native partitioning biedt automatische partition pruning
  • Essentieel voor time-series ruimtelijke data
  • Schaalt goed naarmate data groeit

5. Parallelle Verwerking Schaalt Complexe Operaties

  • Moderne PostgreSQL versies excelleren in parallelle uitvoering
  • Configureer instellingen voor je hardware
  • Monitor CPU benutting om effectiviteit te verifiëren

Implementatie Checklist

Als je vergelijkbare ruimtelijke query performance problemen hebt:

  • Analyseer je queries: Gebruik EXPLAIN ANALYZE om bottlenecks te identificeren
  • Maak GiST indexen: Voor alle geografische kolommen
  • Voeg partiële indexen toe: Voor veelgebruikte gefilterde data
  • Overweeg covering indexen: Voor queries die extra kolommen nodig hebben
  • Implementeer partitioning: Voor tijd-gebaseerde ruimtelijke data
  • Configureer parallelle verwerking: Voor complexe ruimtelijke operaties
  • Monitor performance: Volg query tijden en resource gebruik

Samenvatting

Het optimaliseren van ruimtelijke queries in PostgreSQL vereist een multi-layer aanpak. Door GiST indexen, partiële indexing, covering indexen, native partitioning en parallelle verwerking te combineren, behaalden we een 55x performance verbetering voor Duikersgids.nl.

De sleutel was het begrijpen dat ruimtelijke data unieke vereisten heeft en gespecialiseerde optimalisatie technieken vereist. Generieke database optimalisatie aanpakken werken niet voor geografische queries.

Als dit artikel je hielp ruimtelijke query optimalisatie te begrijpen, kunnen we je helpen deze technieken te implementeren in je eigen applicaties. Bij Ludulicious specialiseren we ons in:

  • Ruimtelijke Data Oplossingen: Geografische queries en locatie-gebaseerde applicaties
  • Database Performance Optimalisatie: Van langzame queries tot indexing strategieën
  • Custom Development: Op maat gemaakte oplossingen voor je specifieke use case

Klaar om je ruimtelijke queries te optimaliseren?

Neem contact op voor een gratis consultatie, of bekijk onze andere optimalisatie gidsen:


Deze optimalisatie case study is gebaseerd op echte productie ervaring met Duikersgids.nl. Alle performance nummers komen van echte productie systemen.