【问题标题】:Find nearest latitude/longitude with an SQL query使用 SQL 查询查找最近的纬度/经度
【发布时间】:2010-02-10 03:21:59
【问题描述】:

我有纬度和经度,我想从数据库中拉出记录,该记录与距离最近的纬度和经度,如果该距离大于指定的距离,则不要检索它。

表结构:

id
latitude
longitude
place name
city
country
state
zip
sealevel

【问题讨论】:

  • 这是proximity search问题的重复。
  • Alexander Rubin 在Geo (proximity) search with MySQL 上有一组幻灯片(PDF 链接)
  • 当心这里的答案。大多数不能使用任何索引,因此对于大型数据集表现不佳。有些仅限于非球面距离计算,因此在可能的全球应用中没有用处。
  • 进一步讨论,包括缩放、精度和 5 种竞争技术的讨论:mysql.rjweb.org/doc.php/find_nearest_in_mysql
  • 如果查询显示HAVING distance < ...,则查询可能会检查每一行并计算每一行的距离。 (缓慢且不可扩展。)

标签: mysql sql coordinates geospatial


【解决方案1】:
SELECT latitude, longitude, SQRT(
    POW(69.1 * (latitude - [startlat]), 2) +
    POW(69.1 * ([startlng] - longitude) * COS(latitude / 57.3), 2)) AS distance
FROM TableName HAVING distance < 25 ORDER BY distance;

其中 [starlat][startlng] 是开始测量距离的位置。

【讨论】:

  • 只是性能说明,最好不要对距离变量进行sqrt,而是对'25'测试值进行平方...稍后如果需要显示距离,则对已通过的结果进行sqrt
  • 对于以米为单位的距离的相同查询是什么? (目前以英里为单位,对吗?)
  • 那个 25 是多少度数?
  • 在这里澄清一下,69.1 是英里到纬度的转换系数。 57.3 大约是 180/pi,所以这是从度到弧度的转换,用于余弦函数。 25 是以英里为单位的搜索半径。这是使用十进制度和法定里程时使用的公式。
  • 另外,它没有考虑地球的曲率。这对于短搜索半径来说不是问题。否则埃文和伊戈尔的答案会更完整。
【解决方案2】:

谷歌的解决方案:

创建表格

在创建 MySQL 表时,需要特别注意 lat 和 lng 属性。使用 Google 地图当前的缩放功能,您应该只需要小数点后 6 位的精度。为了将表所需的存储空间保持在最低限度,您可以将 lat 和 lng 属性指定为大小为 (10,6) 的浮点数。这将让字段存储小数点后 6 位,加上小数点前最多 4 位,例如-123.456789 度。您的表还应该有一个 id 属性作为主键。

