MySQL Büyük Daire Mesafesi (Haversine formülü)


184

Boylam ve Latitude değerlerini alan ve daha sonra bir MySQL sorgusu içine girer çalışan bir PHP komut dosyası var. Sadece MySQL yapmak istiyorum. İşte şu anki PHP Kodum:

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

Herkes bunu tamamen MySQL nasıl yapacağını biliyor mu? İnternete biraz göz attım ama literatürün çoğu oldukça kafa karıştırıcı.


4
Aşağıdaki tüm mükemmel cevaplara dayanarak, işte Haversine formülünün çalışma örneği
StartupGuy

Michael.M'yi paylaştığınız için teşekkürler
Nick Woodhams

stackoverflow.com/a/40272394/1281385 Endeksin nasıl görüntülendiğinden emin olmanın bir örneği var
exussum

Yanıtlar:


357

Gönderen Google Code SSS - PHP, MySQL & Google Maps ile bir Store Locator Oluşturma :

İşte 37, -122 koordinatına 25 mil yarıçap içinde en yakın 20 konumu bulan SQL deyimi. Mesafeyi, o satırın enlem / boylamına ve hedef enlem / boylama göre hesaplar ve sonra yalnızca mesafe değerinin 25'ten küçük olduğu satırları sorar, tüm sorguyu mesafeye göre sıralar ve 20 sonuçla sınırlar. Mil yerine kilometre ile arama yapmak için 3959 yerine 6371 yazın.

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 deyimi gerçekten iyidir. ama koordinatlarımı bu ifadeye nerede aktarabilirim? koordinatların geçtiğini hiçbir yerde göremiyorum
Mann

32
37 ve -122'yi koordinatlarınızla değiştirin.
Pavel Chuchuva

5
Milyonlarca yer (+ binlerce ziyaretçi) varsa bunun performansla ilgili sonuçlarını merak ediyorum ...
Halil Özgür

12
Daha iyi performans için sorguyu bu dokümanda açıklandığı gibi daraltabilirsiniz: tr.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL
maliayas 15:12

2
@FosAvance Evet, markersid, lan ve lng alanlarına sahip tablonuz varsa bu sorgu işe yarar .
Pavel Chuchuva

32

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

radyanda enlem ve boylam ile.

yani

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

SQL sorgunuz

Sonuçlarınızı Km veya mil cinsinden almak için sonucu Dünya'nın ortalama yarıçapıyla ( 3959mil, 6371Km veya3440 deniz mili)

Örneğinizde hesapladığınız şey sınırlayıcı bir kutu. Bir daki koordinat verileri koyarsanız MySQL sütunu etkinleştirdikten konumsal kullanabileceğiniz işlevselliği MySQL'ın yapı verilerini sorgulamak için.

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

13

Koordinatlar tablosuna yardımcı alanlar eklerseniz, sorgunun yanıt süresini iyileştirebilirsiniz.

Bunun gibi:

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 kullanıyorsanız, tahminlerden herhangi birine kümeleme dizinleri eklerseniz daha da iyi performans elde edersiniz, örneğin, şöyle:

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

Temel lat ve lon dereceleri ve radyanlarda sin (lat), radyanlarda cos (lat) * cos (lon) ve her nokta için radyanlarda cos (lat) * sin (lon) gerekir. Sonra bir mysql işlevi, böyle smth oluşturun:

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

Bu size mesafeyi verir.

Sınırlayıcı boksun aramayı yavaşlatmak yerine aramaya yardımcı olabilmesi için lat / lon üzerine bir dizin eklemeyi unutmayın (dizin zaten CREATE TABLE sorgusuna eklenmiştir).

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

Yalnızca lat / lon koordinatlarına sahip eski bir tablo verildiğinde, bunu güncellemek için bir komut dosyası oluşturabilirsiniz: (php meekrodb kullanarak)

$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']
  ));
}

