[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-post-en-\u002Fblog\u002Fduikersgids-spatial-search-optimization-\u002Fen\u002Fblog\u002Fduikersgids-spatial-search-optimization":3,"blog-post-surround-en-\u002Fblog\u002Fduikersgids-spatial-search-optimization-\u002Fen\u002Fblog\u002Fduikersgids-spatial-search-optimization":1067,"related-posts-en-\u002Fblog\u002Fduikersgids-spatial-search-optimization-\u002Fen\u002Fblog\u002Fduikersgids-spatial-search-optimization":1074},{"id":4,"title":5,"authors":6,"badge":13,"body":15,"categories":1048,"date":1050,"description":1051,"extension":1052,"image":1053,"meta":1055,"navigation":152,"path":1056,"readingTime":468,"seo":1057,"stem":1058,"tags":1059,"__hash__":1066},"posts_en\u002Fblog\u002F5.duikersgids-spatial-search-optimization.md","Duikersgids: How I Made Spatial Search 55x Faster",[7],{"name":8,"to":9,"avatar":10,"bio":12},"Rob Schoenaker","https:\u002F\u002Flinkedin.com\u002Fin\u002Frobschoenaker",{"src":11},"\u002Fimages\u002Fteam\u002Frob.jpg","Managing Partner at UpstreamAds and Partner at Ludulicious B.V. with over 20 years of experience in software development, specializing in .NET Core, ServiceStack, C# and database design.",{"label":14},"Spatial Data",{"type":16,"value":17,"toc":1021},"minimark",[18,23,27,33,57,62,115,126,130,133,164,169,183,187,192,195,215,220,249,255,259,262,287,291,308,314,318,321,346,350,367,372,376,380,383,408,412,415,489,493,510,515,519,523,526,566,570,573,636,640,663,668,672,775,779,783,794,798,809,813,824,828,839,843,854,858,861,936,940,943,946,949,969,974,982,1008,1011,1017],[19,20,22],"h2",{"id":21},"the-problem-spatial-queries-killing-user-experience","The Problem: Spatial Queries Killing User Experience",[24,25,26],"p",{},"In 2019, Duikersgids.nl was struggling with a critical performance issue. Users searching for dive sites near their location were waiting 2.5 seconds for results. For a location-based application, this was unacceptable.",[24,28,29],{},[30,31,32],"strong",{},"The Challenge:",[34,35,36,40,51,54],"ul",{},[37,38,39],"li",{},"50,000+ dive sites with geographic coordinates",[37,41,42,43,47,48],{},"Complex spatial queries using ",[44,45,46],"code",{},"ST_DWithin"," and ",[44,49,50],{},"ST_Distance",[37,52,53],{},"Users expecting instant location-based results",[37,55,56],{},"Server struggling under load",[24,58,59],{},[30,60,61],{},"The Numbers:",[63,64,69],"pre",{"className":65,"code":66,"language":67,"meta":68,"style":68},"language-sql shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","-- This query was taking 2.5+ seconds\nSELECT ds.name, ds.location, ds.difficulty_level\nFROM dive_sites ds\nWHERE ST_DWithin(ds.location, ST_Point(4.5, 52.0), 10000)\n  AND ds.status = 'active'\nORDER BY ST_Distance(ds.location, ST_Point(4.5, 52.0))\nLIMIT 20;\n","sql","",[44,70,71,79,85,91,97,103,109],{"__ignoreMap":68},[72,73,76],"span",{"class":74,"line":75},"line",1,[72,77,78],{},"-- This query was taking 2.5+ seconds\n",[72,80,82],{"class":74,"line":81},2,[72,83,84],{},"SELECT ds.name, ds.location, ds.difficulty_level\n",[72,86,88],{"class":74,"line":87},3,[72,89,90],{},"FROM dive_sites ds\n",[72,92,94],{"class":74,"line":93},4,[72,95,96],{},"WHERE ST_DWithin(ds.location, ST_Point(4.5, 52.0), 10000)\n",[72,98,100],{"class":74,"line":99},5,[72,101,102],{},"  AND ds.status = 'active'\n",[72,104,106],{"class":74,"line":105},6,[72,107,108],{},"ORDER BY ST_Distance(ds.location, ST_Point(4.5, 52.0))\n",[72,110,112],{"class":74,"line":111},7,[72,113,114],{},"LIMIT 20;\n",[24,116,117],{},[118,119],"img",{"alt":120,"className":121,"height":123,"src":124,"width":125},"Duikersgids performance monitoring",[122],"rounded-lg",600,"https:\u002F\u002Fpicsum.photos\u002Fid\u002F7\u002F1000\u002F600",1000,[19,127,129],{"id":128},"the-root-cause-missing-spatial-indexes","The Root Cause: Missing Spatial Indexes",[24,131,132],{},"The problem was clear from the execution plan:",[63,134,136],{"className":65,"code":135,"language":67,"meta":68,"style":68},"EXPLAIN ANALYZE SELECT * FROM dive_sites \nWHERE ST_DWithin(location, ST_Point(4.5, 52.0), 10000);\n\n-- Result: Seq Scan on dive_sites (cost=0.00..1250.00 rows=5000 width=64)\n-- Execution time: 2500.123 ms\n",[44,137,138,143,148,154,159],{"__ignoreMap":68},[72,139,140],{"class":74,"line":75},[72,141,142],{},"EXPLAIN ANALYZE SELECT * FROM dive_sites \n",[72,144,145],{"class":74,"line":81},[72,146,147],{},"WHERE ST_DWithin(location, ST_Point(4.5, 52.0), 10000);\n",[72,149,150],{"class":74,"line":87},[72,151,153],{"emptyLinePlaceholder":152},true,"\n",[72,155,156],{"class":74,"line":93},[72,157,158],{},"-- Result: Seq Scan on dive_sites (cost=0.00..1250.00 rows=5000 width=64)\n",[72,160,161],{"class":74,"line":99},[72,162,163],{},"-- Execution time: 2500.123 ms\n",[24,165,166],{},[30,167,168],{},"What was happening:",[34,170,171,174,177,180],{},[37,172,173],{},"PostgreSQL was doing a full table scan on 50,000+ records",[37,175,176],{},"No spatial indexes existed for geographic queries",[37,178,179],{},"Every spatial operation was calculated from scratch",[37,181,182],{},"No optimization for common query patterns",[19,184,186],{"id":185},"the-solution-strategic-spatial-indexing","The Solution: Strategic Spatial Indexing",[188,189,191],"h3",{"id":190},"step-1-create-gist-spatial-index","Step 1: Create GiST Spatial Index",[24,193,194],{},"The first breakthrough came with a proper spatial index:",[63,196,198],{"className":65,"code":197,"language":67,"meta":68,"style":68},"-- The GiST index that changed everything\nCREATE INDEX CONCURRENTLY idx_dive_sites_location \nON dive_sites USING GIST (location);\n",[44,199,200,205,210],{"__ignoreMap":68},[72,201,202],{"class":74,"line":75},[72,203,204],{},"-- The GiST index that changed everything\n",[72,206,207],{"class":74,"line":81},[72,208,209],{},"CREATE INDEX CONCURRENTLY idx_dive_sites_location \n",[72,211,212],{"class":74,"line":87},[72,213,214],{},"ON dive_sites USING GIST (location);\n",[24,216,217],{},[30,218,219],{},"Why This Works:",[34,221,222,228,240,243],{},[37,223,224,227],{},[44,225,226],{},"GIST (location)",": GiST indexes are specifically optimized for geometric data types",[37,229,230,231,233,234,236,237],{},"Enables fast spatial operations like ",[44,232,46],{},", ",[44,235,50],{},", and ",[44,238,239],{},"ST_Contains",[37,241,242],{},"Uses R-tree structure internally for efficient spatial range queries",[37,244,245,248],{},[44,246,247],{},"CONCURRENTLY"," allows index creation without blocking writes",[24,250,251,254],{},[30,252,253],{},"Immediate Result:"," Query time dropped from 2.5 seconds to 800ms (3x improvement)",[188,256,258],{"id":257},"step-2-add-partial-index-for-active-sites","Step 2: Add Partial Index for Active Sites",[24,260,261],{},"Most queries only needed active dive sites, so we created a partial index:",[63,263,265],{"className":65,"code":264,"language":67,"meta":68,"style":68},"-- Partial index for active sites (huge win!)\nCREATE INDEX CONCURRENTLY idx_dive_sites_active \nON dive_sites (created_at) \nWHERE status = 'active';\n",[44,266,267,272,277,282],{"__ignoreMap":68},[72,268,269],{"class":74,"line":75},[72,270,271],{},"-- Partial index for active sites (huge win!)\n",[72,273,274],{"class":74,"line":81},[72,275,276],{},"CREATE INDEX CONCURRENTLY idx_dive_sites_active \n",[72,278,279],{"class":74,"line":87},[72,280,281],{},"ON dive_sites (created_at) \n",[72,283,284],{"class":74,"line":93},[72,285,286],{},"WHERE status = 'active';\n",[24,288,289],{},[30,290,219],{},[34,292,293,299,302,305],{},[37,294,295,298],{},[44,296,297],{},"WHERE status = 'active'",": Only indexes rows matching the condition",[37,300,301],{},"Dramatically reduces index size (only ~40,000 active sites vs 50,000+ total)",[37,303,304],{},"Faster index scans and better cache utilization",[37,306,307],{},"Most queries filter by active status anyway",[24,309,310,313],{},[30,311,312],{},"Result:"," Query time improved to 400ms (6x improvement from original)",[188,315,317],{"id":316},"step-3-create-covering-index-for-common-queries","Step 3: Create Covering Index for Common Queries",[24,319,320],{},"Our most common query pattern needed location, dive type, and difficulty level:",[63,322,324],{"className":65,"code":323,"language":67,"meta":68,"style":68},"-- Composite index for our most common query pattern\nCREATE INDEX CONCURRENTLY idx_dive_sites_location_type \nON dive_sites USING GIST (location) \nINCLUDE (dive_type, difficulty_level);\n",[44,325,326,331,336,341],{"__ignoreMap":68},[72,327,328],{"class":74,"line":75},[72,329,330],{},"-- Composite index for our most common query pattern\n",[72,332,333],{"class":74,"line":81},[72,334,335],{},"CREATE INDEX CONCURRENTLY idx_dive_sites_location_type \n",[72,337,338],{"class":74,"line":87},[72,339,340],{},"ON dive_sites USING GIST (location) \n",[72,342,343],{"class":74,"line":93},[72,344,345],{},"INCLUDE (dive_type, difficulty_level);\n",[24,347,348],{},[30,349,219],{},[34,351,352,358,361,364],{},[37,353,354,357],{},[44,355,356],{},"INCLUDE (dive_type, difficulty_level)",": Covering index includes additional columns",[37,359,360],{},"Eliminates table lookups - all needed data comes from the index",[37,362,363],{},"GiST index handles spatial operations, included columns provide additional data",[37,365,366],{},"Perfect for queries that need location + metadata",[24,368,369,371],{},[30,370,312],{}," Query time dropped to 200ms (12x improvement from original)",[19,373,375],{"id":374},"the-game-changer-postgresql-partitioning","The Game Changer: PostgreSQL Partitioning",[188,377,379],{"id":378},"the-problem-time-based-queries-still-slow","The Problem: Time-Based Queries Still Slow",[24,381,382],{},"Even with spatial indexes, queries filtering by date were still slow:",[63,384,386],{"className":65,"code":385,"language":67,"meta":68,"style":68},"-- This query was still scanning everything\nSELECT * FROM dive_sites \nWHERE ST_DWithin(location, ST_Point(4.5, 52.0), 10000)\n  AND created_at > '2023-01-01';  -- Still scanned all partitions!\n",[44,387,388,393,398,403],{"__ignoreMap":68},[72,389,390],{"class":74,"line":75},[72,391,392],{},"-- This query was still scanning everything\n",[72,394,395],{"class":74,"line":81},[72,396,397],{},"SELECT * FROM dive_sites \n",[72,399,400],{"class":74,"line":87},[72,401,402],{},"WHERE ST_DWithin(location, ST_Point(4.5, 52.0), 10000)\n",[72,404,405],{"class":74,"line":93},[72,406,407],{},"  AND created_at > '2023-01-01';  -- Still scanned all partitions!\n",[188,409,411],{"id":410},"the-solution-native-partitioning","The Solution: Native Partitioning",[24,413,414],{},"PostgreSQL 10+ native partitioning solved this:",[63,416,418],{"className":65,"code":417,"language":67,"meta":68,"style":68},"-- PostgreSQL 10+: Native partitioning\nCREATE TABLE dive_sites_partitioned (\n    id SERIAL,\n    name VARCHAR(255),\n    location GEOGRAPHY(POINT, 4326),\n    created_at TIMESTAMP WITH TIME ZONE\n) PARTITION BY RANGE (created_at);\n\nCREATE TABLE dive_sites_2023 PARTITION OF dive_sites_partitioned\nFOR VALUES FROM ('2023-01-01') TO ('2024-01-01');\n\nCREATE TABLE dive_sites_2024 PARTITION OF dive_sites_partitioned\nFOR VALUES FROM ('2024-01-01') TO ('2025-01-01');\n",[44,419,420,425,430,435,440,445,450,455,460,466,472,477,483],{"__ignoreMap":68},[72,421,422],{"class":74,"line":75},[72,423,424],{},"-- PostgreSQL 10+: Native partitioning\n",[72,426,427],{"class":74,"line":81},[72,428,429],{},"CREATE TABLE dive_sites_partitioned (\n",[72,431,432],{"class":74,"line":87},[72,433,434],{},"    id SERIAL,\n",[72,436,437],{"class":74,"line":93},[72,438,439],{},"    name VARCHAR(255),\n",[72,441,442],{"class":74,"line":99},[72,443,444],{},"    location GEOGRAPHY(POINT, 4326),\n",[72,446,447],{"class":74,"line":105},[72,448,449],{},"    created_at TIMESTAMP WITH TIME ZONE\n",[72,451,452],{"class":74,"line":111},[72,453,454],{},") PARTITION BY RANGE (created_at);\n",[72,456,458],{"class":74,"line":457},8,[72,459,153],{"emptyLinePlaceholder":152},[72,461,463],{"class":74,"line":462},9,[72,464,465],{},"CREATE TABLE dive_sites_2023 PARTITION OF dive_sites_partitioned\n",[72,467,469],{"class":74,"line":468},10,[72,470,471],{},"FOR VALUES FROM ('2023-01-01') TO ('2024-01-01');\n",[72,473,475],{"class":74,"line":474},11,[72,476,153],{"emptyLinePlaceholder":152},[72,478,480],{"class":74,"line":479},12,[72,481,482],{},"CREATE TABLE dive_sites_2024 PARTITION OF dive_sites_partitioned\n",[72,484,486],{"class":74,"line":485},13,[72,487,488],{},"FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');\n",[24,490,491],{},[30,492,219],{},[34,494,495,501,504,507],{},[37,496,497,500],{},[44,498,499],{},"PARTITION BY RANGE (created_at)",": Splits table by date ranges",[37,502,503],{},"Each partition is a separate physical table",[37,505,506],{},"PostgreSQL automatically determines which partitions to scan",[37,508,509],{},"Queries with date filters only scan relevant partitions",[24,511,512,514],{},[30,513,312],{}," Time-filtered queries dropped to 100ms (25x improvement)",[19,516,518],{"id":517},"the-final-optimization-parallel-processing","The Final Optimization: Parallel Processing",[188,520,522],{"id":521},"the-problem-complex-spatial-joins-cpu-bound","The Problem: Complex Spatial Joins CPU-Bound",[24,524,525],{},"Complex queries finding nearby dive sites were using only one CPU core:",[63,527,529],{"className":65,"code":528,"language":67,"meta":68,"style":68},"-- This query was using only one CPU core\nSELECT ds1.name, ds2.name, \n       ST_Distance(ds1.location, ds2.location) as distance\nFROM dive_sites ds1, dive_sites ds2 \nWHERE ds1.id \u003C ds2.id \n  AND ST_DWithin(ds1.location, ds2.location, 1000)\nORDER BY distance;\n",[44,530,531,536,541,546,551,556,561],{"__ignoreMap":68},[72,532,533],{"class":74,"line":75},[72,534,535],{},"-- This query was using only one CPU core\n",[72,537,538],{"class":74,"line":81},[72,539,540],{},"SELECT ds1.name, ds2.name, \n",[72,542,543],{"class":74,"line":87},[72,544,545],{},"       ST_Distance(ds1.location, ds2.location) as distance\n",[72,547,548],{"class":74,"line":93},[72,549,550],{},"FROM dive_sites ds1, dive_sites ds2 \n",[72,552,553],{"class":74,"line":99},[72,554,555],{},"WHERE ds1.id \u003C ds2.id \n",[72,557,558],{"class":74,"line":105},[72,559,560],{},"  AND ST_DWithin(ds1.location, ds2.location, 1000)\n",[72,562,563],{"class":74,"line":111},[72,564,565],{},"ORDER BY distance;\n",[188,567,569],{"id":568},"the-solution-enhanced-parallel-processing","The Solution: Enhanced Parallel Processing",[24,571,572],{},"PostgreSQL 16+ parallel processing configuration:",[63,574,576],{"className":65,"code":575,"language":67,"meta":68,"style":68},"-- PostgreSQL 16+: Better parallel execution\nSET max_parallel_workers_per_gather = 8;\nSET parallel_leader_participation = off;\nSET parallel_tuple_cost = 0.05;  -- Lower cost threshold\n\n-- Same query now uses all CPU cores\nEXPLAIN (ANALYZE, BUFFERS, VERBOSE)\nSELECT ds1.name, ds2.name, \n       ST_Distance(ds1.location, ds2.location) as distance\nFROM dive_sites ds1, dive_sites ds2 \nWHERE ds1.id \u003C ds2.id \n  AND ST_DWithin(ds1.location, ds2.location, 1000)\nORDER BY distance;\n",[44,577,578,583,588,593,598,602,607,612,616,620,624,628,632],{"__ignoreMap":68},[72,579,580],{"class":74,"line":75},[72,581,582],{},"-- PostgreSQL 16+: Better parallel execution\n",[72,584,585],{"class":74,"line":81},[72,586,587],{},"SET max_parallel_workers_per_gather = 8;\n",[72,589,590],{"class":74,"line":87},[72,591,592],{},"SET parallel_leader_participation = off;\n",[72,594,595],{"class":74,"line":93},[72,596,597],{},"SET parallel_tuple_cost = 0.05;  -- Lower cost threshold\n",[72,599,600],{"class":74,"line":99},[72,601,153],{"emptyLinePlaceholder":152},[72,603,604],{"class":74,"line":105},[72,605,606],{},"-- Same query now uses all CPU cores\n",[72,608,609],{"class":74,"line":111},[72,610,611],{},"EXPLAIN (ANALYZE, BUFFERS, VERBOSE)\n",[72,613,614],{"class":74,"line":457},[72,615,540],{},[72,617,618],{"class":74,"line":462},[72,619,545],{},[72,621,622],{"class":74,"line":468},[72,623,550],{},[72,625,626],{"class":74,"line":474},[72,627,555],{},[72,629,630],{"class":74,"line":479},[72,631,560],{},[72,633,634],{"class":74,"line":485},[72,635,565],{},[24,637,638],{},[30,639,219],{},[34,641,642,648,654,660],{},[37,643,644,647],{},[44,645,646],{},"max_parallel_workers_per_gather = 8",": Allows up to 8 worker processes",[37,649,650,653],{},[44,651,652],{},"parallel_leader_participation = off",": Reduces coordination overhead",[37,655,656,659],{},[44,657,658],{},"parallel_tuple_cost = 0.05",": Makes PostgreSQL more likely to choose parallel plans",[37,661,662],{},"Complex spatial joins split across multiple CPU cores",[24,664,665,667],{},[30,666,312],{}," Complex queries improved to 45ms (55x improvement from original)",[19,669,671],{"id":670},"performance-results-summary","Performance Results Summary",[673,674,675,691],"table",{},[676,677,678],"thead",{},[679,680,681,685,688],"tr",{},[682,683,684],"th",{},"Optimization Step",[682,686,687],{},"Query Time",[682,689,690],{},"Improvement",[692,693,694,708,721,734,747,760],"tbody",{},[679,695,696,702,705],{},[697,698,699],"td",{},[30,700,701],{},"Original (No Indexes)",[697,703,704],{},"2,500ms",[697,706,707],{},"Baseline",[679,709,710,715,718],{},[697,711,712],{},[30,713,714],{},"GiST Spatial Index",[697,716,717],{},"800ms",[697,719,720],{},"3x faster",[679,722,723,728,731],{},[697,724,725],{},[30,726,727],{},"Partial Index (Active Sites)",[697,729,730],{},"400ms",[697,732,733],{},"6x faster",[679,735,736,741,744],{},[697,737,738],{},[30,739,740],{},"Covering Index",[697,742,743],{},"200ms",[697,745,746],{},"12x faster",[679,748,749,754,757],{},[697,750,751],{},[30,752,753],{},"Native Partitioning",[697,755,756],{},"100ms",[697,758,759],{},"25x faster",[679,761,762,767,770],{},[697,763,764],{},[30,765,766],{},"Parallel Processing",[697,768,769],{},"45ms",[697,771,772],{},[30,773,774],{},"55x faster",[19,776,778],{"id":777},"key-lessons-learned","Key Lessons Learned",[188,780,782],{"id":781},"_1-spatial-data-requires-special-indexes","1. Spatial Data Requires Special Indexes",[34,784,785,788,791],{},[37,786,787],{},"Regular B-tree indexes don't work for geographic data",[37,789,790],{},"GiST indexes are essential for spatial operations",[37,792,793],{},"Consider the specific spatial operations you need",[188,795,797],{"id":796},"_2-partial-indexes-are-powerful","2. Partial Indexes Are Powerful",[34,799,800,803,806],{},[37,801,802],{},"Only index data you actually query",[37,804,805],{},"Dramatically reduces index size and improves performance",[37,807,808],{},"Perfect for filtered datasets",[188,810,812],{"id":811},"_3-covering-indexes-eliminate-table-lookups","3. Covering Indexes Eliminate Table Lookups",[34,814,815,818,821],{},[37,816,817],{},"Include frequently accessed columns in indexes",[37,819,820],{},"Reduces I\u002FO operations significantly",[37,822,823],{},"Especially valuable for read-heavy workloads",[188,825,827],{"id":826},"_4-partitioning-solves-time-based-queries","4. Partitioning Solves Time-Based Queries",[34,829,830,833,836],{},[37,831,832],{},"Native partitioning provides automatic partition pruning",[37,834,835],{},"Essential for time-series spatial data",[37,837,838],{},"Scales well as data grows",[188,840,842],{"id":841},"_5-parallel-processing-scales-complex-operations","5. Parallel Processing Scales Complex Operations",[34,844,845,848,851],{},[37,846,847],{},"Modern PostgreSQL versions excel at parallel execution",[37,849,850],{},"Configure settings for your hardware",[37,852,853],{},"Monitor CPU utilization to verify effectiveness",[19,855,857],{"id":856},"implementation-checklist","Implementation Checklist",[24,859,860],{},"If you're facing similar spatial query performance issues:",[34,862,865,882,891,900,909,918,927],{"className":863},[864],"contains-task-list",[37,866,869,873,874,877,878,881],{"className":867},[868],"task-list-item",[870,871],"input",{"disabled":152,"type":872},"checkbox"," ",[30,875,876],{},"Analyze your queries",": Use ",[44,879,880],{},"EXPLAIN ANALYZE"," to identify bottlenecks",[37,883,885,873,887,890],{"className":884},[868],[870,886],{"disabled":152,"type":872},[30,888,889],{},"Create GiST indexes",": For all geographic columns",[37,892,894,873,896,899],{"className":893},[868],[870,895],{"disabled":152,"type":872},[30,897,898],{},"Add partial indexes",": For commonly filtered data",[37,901,903,873,905,908],{"className":902},[868],[870,904],{"disabled":152,"type":872},[30,906,907],{},"Consider covering indexes",": For queries needing additional columns",[37,910,912,873,914,917],{"className":911},[868],[870,913],{"disabled":152,"type":872},[30,915,916],{},"Implement partitioning",": For time-based spatial data",[37,919,921,873,923,926],{"className":920},[868],[870,922],{"disabled":152,"type":872},[30,924,925],{},"Configure parallel processing",": For complex spatial operations",[37,928,930,873,932,935],{"className":929},[868],[870,931],{"disabled":152,"type":872},[30,933,934],{},"Monitor performance",": Track query times and resource usage",[19,937,939],{"id":938},"summary","Summary",[24,941,942],{},"Optimizing spatial queries in PostgreSQL requires a multi-layered approach. By combining GiST indexes, partial indexing, covering indexes, native partitioning, and parallel processing, we achieved a 55x performance improvement for Duikersgids.nl.",[24,944,945],{},"The key was understanding that spatial data has unique requirements and requires specialized optimization techniques. Generic database optimization approaches won't work for geographic queries.",[24,947,948],{},"If this article helped you understand spatial query optimization, we can help you implement these techniques in your own applications. At Ludulicious, we specialize in:",[34,950,951,957,963],{},[37,952,953,956],{},[30,954,955],{},"Spatial Data Solutions",": Geographic queries and location-based applications",[37,958,959,962],{},[30,960,961],{},"Database Performance Optimization",": From slow queries to indexing strategies",[37,964,965,968],{},[30,966,967],{},"Custom Development",": Tailored solutions for your specific use case",[24,970,971],{},[30,972,973],{},"Ready to optimize your spatial queries?",[24,975,976,981],{},[977,978,980],"a",{"href":979},"\u002Fcontact","Contact us"," for a free consultation, or check out our other optimization guides:",[34,983,984,990,996,1002],{},[37,985,986],{},[977,987,989],{"href":988},"\u002Fblog\u002Fpostgresql-performance-strategy","PostgreSQL Performance Tuning: Strategic Lessons from Production",[37,991,992],{},[977,993,995],{"href":994},"\u002Fblog\u002Frijmwoordenboek-phonetic-search-optimization","Rijmwoordenboek: Solving the 3-Second Phonetic Search Problem",[37,997,998],{},[977,999,1001],{"href":1000},"\u002Fblog\u002Fupstreamads-fulltext-search-optimization","UpstreamAds: From 1.2s to 35ms Full-Text Search",[37,1003,1004],{},[977,1005,1007],{"href":1006},"\u002Fblog\u002Fpostgresql-configuration-optimization","PostgreSQL Configuration: The Settings That Matter",[1009,1010],"hr",{},[24,1012,1013],{},[1014,1015,1016],"em",{},"This optimization case study is based on real production experience with Duikersgids.nl. All performance numbers are from actual production systems.",[1018,1019,1020],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":68,"searchDepth":81,"depth":81,"links":1022},[1023,1024,1025,1030,1034,1038,1039,1046,1047],{"id":21,"depth":81,"text":22},{"id":128,"depth":81,"text":129},{"id":185,"depth":81,"text":186,"children":1026},[1027,1028,1029],{"id":190,"depth":87,"text":191},{"id":257,"depth":87,"text":258},{"id":316,"depth":87,"text":317},{"id":374,"depth":81,"text":375,"children":1031},[1032,1033],{"id":378,"depth":87,"text":379},{"id":410,"depth":87,"text":411},{"id":517,"depth":81,"text":518,"children":1035},[1036,1037],{"id":521,"depth":87,"text":522},{"id":568,"depth":87,"text":569},{"id":670,"depth":81,"text":671},{"id":777,"depth":81,"text":778,"children":1040},[1041,1042,1043,1044,1045],{"id":781,"depth":87,"text":782},{"id":796,"depth":87,"text":797},{"id":811,"depth":87,"text":812},{"id":826,"depth":87,"text":827},{"id":841,"depth":87,"text":842},{"id":856,"depth":81,"text":857},{"id":938,"depth":81,"text":939},[1049,14],"Database Optimization","2025-01-17","Learn how we optimized PostgreSQL spatial queries for Duikersgids.nl, reducing search times from 2.5 seconds to 45ms using GiST indexes, partitioning, and parallel processing techniques.","md",{"src":1054},"https:\u002F\u002Fpicsum.photos\u002Fid\u002F7\u002F640\u002F360",{},"\u002Fblog\u002Fduikersgids-spatial-search-optimization",{"title":5,"description":1051},"blog\u002F5.duikersgids-spatial-search-optimization",[1060,1061,1062,1063,1064,1065],"PostgreSQL","Spatial Queries","GiST Indexes","Geographic Data","Performance Optimization","Duikersgids","YO8mizcHQG807ojcMuEpTPUBPXfwh6jfQiCE0mJhXKE",[1068,1071],{"title":989,"path":988,"stem":1069,"description":1070,"children":-1},"blog\u002F4.postgresql-performance-strategy","Learn PostgreSQL performance optimization strategies from real production workloads. From version 9.6 to 17, discover the techniques that improved our database performance by 10-55x across multiple applications.",{"title":995,"path":994,"stem":1072,"description":1073,"children":-1},"blog\u002F6.rijmwoordenboek-phonetic-search-optimization","Learn how we optimized PostgreSQL phonetic search for Van Dale Rijmwoordenboek, reducing search times from 3.2 seconds to 85ms using multi-layered indexing strategies and B-tree deduplication.",[]]