ST_DWithin
answers "is b within n units of a" and, unlike ST_Distance, it can reach a
spatial index. that single difference is why it is almost always the right
proximity predicate.
SELECT id, name
FROM stores
WHERE ST_DWithin(geom, :point, 1000);
why not ST_Distance
the obvious version of the same query looks like this:
SELECT id FROM stores WHERE ST_Distance(geom, :point) < 1000;
ST_Distance(...) < 1000 is a comparison on the result of a function call. the
planner has nothing to match against an index, so it computes the distance for
every row in the table.
ST_DWithin is different. internally it expands the search geometry's bounding
box by the distance and asks the index for candidates first, then runs the exact
test on the survivors. the bounding box stage is what
the gist index serves.
on any table that matters, that is the difference between two plans:
-- ST_Distance
Seq Scan on stores (cost=0.00..24500.00 rows=1 width=36)
-- ST_DWithin
Index Scan using stores_geom_idx on stores (cost=0.28..8.55 rows=1 width=36)
Index Cond: (geom && st_expand(:point, 1000))
Filter: st_dwithin(geom, :point, 1000)
the units come from the type, not from you
this is where most wrong results come from, and they are wrong quietly.
| type | unit at srid 4326 | cost | correct over long distances |
|---|---|---|---|
geometry |
degrees | cheap | no |
geography |
meters | 2 to 5 times more | yes |
with geometry(Point, 4326), ST_DWithin(a, b, 1000) means 1000 degrees,
which covers the planet. one degree of latitude is roughly 111 km, but one
degree of longitude shrinks toward the poles, so a radius expressed in degrees
is an ellipse that changes shape depending on where you are.
two correct options:
-- meters, spheroidal, easy to read
WHERE ST_DWithin(geom::geography, :point::geography, 1000)
-- meters, cheaper, valid inside the projection's zone
WHERE ST_DWithin(ST_Transform(geom, 31983), ST_Transform(:point, 31983), 1000)
if you cast on every query, store the cast instead. either make the column
geography, or build an index on the exact expression:
CREATE INDEX stores_geog_idx ON stores USING GIST ((geom::geography));
an index on geom will not serve a query on geom::geography. the expression
has to match.
it filters, it does not sort
for "the nearest ten within a radius", combine the filter with the knn operator, which the same index can also serve:
SELECT id, name, ST_Distance(geom::geography, :point) AS meters
FROM stores
WHERE ST_DWithin(geom::geography, :point, 5000)
ORDER BY geom <-> :point
LIMIT 10;
ordering by ST_Distance(...) works too, but it sorts the whole filtered set
first. <-> walks the index in distance order and stops at ten.
gotchas
- mismatched srids raise an error, they do not reproject. keep the column
and the parameter on the same srid, or call
ST_Transformexplicitly. ST_DWithin(a, b, 0)is notST_Intersects. it goes through the distance machinery anyway. use the predicate you actually mean.geographyassumes a spheroid, so results differ from projected math by a fraction of a percent. that matters for billing, not for finding nearby stores.