Велика відстань кола MySQL (формула Гаверсіна)


184

У мене працює сценарій PHP, який отримує значення Longitude і Latitude, а потім вводить їх у запит MySQL. Я хотів би зробити це виключно MySQL. Ось мій поточний PHP-код:

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 ";
    }

Хтось знає, як зробити це повністю MySQL? Я трохи переглянув Інтернет, але більша частина літератури про нього досить заплутана.


4
Виходячи з усіх відмінних відповідей нижче, тут працює діючий зразок формули
Хаверсіна

Дякуємо, що поділилися цим Майклом.М
Ніком Вудгемсом

stackoverflow.com/a/40272394/1281385 Є приклад того, як переконатися, що індекс потрапив
exussum

Відповіді:


357

З FAQ щодо коду Google - Створення локатора магазину за допомогою PHP, MySQL та Google Maps :

Ось оператор SQL, який знайде найближчі 20 локацій, які знаходяться в радіусі 25 миль до координати 37, -122. Він обчислює відстань на основі широти / довготи цього рядка та цільової широти / довготи, а потім запитує лише рядки, де значення відстані менше 25, впорядковує весь запит на відстань і обмежує його на 20 результатів. Щоб шукати кілометри замість миль, замініть 3959 на 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;

2
заява sql дійсно хороша. але де я можу передати свої координати в цей вислів? я не можу побачити, де пройшли координати
Манн

32
Замініть свої координати 37 і -122.
Павло Чучува

5
Мені цікаво про наслідки цього для продуктивності, якщо мільйони місць (+ тисячі відвідувачів) ...
Halil Özgür

12
Ви можете звузити запит на кращу ефективність, як це пояснено в цьому документі: tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
maliayas

2
@FosAvance Так, цей запит спрацює, якщо у вас є markersтаблиця з полями id, lan та lng.
Павло Чучува

32

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

з широтою і довготою в радіані.

так

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

це ваш SQL-запит

щоб отримати результати в км або милях, помножте результат на середній радіус Землі ( 3959милі, 6371км або 3440морські милі)

Те, що ви обчислюєте у своєму прикладі, - це обмежувальне поле. Якщо ви розмістите свої координатні дані у просторовому стовпчику MySQL , ви можете використовувати вбудований функціонал MySQL для запиту даних.

SELECT 
  id
FROM spatialEnabledTable
WHERE 
  MBRWithin(ogc_point, GeomFromText('Polygon((0 0,0 3,3 3,3 0,0 0))'))

13

Якщо додати допоміжні поля до таблиці координат, ви можете покращити час відповіді на запит.

Подобається це:

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`)
)    

Якщо ви використовуєте TokuDB, ви отримаєте ще кращу ефективність, якщо додати індекси кластеризації за будь-яким із предикатів, наприклад, таким:

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

Вам знадобляться основні lat і lon в градусах, а також sin (lat) в радіанах, cos (lat) * cos (lon) в радіанах і cos (lat) * sin (lon) в радіанах для кожної точки. Тоді ви створюєте функцію mysql, smth так:

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

Це дає відстань.

Не забудьте додати індекс на lat / lon, щоб обмежувальний бокс міг допомогти пошуку замість уповільнення його (індекс уже додано у запиті CREATE TABLE вище).

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

Враховуючи стару таблицю з лише координатами lat / lon, ви можете налаштувати сценарій для її оновлення так: (php за допомогою 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']
  ));
}

Потім ви оптимізуєте фактичний запит, щоб робити розрахунок відстані лише тоді, коли це дійсно потрібно, наприклад, обмежуючи коло (ну овал) зсередини та зовні. Для цього вам потрібно буде перерахувати кілька показників для самого запиту:

// 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));

Враховуючи ці препарати, запит має щось подібне (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);

ПОЯСНЕННЯ на вищезазначеному запиті може сказати, що він не використовує індекс, якщо не вистачить результатів для його запуску. Індекс буде використаний, коли в таблиці координат буде достатньо даних. Ви можете додати FORCE INDEX (lat_lon_idx) до SELECT, щоб змусити його використовувати індекс, не залежно від розміру таблиці, так що ви можете перевірити EXPLAIN, що він працює правильно.

З наведеними вище зразками коду ви повинні мати працюючу та масштабовану реалізацію пошуку об’єктів на відстані з мінімальною помилкою.


10

Мені довелося детально опрацювати це питання, тому поділюся своїм результатом. Для цього використовується zipтаблиця з latitudeта longitudeтаблиці. Це не залежить від Карт Google; скоріше ви можете адаптувати його до будь-якої таблиці, що містить 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

Подивіться на цей рядок посеред запиту:

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

Цей пошук шукає 30 найближчих записів у zipтаблиці в межах 50,0 миль від точки Lat / long 42,81 / -70,81. Коли ви вбудовуєте це в додаток, саме там ви ставите свою точку та радіус пошуку.

Якщо ви хочете працювати в кілометрах, а не в милях, перейдіть 69на запит 111.045і перейдіть 3963.17на 6378.10.

Ось детальний опис. Я сподіваюся, що це комусь допоможе. http://www.plumislandmedia.net/mysql/haversine-mysql-nevable-loc/


3

Я написав процедуру, яка може обчислити те саме, але ви повинні ввести широту і довготу у відповідну таблицю.

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 ;

3

Я не можу коментувати вищезгадану відповідь, але будьте обережні з відповіддю @Pavel Chuchuva. Ця формула не поверне результат, якщо обидві координати однакові. У цьому випадку відстань є нульовим, і такий рядок не повертається з такою формулою, як є.

Я не експерт MySQL, але це, здається, працює для мене:

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;

2
Якщо позиції однакові, він повинен виходити не NULL, а нульовим (як ACOS(1)0). Можливо, ви побачите проблеми із округленням для xaxis * xaxis + yaxis * yaxis + zaxis * zaxis, що виходять за межі діапазону для ACOS, але, здається, ви не захищаєте?
Роуленд Шоу

3
 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 

на відстань 25 км


Останній (радіан (лат.) Повинен бути гріхом (радіан (лат.))
KGs

я отримую помилку "невідома відстань стовпця", чому це?
Джилл Джон

@JillJohn, якщо ви хочете лише відстань, ви можете видалити замовлення на відстань повністю. Якщо ви хочете сортувати результати, ви можете скористатись цим - ЗАМОВИТИ ПО (6371 * acos (cos (радіани (search_lat)) * cos (радіани (лат)) * cos (радіани (lng) - радіани (search_lng)) + sin (радіани (search_lat)) * sin (радіани (лат.)))).
Harish Lalwani

2

Я подумав, що моя реалізація JavaScript буде хорошою посиланням на:

/*
 * 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);
}

0

обчислити відстань у Mysql

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

таким чином, величина відстані буде обчислена, і кожен може подати заявку за потребою.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.