193

I've got a working PHP script that gets Longitude and Latitude values and then inputs them into a MySQL query. I'd like to make it solely MySQL. Here's my current PHP Code:

if ($distance != "Any" && $customer_zip != "") { //get the great circle distance

    //get the origin zip code info
    $zip_sql = "SELECT * FROM zip_code WHERE zip_code = '$customer_zip'";
    $result = mysql_query($zip_sql);
    $row = mysql_fetch_array($result);
    $origin_lat = $row['lat'];
    $origin_lon = $row['lon'];

    //get the range
    $lat_range = $distance/69.172;
    $lon_range = abs($distance/(cos($details[0]) * 69.172));
    $min_lat = number_format($origin_lat - $lat_range, "4", ".", "");
    $max_lat = number_format($origin_lat + $lat_range, "4", ".", "");
    $min_lon = number_format($origin_lon - $lon_range, "4", ".", "");
    $max_lon = number_format($origin_lon + $lon_range, "4", ".", "");
    $sql .= "lat BETWEEN '$min_lat' AND '$max_lat' AND lon BETWEEN '$min_lon' AND '$max_lon' AND ";
    }

Does anyone know how to make this entirely MySQL? I've browsed the Internet a bit but most of the literature on it is pretty confusing.

Giacomo1968
  • 25,759
  • 11
  • 71
  • 103
Nick Woodhams
  • 11,977
  • 10
  • 50
  • 52
  • 5
    Based on all the excellent answers below, [here is working sample of the Haversine formula in action](http://sqlfiddle.com/#!2/abba1/2/0) – Michael M Jan 19 '14 at 06:29
  • http://stackoverflow.com/a/40272394/1281385 Has an example of how to make sure index are hit – exussum Oct 26 '16 at 21:32

9 Answers9

379

From Google Code FAQ - Creating a Store Locator with PHP, MySQL & Google Maps:

Here's the SQL statement that will find the closest 20 locations that are within a radius of 25 miles to the 37, -122 coordinate. It calculates the distance based on the latitude/longitude of that row and the target latitude/longitude, and then asks for only rows where the distance value is less than 25, orders the whole query by distance, and limits it to 20 results. To search by kilometers instead of miles, replace 3959 with 6371.

SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) 
* cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin(radians(lat)) ) ) AS distance 
FROM markers 
HAVING distance < 25 
ORDER BY distance 
LIMIT 0 , 20;
Pavel Chuchuva
  • 22,633
  • 10
  • 99
  • 115
  • 2
    the sql statement is really good. but where can i pass my coordinates into this statement? i cant see anywhere coordinates have passed – Mann Aug 13 '11 at 14:43
  • 38
    Replace 37 and -122 with your coordinates. – Pavel Chuchuva Aug 14 '11 at 05:32
  • 5
    I wonder about the performance implications of this if there are millions of places (+thousands of visitors)... – Halil Özgür Dec 01 '11 at 10:26
  • caching is always your friend in that circumstance – ricosrealm Feb 21 '12 at 22:13
  • 13
    You can narrow the query for better performance as explained in this doc: http://tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL – maliayas Nov 15 '12 at 01:25
  • are "lat" and "lng" fields in database? – FosAvance Mar 29 '14 at 16:49
  • 2
    @FosAvance Yes, this query would work if you have `markers` table with id, lan and lng fields. – Pavel Chuchuva Mar 29 '14 at 23:08
  • 2
    speaking of performance, could someone tell me if the Google's version is better than this one: __((ACOS(SIN(51.5073509 * PI() / 180) * SIN(`lat` * PI() / 180) + COS(51.5073509 * PI() / 180) * COS(`lat` * PI() / 180) * COS((-0.1277583 - `long`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515)__ – Eugen Zaharia Mar 20 '15 at 09:20
  • 1
    @EugenZaharia The formula that you are referring to is the Spherical law of cosines and is almost identical to the Haversine formula. The spherical law of cosines formula returns the exact same result as the Haversine formula for distances larger than a few meters, and is twice as fast. – Kevin Furmanski Apr 10 '15 at 14:56
  • I wants to add sql LIKE query in this.? – Rahul Bhargava Oct 27 '15 at 09:00
  • This formula is buggy. I used a very similar one (in kilometers) and if a point is exactly at the center of the circle (radius) this point will not be selected. I am struggling to solve this bug, check this -> http://stackoverflow.com/questions/42522005/select-points-from-map-database-according-to-radius – Samul Mar 01 '17 at 03:34
  • 2
    There's now an error on the original google page. The code snippet still has 37 and -122 as lat / lng in the query, but the text now says "Here's the SQL statement that finds the closest 20 locations within a radius of 25 miles to the -33, 151 coordinate" - this makes it pretty confusing! – stef Jun 07 '17 at 12:48
  • how can we use a variable instead of static(HAVING distance < 25)???? Samul Pavel Chuchuva Yahel – Jayvirsinh Vaghela Jun 25 '18 at 07:10
  • How to convert **distance** to **KM** after this query? – Iman Marashi Jul 20 '18 at 13:08
  • 2
    @ImanMarashi To search by kilometres instead of miles, replace 3959 with 6371. – Pavel Chuchuva Jul 21 '18 at 21:36
  • Without a "bounding box", this query will scan the entire table, hence be slow for large tables. – Rick James May 05 '19 at 23:51
  • Be aware - If the distance is 0 it will be returned as null and be omitted from the results – Ben Southall Apr 12 '20 at 22:57