CREATE TABLE `markers` (
  `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
  `name` VARCHAR( 60 ) NOT NULL ,
  `address` VARCHAR( 80 ) NOT NULL ,
  `lat` FLOAT( 10, 6 ) NOT NULL ,
  `lng` FLOAT( 10, 6 ) NOT NULL
) ENGINE = MYISAM ;

填充表格

创建表后,是时候用数据填充它了。下面提供的样本数据是针对分散在美国各地的大约 180 家披萨店。在 phpMyAdmin 中,您可以使用 IMPORT 选项卡来导入各种文件格式,包括 CSV(逗号分隔值)。 Microsoft Excel 和 Google 电子表格都导出为 CSV 格式,因此您可以通过导出/导入 CSV 文件轻松地将数据从电子表格传输到 MySQL 表。

INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Frankie Johnnie & Luigo Too','939 W El Camino Real, Mountain View, CA','37.386339','-122.085823');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Amici\'s East Coast Pizzeria','790 Castro St, Mountain View, CA','37.38714','-122.083235');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Kapp\'s Pizza Bar & Grill','191 Castro St, Mountain View, CA','37.393885','-122.078916');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Round Table Pizza: Mountain View','570 N Shoreline Blvd, Mountain View, CA','37.402653','-122.079354');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Tony & Alba\'s Pizza & Pasta','619 Escuela Ave, Mountain View, CA','37.394011','-122.095528');
INSERT INTO `markers` (`name`, `address`, `lat`, `lng`) VALUES ('Oregano\'s Wood-Fired Pizza','4546 El Camino Real, Los Altos, CA','37.401724','-122.114646');

使用 MySQL 查找位置

要在标记表中查找位于给定纬度/经度的特定半径距离内的位置,您可以使用基于 Haversine 公式的 SELECT 语句。 Haversine 公式通常用于计算球体上两对坐标之间的大圆距离。 Wikipedia 给出了深入的数学解释,并且在 Movable Type 的网站上对与编程相关的公式进行了很好的讨论。

下面的 SQL 语句将查找距离 37, -122 坐标 25 英里半径范围内最近的 20 个位置。它根据该行的纬度/经度和目标纬度/经度计算距离,然后仅询问距离值小于 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 < 28 
ORDER BY distance LIMIT 0, 20;

这个是求28英里以内的经纬度。

另一种方法是在 28 到 29 英里的距离内找到它们:

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 < 29 and distance > 28 
ORDER BY distance LIMIT 0, 20;

https://developers.google.com/maps/articles/phpsqlsearch_v3#creating-the-map

【讨论】:

  • 应该是HAVING distance &lt; 25,因为我们正在查询半径25英里内的位置吗?
  • 应该 > 25 ,然后搜索所有记录
  • 你确定@vidurpunj 关于 > 25 吗?
  • 我尝试使用 sql 查询:距离
  • 37,-122坐标,这个纬度和经度是我们需要从哪个位置求距离的位置吗?
【解决方案3】:

这是我用 PHP 实现的完整解决方案。

此解决方案使用 http://www.scribd.com/doc/2569355/Geo-Distance-Search-with-MySQL 中提供的 Haversine 公式。

应该注意的是,Haversine 公式在极点附近存在弱点。 This answer 展示了如何实现 vincenty Great Circle Distance formula 来解决这个问题,但是我选择只使用 Haversine,因为它足以满足我的目的。

我将纬度存储为 DECIMAL(10,8),将经度存储为 DECIMAL(11,8)。希望这会有所帮助!

showClosest.php

<?PHP
/**
 * Use the Haversine Formula to display the 100 closest matches to $origLat, $origLon
 * Only search the MySQL table $tableName for matches within a 10 mile ($dist) radius.
 */
include("./assets/db/db.php"); // Include database connection function
$db = new database(); // Initiate a new MySQL connection
$tableName = "db.table";
$origLat = 42.1365;
$origLon = -71.7559;
$dist = 10; // This is the maximum distance (in miles) away from $origLat, $origLon in which to search
$query = "SELECT name, latitude, longitude, 3956 * 2 * 
          ASIN(SQRT( POWER(SIN(($origLat - latitude)*pi()/180/2),2)
          +COS($origLat*pi()/180 )*COS(latitude*pi()/180)
          *POWER(SIN(($origLon-longitude)*pi()/180/2),2))) 
          as distance FROM $tableName WHERE 
          longitude between ($origLon-$dist/cos(radians($origLat))*69) 
          and ($origLon+$dist/cos(radians($origLat))*69) 
          and latitude between ($origLat-($dist/69)) 
          and ($origLat+($dist/69)) 
          having distance < $dist ORDER BY distance limit 100"; 
$result = mysql_query($query) or die(mysql_error());
while($row = mysql_fetch_assoc($result)) {
    echo $row['name']." > ".$row['distance']."<BR>";
}
mysql_close($db);
?>

./assets/db/db.php

<?PHP
/**
 * Class to initiate a new MySQL connection based on $dbInfo settings found in dbSettings.php
 *
 * @example $db = new database(); // Initiate a new database connection
 * @example mysql_close($db); // close the connection
 */
class database{
    protected $databaseLink;
    function __construct(){
        include "dbSettings.php";
        $this->database = $dbInfo['host'];
        $this->mysql_user = $dbInfo['user'];
        $this->mysql_pass = $dbInfo['pass'];
        $this->openConnection();
        return $this->get_link();
    }
    function openConnection(){
    $this->databaseLink = mysql_connect($this->database, $this->mysql_user, $this->mysql_pass);
    }

    function get_link(){
    return $this->databaseLink;
    }
}
?>

./assets/db/dbSettings.php

<?php
$dbInfo = array(
    'host'      => "localhost",
    'user'      => "root",
    'pass'      => "password"
);
?>

按照上面发布的“Geo-Distance-Search-with-MySQL”文章的建议,可以通过使用 MySQL 存储过程来提高性能。

我有一个约 17,000 个地点的数据库,查询执行时间为 0.054 秒。

【讨论】:

  • 如何获得以公里或米为单位的距离?问候!
  • 警告。优秀的解决方案,但它有一个错误。应删除所有abs。从度数转换为弧度时,无需采用 abs 值,即使您这样做了,也只是对其中一个纬度进行了转换。请编辑它以修复错误。
  • 对于任何想要以米为单位的人:将 3956 英里转换为公里:地球的半径;将 69 英里转换为公里:1 度纬度的近似长度,单位为公里;并以公里为单位输入距离。
  • 应该能够将39566371 交换以得到以千米为单位的结果,因为这是以千米为单位的地球半径。
  • 并将69 替换为111,044736(在上面的评论中忘记了)
【解决方案4】:

以防万一您像我一样懒惰,这是一个从这个和其他关于 SO 的答案合并的解决方案。

set @orig_lat=37.46; 
set @orig_long=-122.25; 
set @bounding_distance=1;

SELECT
*
,((ACOS(SIN(@orig_lat * PI() / 180) * SIN(`lat` * PI() / 180) + COS(@orig_lat * PI() / 180) * COS(`lat` * PI() / 180) * COS((@orig_long - `long`) * PI() / 180)) * 180 / PI()) * 60 * 1.1515) AS `distance` 
FROM `cities` 
WHERE
(
  `lat` BETWEEN (@orig_lat - @bounding_distance) AND (@orig_lat + @bounding_distance)
  AND `long` BETWEEN (@orig_long - @bounding_distance) AND (@orig_long + @bounding_distance)
)
ORDER BY `distance` ASC
limit 25;

【讨论】:

  • bounding_distance 究竟代表什么?这个值是否将结果限制为一英里?那么在这种情况下,它将在 1 英里内返回结果?
  • @bounding_distance 在这里以度为单位,用于通过限制有效搜索区域来加速计算。例如,如果您知道您的用户在某个城市,并且您知道在该城市中有几个点,您可以安全地将边界距离设置为几度。
  • 这是使用哪个地理距离公式?
  • WHERE 中使用了“边界框”,以便可以使用INDEX(lat, lng), INDEX(lng, lat),但它不是最理想的,因为@bounding_distance 需要通过除以@987654326 来调整@.
【解决方案5】:

这个问题的原始答案很好,但新版本的 mysql(MySQL 5.7.6 上)支持地理查询,因此您现在可以使用内置功能而不是执行复杂的查询。

您现在可以执行以下操作:

select *, ST_Distance_Sphere( point ('input_longitude', 'input_latitude'), 
                              point(longitude, latitude)) * .000621371192 
          as `distance_in_miles` 
  from `TableName`
having `distance_in_miles` <= 'input_max_distance'
 order by `distance_in_miles` asc

结果在meters 中返回。因此,如果您想使用KM,只需使用.001 而不是.000621371192(用于里程)。

MySql docs are here

【讨论】:

  • 如果可能,请在答案中添加mysql版本。
  • ST_Distance_Sphere 在我的主机安装中不存在 (mysql Ver 15.1 Distrib 10.2.23-MariaDB)。我在某处阅读以替代ST_Distance,但距离很远。
  • @ashleedawg - 从版本来看,我认为您使用的是 MariaDB,它是 mysql 的一个分支。从this conversation 看来,MariaDB 似乎还没有实现ST_Distance_Sphere
  • 这个答案应该更高,因为它是唯一使用“新”功能并产生更好查询的答案。虽然老实说,我对默认为英里而不是公制系统的答案数量感到困惑;至少这个费心展示如何在两者之间切换。
【解决方案6】:

简单;)

SELECT * FROM `WAYPOINTS` W ORDER BY
ABS(ABS(W.`LATITUDE`-53.63) +
ABS(W.`LONGITUDE`-9.9)) ASC LIMIT 30;

只需将坐标替换为您需要的坐标即可。这些值必须存储为双精度值。这是一个有效的 MySQL 5.x 示例。

干杯

【讨论】:

  • 不知道为什么要投票,OP想要限制并按一定距离排序,而不是限制30并按dx+dy排序
  • 这是为我做的。这不是OP想要的,但这是我想要的,所以谢谢你的回答! :)
  • 最外层的 ABS() 就足够了。
【解决方案7】:

试试这个,它会显示离所提供坐标最近的点(50 公里内)。效果很好:

SELECT m.name,
    m.lat, m.lon,
    p.distance_unit
             * DEGREES(ACOS(COS(RADIANS(p.latpoint))
             * COS(RADIANS(m.lat))
             * COS(RADIANS(p.longpoint) - RADIANS(m.lon))
             + SIN(RADIANS(p.latpoint))
             * SIN(RADIANS(m.lat)))) AS distance_in_km
FROM <table_name> AS m
JOIN (
      SELECT <userLat> AS latpoint, <userLon> AS longpoint,
             50.0 AS radius, 111.045 AS distance_unit
     ) AS p ON 1=1
WHERE m.lat
BETWEEN p.latpoint  - (p.radius / p.distance_unit)
    AND p.latpoint  + (p.radius / p.distance_unit)
    AND m.lon BETWEEN p.longpoint - (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
    AND p.longpoint + (p.radius / (p.distance_unit * COS(RADIANS(p.latpoint))))
ORDER BY distance_in_km

只需更改&lt;table_name&gt;&lt;userLat&gt;&lt;userLon&gt;

您可以在此处阅读有关此解决方案的更多信息:http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/

【讨论】:

    【解决方案8】:

    您正在寻找像haversine formula 这样的东西。参见here

    还有其他的,但这是最常被引用的。

    如果您正在寻找更强大的功能,您可能需要查看您的数据库 GIS 功能。他们能够做一些很酷的事情,比如告诉你一个点(城市)是否出现在给定的多边形(地区、国家、大陆)内。

    【讨论】:

    【解决方案9】:

    根据文章Geo-Distance-Search-with-MySQL查看此代码:

    示例:查找距离我当前位置 10 英里半径范围内最近的 10 家酒店:

    #Please notice that (lat,lng) values mustn't be negatives to perform all calculations
    
    set @my_lat=34.6087674878572; 
    set @my_lng=58.3783670308302;
    set @dist=10; #10 miles radius
    
    SELECT dest.id, dest.lat, dest.lng,  3956 * 2 * ASIN(SQRT(POWER(SIN((@my_lat -abs(dest.lat)) * pi()/180 / 2),2) + COS(@my_lat * pi()/180 ) * COS(abs(dest.lat) *  pi()/180) * POWER(SIN((@my_lng - abs(dest.lng)) *  pi()/180 / 2), 2))
    ) as distance
    FROM hotel as dest
    having distance < @dist
    ORDER BY distance limit 10;
    
    #Also notice that distance are expressed in terms of radius.
    

    【讨论】:

      【解决方案10】:

      查找离我最近的用户:

      以米为单位的距离

      基于Vincenty's formula

      我有用户表:

      +----+-----------------------+---------+--------------+---------------+
      | id | email                 | name    | location_lat | location_long |
      +----+-----------------------+---------+--------------+---------------+
      | 13 | xxxxxx@xxxxxxxxxx.com | Isaac   | 17.2675625   | -97.6802361   |
      | 14 | xxxx@xxxxxxx.com.mx   | Monse   | 19.392702    | -99.172596    |
      +----+-----------------------+---------+--------------+---------------+
      

      sql:

      -- my location:  lat   19.391124   -99.165660
      SELECT 
      (ATAN(
          SQRT(
              POW(COS(RADIANS(users.location_lat)) * SIN(RADIANS(users.location_long) - RADIANS(-99.165660)), 2) +
              POW(COS(RADIANS(19.391124)) * SIN(RADIANS(users.location_lat)) - 
             SIN(RADIANS(19.391124)) * cos(RADIANS(users.location_lat)) * cos(RADIANS(users.location_long) - RADIANS(-99.165660)), 2)
          )
          ,
          SIN(RADIANS(19.391124)) * 
          SIN(RADIANS(users.location_lat)) + 
          COS(RADIANS(19.391124)) * 
          COS(RADIANS(users.location_lat)) * 
          COS(RADIANS(users.location_long) - RADIANS(-99.165660))
       ) * 6371000) as distance,
      users.id
      FROM users
      ORDER BY distance ASC
      

      地球半径:6371000(米)

      【讨论】:

        【解决方案11】:
        simpledb.execSQL("CREATE TABLE IF NOT EXISTS " + tablename + "(id INTEGER PRIMARY KEY   AUTOINCREMENT,lat double,lng double,address varchar)");
                    simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2891001','70.780154','craftbox');");
                    simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2901396','70.7782428','kotecha');");//22.2904718 //70.7783906
                    simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2863155','70.772108','kkv Hall');");
                    simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.275993','70.778076','nana mava');");
                    simpledb.execSQL("insert into '" + tablename + "'(lat,lng,address)values('22.2667148','70.7609386','Govani boys hostal');");
        
        
            double curentlat=22.2667258;  //22.2677258
            double curentlong=70.76096826;//70.76096826
        
            double curentlat1=curentlat+0.0010000;
            double curentlat2=curentlat-0.0010000;
        
            double curentlong1=curentlong+0.0010000;
            double curentlong2=curentlong-0.0010000;
        
            try{
        
                Cursor c=simpledb.rawQuery("select * from '"+tablename+"' where (lat BETWEEN '"+curentlat2+"' and '"+curentlat1+"') or (lng BETWEEN         '"+curentlong2+"' and '"+curentlong1+"')",null);
        
                Log.d("SQL ", c.toString());
                if(c.getCount()>0)
                {
                    while (c.moveToNext())
                    {
                        double d=c.getDouble(1);
                        double d1=c.getDouble(2);
        
                    }
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
        

        【讨论】:

          【解决方案12】:

          听起来您想在距离上进行一些限制的最近邻居搜索。据我所知,SQL 不支持这样的任何东西,您需要使用另一种数据结构,例如 R-treekd-tree

          【讨论】:

            【解决方案13】:

            此处为 MS SQL 版本:

                    DECLARE @SLAT AS FLOAT
                    DECLARE @SLON AS FLOAT
            
                    SET @SLAT = 38.150785
                    SET @SLON = 27.360249
            
                    SELECT TOP 10 [LATITUDE], [LONGITUDE], SQRT(
                        POWER(69.1 * ([LATITUDE] - @SLAT), 2) +
                        POWER(69.1 * (@SLON - [LONGITUDE]) * COS([LATITUDE] / 57.3), 2)) AS distance
                    FROM [TABLE] ORDER BY 3
            

            【讨论】:

              【解决方案14】:
               +----+-----------------------+---------+--------------+---------------+
              | id | email                 | name    | location_lat | location_long |
              +----+-----------------------+---------+--------------+---------------+
              | 7  | test@gmail.com        | rembo   | 23.0249256   |  72.5269697   |
              | 25 | test1@gmail.com.      | Rajnis  | 23.0233221    | 72.5342112   |
              +----+-----------------------+---------+--------------+---------------+
              

              $lat = 23.02350629;

              $long = 72.53230239;

              数据库:: 选择 (" 选择 * 从 ( 选择 , ( ( ( acos( sin(( “.$ lat .” * pi() / 180)) * sin(( lat * pi() / 180)) + cos(( “.$ lat .” pi() / 180 )) * cos(( lat * pi() / 180)) * cos((( ".$ long ." - LONG) * pi() / 180))) ) * 180 / pi() ) * 60 * 1.1515 * 1.609344 ) 作为距离 从 users ) 用户 在哪里 距离

              【讨论】:

              • 您可以格式化查询以提高可读性。此外,粘贴代码时使用适当的标签,以便正确显示。
              • 如果您也能解释一下这里发生了什么,那就太好了。就像这只是一个复制粘贴的答案
              【解决方案15】:

              听起来您应该只使用 PostGIS、SpatialLite、SQLServer2008 或 Oracle Spatial。他们都可以使用空间 SQL 为您回答这个问题。

              【讨论】:

              • 听起来你不应该建议人们切换他们的整个数据库平台,并在我明确搜索“Oracle”时导致不相关的结果出现在我的谷歌搜索中......
              【解决方案16】:

              在极端情况下,这种方法会失败,但为了提高性能,我跳过了三角函数并简单地计算了对角线平方。

              【讨论】:

                【解决方案17】:

                Mysql查询带有距离限制和where条件的搜索坐标

                 SELECT id, ( 3959 * acos( cos( radians('28.5850154') ) * cos( radians(latitude) ) * cos( radians( longitude ) - radians('77.07207489999999') ) + sin( radians('28.5850154') ) * sin( radians( latitude ) ) ) ) AS distance FROM `vendors` HAVING distance < 5;
                

                【讨论】:

                  【解决方案18】:

                  这个问题一点也不难,但是如果你需要优化它就会变得更加复杂。

                  我的意思是,您的数据库中有 100 个位置还是 1 亿个位置?它有很大的不同。

                  如果位置的数量很少,只需执行以下操作即可将它们从 SQL 中取出并放入代码中 ->

                  Select * from Location
                  

                  将它们写入代码后,使用 Haversine 公式计算每个纬度/经度与原始数据之间的距离并对其进行排序。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2019-05-28
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多