ระยะทางวงกลมใหญ่ MySQL (สูตรฮาร์ซีน)


184

ฉันมีสคริปต์ PHP ที่ใช้งานได้ซึ่งได้ค่าลองจิจูดและค่าละติจูดแล้วป้อนลงในคิวรี 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
ขึ้นอยู่กับคำตอบที่ยอดเยี่ยมทั้งหมดด้านล่างนี่คือตัวอย่างการทำงานของสูตร Haversine ที่ใช้งานจริง
StartupGuy

ขอขอบคุณที่แบ่งปัน Michael.M
Nick Woodhams

stackoverflow.com/a/40272394/1281385 มีตัวอย่างของวิธีการตรวจสอบให้แน่ใจว่าดัชนีถูกตี
exussum

คำตอบ:


357

จากคำถามที่พบบ่อยเกี่ยวกับรหัสของ 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 นั้นดีจริงๆ แต่ฉันจะส่งพิกัดของฉันไปที่คำสั่งนี้ได้ที่ไหน ฉันไม่เห็นพิกัดที่ใดก็ได้ผ่านไปแล้ว
Mann

32
แทนที่ 37 และ -122 ด้วยพิกัดของคุณ
Pavel Chuchuva

5
ฉันสงสัยเกี่ยวกับความหมายของการแสดงหากมีสถานที่หลายล้านแห่ง (มีผู้เข้าชมนับพัน)
Halil Özgür

12
คุณสามารถ จำกัดคิวรีให้
maliayas

2
@FosAvance ใช่แบบสอบถามนี้จะทำงานหากคุณมีmarkersตารางที่มีเขตข้อมูล id, lan และ lng
Pavel Chuchuva

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 ที่เปิดใช้งานเชิงพื้นที่คุณสามารถใช้ฟังก์ชัน build ของ 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`)

ด้วยตารางเก่าที่มีพิกัดละติจูด / ลองจิจูดเท่านั้นคุณสามารถตั้งค่าสคริปต์เพื่ออัปเดตดังนี้: (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 เพื่อให้ใช้ดัชนีโดยไม่คำนึงถึงขนาดของตารางดังนั้นคุณสามารถตรวจสอบด้วยอธิบายว่าทำงานได้อย่างถูกต้อง

ด้วยตัวอย่างโค้ดข้างต้นคุณควรมีการทำงานและปรับใช้การค้นหาวัตถุตามระยะทางโดยมีข้อผิดพลาดน้อยที่สุด


10

ฉันต้องทำงานในรายละเอียดบางอย่างดังนั้นฉันจะแบ่งปันผลของฉัน ใช้zipตารางกับlatitudeและlongitudeตาราง มันไม่ได้ขึ้นอยู่กับ Google Maps; คุณสามารถปรับให้เข้ากับตารางใดก็ได้ที่มี 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 ไมล์จากจุดละติจูด / ลองจิจูด 42.81 / -70.81 เมื่อคุณสร้างสิ่งนี้ลงในแอปนั่นคือที่ที่คุณวางจุดของคุณเองและค้นหารัศมี

หากคุณต้องการที่จะทำงานในกิโลเมตรมากกว่าไมล์เปลี่ยนแปลง69ไป111.045และการเปลี่ยนแปลง3963.17ไป6378.10ในแบบสอบถาม

นี่คือรายละเอียดการเขียน ฉันหวังว่ามันจะช่วยให้ใครบางคน http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-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 แต่คุณดูเหมือนจะไม่ได้รับการปกป้องหรือไม่
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 กม


สุดท้าย (เรเดียน (lat) จะต้องเป็น sin (เรเดียน (lat))
KGs

ฉันได้รับข้อผิดพลาด "ระยะห่างของคอลัมน์ที่ไม่รู้จัก" ทำไมจึงเป็นเช่นนี้
Jill John

@JillJohn ถ้าคุณต้องการระยะทางแล้วคุณสามารถลบคำสั่งซื้อตามระยะทางได้อย่างสมบูรณ์ หากคุณต้องการเรียงลำดับผลลัพธ์คุณสามารถใช้ - ORDER BY (6371 * acos (cos (เรเดียน (search_lat)) * cos (เรเดียน (lat)) * cos (เรเดียน (lng) - เรเดียน (search_lng)) + sin (เรเดียน) (search_lat)) * sin (เรเดียน (lat))))
Harish Lalwani

2

ฉันคิดว่าการใช้งานจาวาสคริปต์ของฉันจะเป็นการอ้างอิงที่ดีถึง:

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

ดังนั้นค่าระยะทางจะถูกคำนวณและทุกคนสามารถนำไปใช้ได้ตามต้องการ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.