36

$greatCircleDistance = acos( cos($latitude0) * cos($latitude1) * cos($longitude0 - $longitude1) + sin($latitude0) * sin($latitude1));

with latitude and longitude in radian.

so

SELECT 
  acos( 
      cos(radians( $latitude0 ))
    * cos(radians( $latitude1 ))
    * cos(radians( $longitude0 ) - radians( $longitude1 ))
    + sin(radians( $latitude0 )) 
    * sin(radians( $latitude1 ))
  ) AS greatCircleDistance 
 FROM yourTable;

is your SQL query

to get your results in Km or miles, multiply the result with the mean radius of Earth (3959 miles,6371 Km or 3440 nautical miles)

The thing you are calculating in your example is a bounding box. If you put your coordinate data in a spatial enabled MySQL column, you can use MySQL's build in functionality to query the data.

SELECT 
  id
FROM spatialEnabledTable
WHERE 
  MBRWithin(ogc_point, GeomFromText('Polygon((0 0,0 3,3 3,3 0,0 0))'))
Jacco
  • 23,534
  • 17
  • 88
  • 105
14

If you add helper fields to the coordinates table, you can improve response time of the query.

Like this:

CREATE TABLE `Coordinates` (
`id` INT(10) UNSIGNED NOT NULL COMMENT 'id for the object',
`type` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT 'type',
`sin_lat` FLOAT NOT NULL COMMENT 'sin(lat) in radians',
`cos_cos` FLOAT NOT NULL COMMENT 'cos(lat)*cos(lon) in radians',
`cos_sin` FLOAT NOT NULL COMMENT 'cos(lat)*sin(lon) in radians',
`lat` FLOAT NOT NULL COMMENT 'latitude in degrees',
`lon` FLOAT NOT NULL COMMENT 'longitude in degrees',
INDEX `lat_lon_idx` (`lat`, `lon`)
)    

If you're using TokuDB, you'll get even better performance if you add clustering indexes on either of the predicates, for example, like this:

alter table Coordinates add clustering index c_lat(lat);
alter table Coordinates add clustering index c_lon(lon);

You'll need the basic lat and lon in degrees as well as sin(lat) in radians, cos(lat)*cos(lon) in radians and cos(lat)*sin(lon) in radians for each point. Then you create a mysql function, smth like this:

CREATE FUNCTION `geodistance`(`sin_lat1` FLOAT,
                              `cos_cos1` FLOAT, `cos_sin1` FLOAT,
                              `sin_lat2` FLOAT,
                              `cos_cos2` FLOAT, `cos_sin2` FLOAT)
    RETURNS float
    LANGUAGE SQL
    DETERMINISTIC
    CONTAINS SQL
    SQL SECURITY INVOKER
   BEGIN
   RETURN acos(sin_lat1*sin_lat2 + cos_cos1*cos_cos2 + cos_sin1*cos_sin2);
   END

This gives you the distance.

Don't forget to add an index on lat/lon so the bounding boxing can help the search instead of slowing it down (the index is already added in the CREATE TABLE query above).

INDEX `lat_lon_idx` (`lat`, `lon`)

Given an old table with only lat/lon coordinates, you can set up a script to update it like this: (php using meekrodb)

$users = DB::query('SELECT id,lat,lon FROM Old_Coordinates');

foreach ($users as $user)
{
  $lat_rad = deg2rad($user['lat']);
  $lon_rad = deg2rad($user['lon']);

  DB::replace('Coordinates', array(
    'object_id' => $user['id'],
    'object_type' => 0,
    'sin_lat' => sin($lat_rad),
    'cos_cos' => cos($lat_rad)*cos($lon_rad),
    'cos_sin' => cos($lat_rad)*sin($lon_rad),
    'lat' => $user['lat'],
    'lon' => $user['lon']
  ));
}

