12

If I have a query returning nearby cafes:

SELECT * FROM cafes c WHERE (
   ST_DWithin(
   ST_GeographyFromText(
     'SRID=4326;POINT(' || c.longitude || ' ' || c.latitude || ')'
   ),
   ST_GeographyFromText('SRID=4326;POINT(-76.000000 39.000000)'),
     2000
   )
)

How do I select distance, and order by distance as well?
Is there a more efficient way than this one:

 SELECT id, 
 ST_Distance(ST_GeographyFromText('SRID=4326;POINT(-76.000000 39.000000)'),
             ST_GeographyFromText(
             'SRID=4326;POINT(' || c.longitude || ' ' || c.latitude || ')')      
             ) as distance 
 FROM cafes c
   WHERE (
   ST_DWithin(
     ST_GeographyFromText(
     'SRID=4326;POINT(' || c.longitude || ' ' || c.latitude || ')'
   ),
    ST_GeographyFromText('SRID=4326;POINT(-76.000000 39.000000)'),
   2000
 )
 ) order by distance
Erwin Brandstetter
  • 146,376
  • 19
  • 347
  • 493
Gandalf StormCrow
  • 535
  • 1
  • 6
  • 16

1 Answers1

12

First, use

ST_SetSRID(ST_MakePoint(c.longitude, c.latitude),4326)::geography

instead of

ST_GeographyFromText('SRID=4326;POINT(' || c.longitude || ' ' || c.latitude || ')')

The manual:

ST_MakePoint while not being OGC compliant is generally faster and more precise than ST_GeomFromText and ST_PointFromText. It is also easier to use if you have raw coordinates rather than WKT.

Next, to make the query shorter and only enter search parameters once (without much effect on performance), use a subquery (or CTE):

SELECT id
     , ST_Distance(t.x
                 , ST_SetSRID(ST_MakePoint(c.longitude, c.latitude),4326)::geography) AS dist
FROM   cafes c
    , (SELECT ST_GeographyFromText('SRID=4326;POINT(-76.000000 39.000000)')) AS t(x)
WHERE  ST_DWithin(t.x
                , ST_SetSRID(ST_MakePoint(c.longitude, c.latitude),4326)::geography, 2000)
ORDER  BY dist;

Finally, you need a GiST index to make this fast for big tables. The manual on ST_DWithin():

This function call will automatically include a bounding box comparison that will make use of any indexes that are available on the geometries.

You could get this to work with a functional index on the expression at the start of the answer. But I would store a geography type column to begin with (let's name it thegeog) and create a plain GiST index like:

CREATE INDEX cafes_thegeog_gist ON cafes USING gist(thegeog);

Arriving at this much simpler and faster query:

SELECT id, ST_Distance(t.x, thegeog) AS distance 
FROM   cafes c
    , (SELECT ST_GeographyFromText('SRID=4326;POINT(-76.000000 39.000000)')) AS t(x)
WHERE  ST_DWithin(t.x, thegeog, 2000)
ORDER  BY distance;

Updated to match geography with geography, as pointed out by @LR1234567 in the comment. As an alternative, you could work with geometry. All functions used here work for both (except for ST_MakePoint, hence the appended cast). What's the difference? See:

If you want to get the n nearest cafes instead all within a radius, consider a "nearest neighbour" search. Often more convenient.

Erwin Brandstetter
  • 146,376
  • 19
  • 347
  • 493
  • 2
    You shouldn't mix geometry with geography. You should do: ST_SetSRID(ST_MakePoint(c.longitude, c.latitude),4326)::geography - note that ST_MakePoint returns a geometry. – LR1234567 Mar 12 '14 at 20:22
  • @LR1234567: Thanks for pointing that out. I updated the answer accordingly. – Erwin Brandstetter Mar 12 '14 at 20:49
  • +1 + acc, pretty awesome answer I hope that will help many people as well – Gandalf StormCrow Mar 12 '14 at 21:47
  • Hi Erwin I did everything like you wrote, I created a geography column (thegeog), created index and I dumped all the latlongs into it `UPDATE cafes SET thegeog = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326);` Do I still need to use that part where I `select ST_GeographyFromText .... AS t(x)` now that I have this column populated? Can I take advantage of that column and ask nearby that directly instead of specifying lat/long? – Gandalf StormCrow Apr 24 '14 at 19:37
  • @GandalfStormCrow: Yes, as demonstrated in my last query. – Erwin Brandstetter Apr 24 '14 at 21:58
  • @ErwinBrandstetter your last query works well and I'm using it right now but instead of providing coordinates to look for nearby cafes, can I search based on the geography column? Are lat/long columns redundant in my table? Should I delete them all together and just save lat/long in the geography column? Or should I ask a new question for this? Sorry about all the questions – Gandalf StormCrow Apr 24 '14 at 22:03
  • @GandalfStormCrow: Search for *what*? Obviously, you need a point of reference in your search. You could search cafes around a specified cafe ...`lat` / `long` are redundant now. If there's more to it, start a new question. You can always reference this one for context. – Erwin Brandstetter Apr 24 '14 at 22:06
  • I need to join a table and getting; ` There is an entry for table "groups", but it cannot be referenced from this part of the query` Any ideas on why groups isn't available in the join? SELECT groups.id, ST_Distance(t.x, locations.center) AS distance FROM groups, (SELECT ST_GeographyFromText('SRID=4326;POINT(-76.000000 39.000000)')) AS t(x) JOIN locations ON groups.location_id = locations.id WHERE ST_DWithin(t.x, locations.center, 20000) ORDER BY distance; – Underwater_developer Feb 09 '22 at 18:37
  • 1
    @Underwater_developer: This should work: `SELECT groups.id, ST_Distance(t.x, locations.center) AS distance FROM groups JOIN locations ON groups.location_id = locations.id , (SELECT ST_GeographyFromText('SRID=4326;POINT(-76.000000 39.000000)')) AS t(x) WHERE ST_DWithin(t.x, locations.center, 20000) ORDER BY distance;` Because: https://stackoverflow.com/a/34598292/939860 Please ask your questions as ***question***. – Erwin Brandstetter Feb 10 '22 at 03:29
  • @ErwinBrandstetter Thanks for the reply. I wrote up my next question. In the end I managed to avoid the error in my comment by switching the order entities in the FROM statement (which you can see in my new question) https://dba.stackexchange.com/questions/307320/cte-clause-not-able-to-be-used-in-final-order-by-statement – Underwater_developer Feb 10 '22 at 19:02