【问题标题】:Sorting MySQL query by Latitude/Longitude按纬度/经度对 MySQL 查询进行排序
【发布时间】:2013-05-04 03:58:01
【问题描述】:

我数据库中的每个用户都将他们的纬度和经度存储在两个字段(纬度、经度)中

每个字段的格式为:

lon | -1.403976 
lat | 53.428691

如果用户在 100 英里范围内搜索其他用户,我会执行以下操作以计算适当的纬度/经度范围($lat 和 $lon 是当前用户值)

$R = 3960;  // earth's mean radius
$rad = '100';
// first-cut bounding box (in degrees)
$maxLat = $lat + rad2deg($rad/$R);
$minLat = $lat - rad2deg($rad/$R);
// compensate for degrees longitude getting smaller with increasing latitude
$maxLon = $lon + rad2deg($rad/$R/cos(deg2rad($lat)));
$minLon = $lon - rad2deg($rad/$R/cos(deg2rad($lat)));

$maxLat=number_format((float)$maxLat, 6, '.', '');
$minLat=number_format((float)$minLat, 6, '.', '');
$maxLon=number_format((float)$maxLon, 6, '.', '');
$minLon=number_format((float)$minLon, 6, '.', '');

然后我可以执行如下查询:

$query = "SELECT * FROM table WHERE lon BETWEEN '$minLon' AND '$maxLon' AND lat BETWEEN '$minLat' AND '$maxLat'";

这很好用,我在输出阶段使用函数来计算和显示用户之间的实际距离,但我希望能够在查询阶段通过减少或增加距离来对结果进行排序。

有什么办法吗?