Then you optimize the actual query to only do the distance calculation when really needed, for example by bounding the circle (well, oval) from inside and outside. For that, you'll need to precalculate several metrics for the query itself:

// assuming the search center coordinates are $lat and $lon in degrees
// and radius in km is given in $distance
$lat_rad = deg2rad($lat);
$lon_rad = deg2rad($lon);
$R = 6371; // earth's radius, km
$distance_rad = $distance/$R;
$distance_rad_plus = $distance_rad * 1.06; // ovality error for outer bounding box
$dist_deg_lat = rad2deg($distance_rad_plus); //outer bounding box
$dist_deg_lon = rad2deg($distance_rad_plus/cos(deg2rad($lat)));
$dist_deg_lat_small = rad2deg($distance_rad/sqrt(2)); //inner bounding box
$dist_deg_lon_small = rad2deg($distance_rad/cos(deg2rad($lat))/sqrt(2));

Given those preparations, the query goes something like this (php):

$neighbors = DB::query("SELECT id, type, lat, lon,
       geodistance(sin_lat,cos_cos,cos_sin,%d,%d,%d) as distance
       FROM Coordinates WHERE
       lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d
       HAVING (lat BETWEEN %d AND %d AND lon BETWEEN %d AND %d) OR distance <= %d",
  // center radian values: sin_lat, cos_cos, cos_sin
       sin($lat_rad),cos($lat_rad)*cos($lon_rad),cos($lat_rad)*sin($lon_rad),
  // min_lat, max_lat, min_lon, max_lon for the outside box
       $lat-$dist_deg_lat,$lat+$dist_deg_lat,
       $lon-$dist_deg_lon,$lon+$dist_deg_lon,
  // min_lat, max_lat, min_lon, max_lon for the inside box
       $lat-$dist_deg_lat_small,$lat+$dist_deg_lat_small,
       $lon-$dist_deg_lon_small,$lon+$dist_deg_lon_small,
  // distance in radians
       $distance_rad);

EXPLAIN on the above query might say that it's not using index unless there's enough results to trigger such. The index will be used when there's enough data in the coordinates table. You can add FORCE INDEX (lat_lon_idx) to the SELECT to make it use the index with no regards to the table size, so you can verify with EXPLAIN that it is working correctly.

With the above code samples you should have a working and scalable implementation of object search by distance with minimal error.

silvio
  • 899
  • 7
  • 5
11

I have had to work this out in some detail, so I'll share my result. This uses a zip table with latitude and longitude tables. It doesn't depend on Google Maps; rather you can adapt it to any table containing lat/long.

SELECT zip, primary_city, 
       latitude, longitude, distance_in_mi
  FROM (
SELECT zip, primary_city, latitude, longitude,r,
       (3963.17 * ACOS(COS(RADIANS(latpoint)) 
                 * COS(RADIANS(latitude)) 
                 * COS(RADIANS(longpoint) - RADIANS(longitude)) 
                 + SIN(RADIANS(latpoint)) 
                 * SIN(RADIANS(latitude)))) AS distance_in_mi
 FROM zip
 JOIN (
        SELECT  42.81  AS latpoint,  -70.81 AS longpoint, 50.0 AS r
   ) AS p 
 WHERE latitude  
  BETWEEN latpoint  - (r / 69) 
      AND latpoint  + (r / 69)
   AND longitude 
  BETWEEN longpoint - (r / (69 * COS(RADIANS(latpoint))))
      AND longpoint + (r / (69 * COS(RADIANS(latpoint))))
  ) d
 WHERE distance_in_mi <= r
 ORDER BY distance_in_mi
 LIMIT 30

Look at this line in the middle of that query:

    SELECT  42.81  AS latpoint,  -70.81 AS longpoint, 50.0 AS r

This searches for the 30 nearest entries in the zip table within 50.0 miles of the lat/long point 42.81/-70.81 . When you build this into an app, that's where you put your own point and search radius.

If you want to work in kilometers rather than miles, change 69 to 111.045 and change 3963.17 to 6378.10 in the query.

Here's a detailed writeup. I hope it helps somebody. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

O. Jones
  • 103,626
  • 17
  • 118
  • 172
4
 SELECT *, (  
    6371 * acos(cos(radians(search_lat)) * cos(radians(lat) ) *   
cos(radians(lng) - radians(search_lng)) + sin(radians(search_lat)) *         sin(radians(lat)))  
) AS distance  
FROM table  
WHERE lat != search_lat AND lng != search_lng AND distance < 25  
 ORDER BY distance  
FETCH 10 ONLY 

