Problem Statement
Three access methods can index a PostGIS geometry column, and the documentation describes each one accurately without saying which to use. This page turns the descriptions into a decision: what the three structures actually do differently, which properties of your data make one of them clearly right, and how to measure the choice rather than argue about it. It is the comparison behind choosing a spatial index type.
Why the Naive Approach Fails
The default advice — “use GiST” — is right most of the time and wrong in two situations that both come up in production: a billion-row append-only sensor table where the GiST index is larger than the machine’s memory, and a uniformly distributed point table where SP-GiST’s non-overlapping partitions genuinely index better. Applying the default without checking those cases costs either an unaffordable index or a slower one.
The opposite failure is worse. Reading that BRIN is “1000× smaller” and applying it to a table with scattered writes produces an index the planner trusts and that prunes nothing.
Production-Ready Implementation
Build all three and measure. On a real table this takes an afternoon and settles the question permanently:
-- 10 million sensor points, append-ordered by acquisition time
CREATE INDEX CONCURRENTLY obs_geom_gist ON observations USING gist (geom);
CREATE INDEX CONCURRENTLY obs_geom_spgist ON observations USING spgist (geom);
CREATE INDEX CONCURRENTLY obs_geom_brin ON observations USING brin (geom)
WITH (pages_per_range = 64);
SELECT indexrelid::regclass AS index,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_index
WHERE indrelid = 'observations'::regclass
ORDER BY pg_relation_size(indexrelid) DESC;
-- index | size
-- ----------------------+---------
-- obs_geom_gist | 683 MB
-- obs_geom_spgist | 521 MB
-- obs_geom_brin | 2544 kBThen force each in turn and compare the plans. Disabling the others is the reliable way to make the planner use the one you want to measure:
BEGIN;
SET LOCAL enable_seqscan = off;
DROP INDEX obs_geom_spgist, obs_geom_brin; -- inside a rolled-back transaction
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM observations
WHERE geom && ST_MakeEnvelope(-122.5, 47.4, -122.2, 47.8, 4326);
ROLLBACK;Dropping indexes inside a transaction that you roll back is a safe way to isolate one — the drops never commit, and the plan you measured is the one that index produces.
Configuration and Tuning Knobs
GiST fillfactor defaults to 90 and is worth lowering to 70 on a table with heavy updates: leaving space in leaf pages reduces page splits and therefore bloat, at the cost of a larger index from the start.
SP-GiST works on points, boxes and polygons through different operator classes, but its polygon support is newer and less exercised than GiST’s. For anything other than points, treat SP-GiST as the option to benchmark rather than the one to assume.
BRIN pages_per_range is the whole tuning surface: smaller means more, tighter summaries and sharper pruning; larger means a smaller index and coarser boxes. Match it to the size of one spatially compact insert burst.
autosummarize = on on a BRIN index makes newly appended ranges summarised automatically instead of waiting for a vacuum, which matters on a high-velocity ingest where the newest rows are the ones people query.
Verification Steps
-- which index is the planner actually choosing?
SELECT indexrelname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
WHERE relname = 'observations'
ORDER BY idx_scan DESC;
-- and for BRIN specifically, is the correlation still there?
SELECT attname, correlation
FROM pg_stats
WHERE tablename = 'observations' AND attname = 'recorded_at';A BRIN index on a table whose correlation has fallen below about 0.9 is no longer pruning meaningfully. That number moves as the table ages, which is why it belongs in monitoring rather than in a one-off assessment.
Three data shapes, three clear answers
Most tables do not need this analysis because their shape settles the question outright.
Mixed polygons queried by viewport — GiST. Parcels, buildings, administrative areas, anything with varied extents queried by a map. Overlapping bounding boxes are inherent to the data, which is precisely what an R-tree handles, and the exact-predicate recheck needs the heap pointer GiST leaf entries carry.
Uniform points with no nearest-neighbour requirement — benchmark SP-GiST. Grid samples, regularly spaced observations, synthetic data. The non-overlapping quadtree partitions produce a smaller structure and slightly faster descents. Check first that no query uses the distance operator, because losing ordered nearest-neighbour scans is a large price for a small gain.
Append-only, spatially ordered, enormous — BRIN. Radar sweeps, vehicle tracks, tiled imports written in tile order. When the physical order already encodes location, the summaries are tight and the index is three orders of magnitude smaller than the alternative. Verify the correlation rather than assuming it, and re-verify after any bulk backfill.
Everything outside those three shapes is a GiST table until measurement says otherwise.
Gotchas Checklist
- Only GiST supports the
<->distance operator for ordered nearest-neighbour scans. Choosing SP-GiST on a table that serves KNN queries silently removes that capability. - BRIN indexes are cheap to create and cheap to be wrong about. Because they build in seconds, people add them speculatively; because they prune nothing on uncorrelated data, they then make plans worse. Always check
correlationfirst. - An unused index still costs writes. Benchmarking three indexes is fine; leaving all three in place afterwards is not — drop the losers.
pages_per_rangecannot be changed in place. Altering it requires rebuilding the index, so pick deliberately or plan for a rebuild.- SP-GiST does not support
INCLUDE. If a covering index is part of the plan, GiST is the only option.
Changing your mind later
None of these choices is permanent. Because all three access methods can coexist on the same column, migrating from one to another is a matter of building the replacement concurrently, confirming from pg_stat_user_indexes that the planner has moved to it, and dropping the old one. The whole sequence is online, reversible at every step, and takes an afternoon. That is worth knowing before the decision, because it lowers the stakes considerably: pick the one the evidence supports today, measure it in production, and change it if the data shape changes.
Related Topics
- Choosing a Spatial Index Type — parent topic: the fuller decision context
- BRIN Indexes for Append-Only Spatial Tables — the case where BRIN wins outright
- Clustering a Table on Its Spatial Index — creating the correlation BRIN depends on
- Advanced GiST Indexing & Optimization — the parent section on index architecture