Daha sonra gerçek sorguyu yalnızca gerçekten gerekli olduğunda mesafe hesaplamasını yapmak için optimize edersiniz, örneğin daireyi (kuyu, oval) içten ve dıştan sınırlayarak. Bunun için, sorgunun kendisi için birkaç metriği önceden hesaplamanız gerekir:

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

Bu hazırlıklar göz önüne alındığında, sorgu şu şekilde gider (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);

Yukarıdaki sorgudaki EXPLAIN, böyle bir tetikleme için yeterli sonuç olmadığı sürece dizin kullanmadığını söyleyebilir. Dizin, koordinatlar tablosunda yeterli veri olduğunda kullanılacaktır. FORCE INDEX (lat_lon_idx) öğesini SELECT öğesine tablo boyutuna bakmaksızın dizini kullanmasını sağlamak için ekleyebilirsiniz, böylece EXPLAIN ile doğru çalıştığını doğrulayabilirsiniz.

Yukarıdaki kod örnekleri ile, minimum hata ile mesafeye göre nesne arama çalışma ve ölçeklenebilir bir uygulama olması gerekir.


10

Bunu biraz ayrıntılı olarak çalışmak zorunda kaldım, bu yüzden sonucumu paylaşacağım. Bu, ve zipile bir tablo kullanır . Google Haritalar'a bağlı değildir; bunun yerine lat / long içeren herhangi bir tabloya uyarlayabilirsiniz.latitudelongitude

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

Sorgunun ortasındaki şu satıra bakın:

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

Bu, en yakın 30 girişi arar. zip tablodaki 42.81 / -70.81 enlem / uzun noktasının 50.0 mil yakınında arar. Bunu bir uygulamada oluşturduğunuzda, kendi noktanızı ve arama yarıçapınızı koyduğunuz yer burasıdır.

Eğer kilometre ziyade mil, değişim iş istiyorsanız 69etmek 111.045ve değişim 3963.17için6378.10 sorguda.

İşte ayrıntılı bir yazı. Umarım birine yardımcı olur. http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/


3

Aynı hesaplayabilen bir prosedür yazdım, ancak ilgili tabloya enlem ve boylam girmeniz gerekiyor.

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

Yukarıdaki cevap hakkında yorum yapamam, ama @Pavel Chuchuva'nın cevabına dikkat et. Her iki koordinat aynı ise, bu formül bir sonuç döndürmez. Bu durumda, mesafe null olur ve böylece bu satır bu formülle olduğu gibi döndürülmez.

Ben bir MySQL uzmanı değilim, ama bu benim için çalışıyor gibi görünüyor:

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
Pozisyonlar aynıysa, NULL çıkmamalı, sıfır olarak ( 0 gibi) çıkmalıdırACOS(1) . Sen belki Xaxis ile sorunları yuvarlama bkz * x- + y-ekseni * y-ekseni + Zaxis * Zaxis ACOS için kapsama alanı dışına çıkıyor ama bu karşı bekçilik görünmüyor?
Rowland Shaw

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 km mesafe için


Son (radyan (lat)) günah (radyan (lat)) olmalı
KGs

im "bilinmeyen sütun mesafesi" hatası alıyorum neden bu?
Jill John

@JillJohn sadece mesafe istiyorsanız o zaman tamamen mesafeye göre sipariş kaldırabilirsiniz. Sonuçları sıralamak isterseniz bunu kullanabilirsiniz - ORDER BY (6371 * acos (cos (radyanlar (arama_lat)) * cos (radyanlar (lat)) * cos (radyanlar (lng) - radyanlar (arama_dng)) + sin (radyanlar) (search_lat)) * sin (radyan (lat)))).
Harish Lalwani

2

Javascript uygulamamın iyi bir referans olacağını düşündüm:

/*
 * 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 mesafe hesaplamak

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

böylece mesafe değeri hesaplanacak ve herkes gerektiği gibi başvurabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.