for distance of 25 km

Harish Lalwani
  • 754
  • 5
  • 20
  • The last (radians(lat) must be sin(radians(lat)) – KGs Mar 08 '17 at 20:01
  • im getting an error "unknown column distance" why is this? –  Apr 09 '20 at 20:25
  • @JillJohn if you only want distance then you can remove order by distance completely. If you want to sort results you can use this - ORDER BY ( 6371 * acos(cos(radians(search_lat)) * cos(radians(lat) ) * cos(radians(lng) - radians(search_lng)) + sin(radians(search_lat)) * sin(radians(lat))) ). – Harish Lalwani Apr 11 '20 at 12:21
3

I can't comment on the above answer, but be careful with @Pavel Chuchuva's answer. That formula will not return a result if both coordinates are the same. In that case, distance is null, and so that row won't be returned with that formula as is.

I'm not a MySQL expert, but this seems to be working for me:

SELECT id, ( 3959 * acos( cos( radians(37) ) * cos( radians( lat ) ) * cos( radians( lng ) - radians(-122) ) + sin( radians(37) ) * sin( radians( lat ) ) ) ) AS distance 
FROM markers HAVING distance < 25 OR distance IS NULL ORDER BY distance LIMIT 0 , 20;
John Crenshaw
  • 85
  • 1
  • 5
  • 2
    If the positions are identical, it *shouldn't* come out NULL, but as zero (as `ACOS(1)` is 0). You *might* see rounding issues with the xaxis * xaxis + yaxis * yaxis + zaxis * zaxis going out of range for ACOS but you don't appear to be guarding against that? – Rowland Shaw Mar 15 '13 at 13:18
3

I have written a procedure that can calculate the same, but you have to enter the latitude and longitude in the respective table.

drop procedure if exists select_lattitude_longitude;

delimiter //

create procedure select_lattitude_longitude(In CityName1 varchar(20) , In CityName2 varchar(20))

begin

    declare origin_lat float(10,2);
    declare origin_long float(10,2);

    declare dest_lat float(10,2);
    declare dest_long float(10,2);

    if CityName1  Not In (select Name from City_lat_lon) OR CityName2  Not In (select Name from City_lat_lon) then 

        select 'The Name Not Exist or Not Valid Please Check the Names given by you' as Message;

    else

        select lattitude into  origin_lat from City_lat_lon where Name=CityName1;

        select longitude into  origin_long  from City_lat_lon where Name=CityName1;

        select lattitude into  dest_lat from City_lat_lon where Name=CityName2;

        select longitude into  dest_long  from City_lat_lon where Name=CityName2;

        select origin_lat as CityName1_lattitude,
               origin_long as CityName1_longitude,
               dest_lat as CityName2_lattitude,
               dest_long as CityName2_longitude;

        SELECT 3956 * 2 * ASIN(SQRT( POWER(SIN((origin_lat - dest_lat) * pi()/180 / 2), 2) + COS(origin_lat * pi()/180) * COS(dest_lat * pi()/180) * POWER(SIN((origin_long-dest_long) * pi()/180 / 2), 2) )) * 1.609344 as Distance_In_Kms ;

    end if;

end ;

//

delimiter ;
Bo Persson
  • 90,663
  • 31
  • 146
  • 203
Abdul Manaf
  • 4,768
  • 3
  • 27
  • 34
2

I thought my javascript implementation would be a good reference to:

/*
 * Check to see if the second coord is within the precision ( meters )
 * of the first coord and return accordingly
 */
function checkWithinBound(coord_one, coord_two, precision) {
    var distance = 3959000 * Math.acos( 
        Math.cos( degree_to_radian( coord_two.lat ) ) * 
        Math.cos( degree_to_radian( coord_one.lat ) ) * 
        Math.cos( 
            degree_to_radian( coord_one.lng ) - degree_to_radian( coord_two.lng ) 
        ) +
        Math.sin( degree_to_radian( coord_two.lat ) ) * 
        Math.sin( degree_to_radian( coord_one.lat ) ) 
    );
    return distance <= precision;
}

/**
 * Get radian from given degree
 */
function degree_to_radian(degree) {
    return degree * (Math.PI / 180);
}
David Harkness
  • 35,992
  • 10
  • 112
  • 134
Sam Vloeberghs
  • 1,082
  • 5
  • 18
  • 29
2

calculate distance in Mysql

 SELECT (6371 * acos(cos(radians(lat2)) * cos(radians(lat1) ) * cos(radians(long1) -radians(long2)) + sin(radians(lat2)) * sin(radians(lat1)))) AS distance

thus distance value will be calculated and anyone can apply as required.