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