If you want to avoid a GIS extension, I adapted the functions from this post to postgres sql:
create or replace function change_in_lat(miles numeric)
returns double precision as $$
with v as (select
3960.0 as earth_radius,
180 / pi() as radians_to_degrees
) select ( miles / earth_radius ) * radians_to_degrees from v;
$$ language sql
returns null on null input;
create or replace function change_in_long(lat numeric, miles numeric)
returns double precision as $$
with v as (select
3960.0 as earth_radius,
pi() / 180 as degrees_to_radians,
180 / pi() as radians_to_degrees
) select (
miles / (earth_radius * cos(lat * degrees_to_radians))
) * radians_to_degrees from v;
$$ language sql
returns null on null input;
using those you can do some surrounding-square queries:
--find all "a"s within 25 miles of any "b"
select * from a join b on (
a.gpslat between
b.gpslat - change_in_lat(25) and b.gpslat + change_in_lat(25)
and a.gpslong between
b.gpslong - change_in_long(b.gpslat::numeric, 25)
and b.gpslong + change_in_long(b.gpslat::numeric, 25)
);
if you used it frequently enough I'm sure turning the between statements into a single function would be easy. I never did any actual "within radius" queries with this though.
For anything more complicated, you'll probably want a GIS extension like other answers have said. PostGIS is good, but I found a lot of the gis-specific functions can be hard to get right, and unless you utilize bounding-box indices your spacial queries might take a day if your data set is large enough. But the tradeoff in complexity is definitely worth it for all the fancy things, like outputting your data in geojson format, etc.