【问题讨论】:

    标签: php mysql


    【解决方案1】:

    还记得毕达哥拉斯吗?

    $sql = "SELECT * FROM table 
        WHERE lon BETWEEN '$minLon' AND '$maxLon' 
          AND lat BETWEEN '$minLat' AND '$maxLat'
        ORDER BY (POW((lon-$lon),2) + POW((lat-$lat),2))";
    

    从技术上讲,这是距离的平方,而不是实际距离,但因为您只是将它用于排序,所以没关系。

    这使用了平面距离公式,这应该适用于小距离。

    但是:

    如果您想更精确或使用更长的距离,请使用this formula for great circle distances in radians

    dist = acos[ sin(lat1)*sin(lat2)+cos(lat1)*cos(lat2)*cos(lng1-lng2) ]
    

    (要以实际单位而不是弧度获得距离,请将其乘以地球的半径。不过,这对于排序目的不是必需的。)

    MySQL 计算引擎假定纬度和经度以弧度为单位,因此如果它以度为单位(很可能是),您必须将每个值乘以 pi/180,大约为 0.01745:

    $sf = 3.14159 / 180; // scaling factor
    $sql = "SELECT * FROM table 
        WHERE lon BETWEEN '$minLon' AND '$maxLon' 
          AND lat BETWEEN '$minLat' AND '$maxLat'
        ORDER BY ACOS(SIN(lat*$sf)*SIN($lat*$sf) + COS(lat*$sf)*COS($lat*$sf)*COS((lon-$lon)*$sf))";
    

    甚至:

    $sf = 3.14159 / 180; // scaling factor
    $er = 6350; // earth radius in miles, approximate
    $mr = 100; // max radius
    $sql = "SELECT * FROM table 
        WHERE $mr >= $er * ACOS(SIN(lat*$sf)*SIN($lat*$sf) + COS(lat*$sf)*COS($lat*$sf)*COS((lon-$lon)*$sf))
        ORDER BY ACOS(SIN(lat*$sf)*SIN($lat*$sf) + COS(lat*$sf)*COS($lat*$sf)*COS((lon-$lon)*$sf))";
    

    【讨论】:

    • 谢谢 Blaze,这似乎奏效了 - 有一两个不合适的地方,但它不是破坏交易的(除非你能想到我如何解决它)。不顾一切点赞。谢谢
    • 根据平方距离,它们真的不合适吗?还是您认为平方距离计算不正确?
    • 该查询没有考虑到经度是从格林威治子午线向东西方向测量的事实,因此它将返回彼此靠近和靠近 180 度子午线的两个地方的非常长的距离。跨度>
    • 添加了一个更精确的公式,从维基百科借来的。
    • 这很有趣,也很有教育意义。 :-)
    【解决方案2】:

    仅使用 SELECT * FROM Table WHERE lat between $minlat and $maxlat 不够准确。

    查询距离的正确方法是使用弧度坐标。

    <?php
      $sql = "SELECT * FROM Table WHERE acos(sin(1.3963) * sin(Lat) + cos(1.3963) * cos(Lat) * cos(Lon - (-0.6981))) * 6371 <= 1000";
    

    这是一个方便的参考 - http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates

    例如:

    <?php
      $distance = 100;
      $current_lat = 1.3963;
      $current_lon = -0.6981;
      $earths_radius = 6371;
    
      $sql = "SELECT * FROM Table T WHERE acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius <= $distance";
    

    如果你想按顺序显示距离:

    <?php
      $distance = 100;
      $current_lat = 1.3963;
      $current_lon = -0.6981;
      $earths_radius = 6371;
    
      $sql = "SELECT *, (acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius) as distance FROM Table T WHERE acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius <= $distance ORDER BY acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius DESC";
    

    为@Blazemonger 编辑并避免疑问:) 如果您想以度数而不是弧度工作:

    <?php
      $current_lat_deg = 80.00209691585;
      $current_lon_deg = -39.99818366895;
      $radians_to_degs = 57.2957795;
    
      $distance = 100;
      $current_lat = $current_lat_deg / $radians_to_degs;
      $current_lon = $current_lon_deg / $radians_to_degs;
      $earths_radius = 6371;
    
      $sql = "SELECT *, (acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius) as distance FROM Table T WHERE acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius <= $distance ORDER BY acos(sin($current_lat) * sin(T.Lat) + cos($current_lat) * cos(T.Lat) * cos(T.Lon - ($current_lon))) * $earths_radius DESC";
    

    您可以轻松地将其封装到一个接受上述信息中的弧度或度数的类中。

    【讨论】:

    • 我有相当多的地理定位应用程序使用这个确切的代码。目前没有投诉! :)
    • 卢克,我很欣赏这个答案,但这让我有点困惑 - 你能在正确的地方使用我的 $max 和 $min 变量来编辑它吗?我不想问,但是,好吧,你知道 :)
    • 没问题,试一试!
    • 他举了lat | 53.428691的例子——这个数字太大了,不能用弧度表示。
    • 嘿@SaschaGrindau,地球半径和搜索半径以千米为单位。希望这会有所帮助。
    【解决方案3】:

    这是给我正确结果的公式(与上面的解决方案相反)。使用谷歌地图“测量距离”功能确认(直接距离,而不是运输距离)。

    SELECT
        *,
        ( 3959 * acos( cos( radians(:latitude) ) * cos( radians( latitude ) ) * cos( radians( longitude ) - radians(:longitude) ) + sin( radians(:latitude) ) * sin( radians( latitude ) ) ) ) AS `distance`
    FROM `locations`
    ORDER BY `distance` ASC
    

    :latitude:longitude 是 PDO 函数的占位符。如果您愿意,可以将它们替换为实际值。 latitudelongitude 是列名。

    3959 是以英里为单位的地球半径; distance 输出也将以英里为单位。要将其更改为公里,请将3959 替换为6371

    【讨论】:

    • 这与我自己的答案和@Blazemonger 的答案相同。论坛的顺序不同,但除此之外,您使用了 PDO(当 OP 不使用时)并且您通过使用 MYSQL 中的弧度函数来强制弧度。更不用说,声称其他答案具有误导性,并且随意投反对票不符合 SO 的精神。
    • 感谢您的评论。同样,我从上面的答案(包括您的)中复制了查询,这些查询给了我 misleading 值 - 很好,我已经仔细检查了它们。所以我浏览了我的旧代码档案,找到了当时对我有用的查询。所以这只是另一个(工作)答案,是的,我投了反对票,因为其他查询对我不起作用。我不明白这有什么问题,抱歉。
    • 这很奇怪。使用我的数据,查询给出了相同的答案。我想看看你的数据来比较结果!一定是发生了很奇怪的事情。
    • 这里也一样,上面的答案给了我奇怪的结果。这对我来说似乎很准确。
    【解决方案4】:

    不会为您提供按平面距离排序的结果(不考虑地球曲率),但对于小半径'应该可以解决。

    SELECT * from table where lon between '$minLon' and '$maxLon' and lat between '$minLat' and '$maxLat' order by (abs(lon-$lon)/2) + (abs(lat-$lat)/2);
    

    【讨论】:

    • Orange,谢谢你,就像 Blaze 的回答一样,这适用于少数不合适的地方 - Blaze 似乎少了一两个不合适的地方。无论如何都赞成。谢谢你的回答。
    • /2 是多余的,因为您只是在比较大小,而额外的括号是多余的,因为加法的优先级已经高于除法。
    【解决方案5】:

    以上答案都不能在格林威治子午线上正确工作。 Haversine 公式:

      // 6371 is the Earth's radius in km
      6371 * 2 * ASIN(SQRT( 
         POWER(SIN((lat - abs(:latitude)) * pi()/180 / 2), 2) 
          + COS(lat * pi()/180 ) * COS(abs(:latitude) * pi()/180) 
          * POWER(SIN((lon - :longitude) *  pi()/180 / 2), 2) 
      )) as distance
    

    我从here 获取的,在这个answer 中引用了一个类似的问题,确实有效。

    【讨论】:

      猜你喜欢
      • 2016-06-11
      • 2013-12-29
      • 2018-11-13
      • 2019-09-23
      • 1970-01-01
      • 2013-06-28
      • 1970-01-01
      • 2014-01-22
      • 2021-10-22
      相关资源
      最近更新 更多