使用空间分析函数和数据类型在 MySQL 中按距离排序
Posted
技术标签:
【中文标题】使用空间分析函数和数据类型在 MySQL 中按距离排序【英文标题】:Sorting by distance in MySQL with spatial analysis functions and data types 【发布时间】:2018-07-07 12:03:04 【问题描述】:我正在使用 Laravel 5.5 构建一个 php web 应用程序,我需要显示一个按与用户指定位置的距离排序的地点列表(例如商店)。 这些位置将存储在 mysql 数据库中,并应作为 Eloquent ORM 模型实例检索。
做一些研究,我发现了很多关于这个主题的帖子和问题(提出不同的解决方案),但是,对数据库和地理定位/地理空间分析的经验很少,他们大多让我感到困惑,我想知道什么方法可以解决遵循以及在这种情况下的最佳做法是什么。
我阅读的大多数答案都建议在 SQL 查询中使用 haversine formula 或 spherical law of cosines,这看起来像(取自 this answer 的示例):
$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))";
This post 指出,在短距离内,假设地球平坦并计算简单的欧几里得距离是一个很好的近似值,并且比使用半正弦公式更快。 由于我一次只需要对一个城市内的地点进行排序,这似乎是一个不错的解决方案。
但是,这些帖子和 SO 答案中的大多数都是几年前的,我想知道现在(MySQL 5.7)是否有更好的解决方案。
例如,这些帖子都没有使用任何 MySQL “空间分析函数”,例如 ST_Distance_Sphere
和 ST_Distance
,它们似乎正是为此目的。
是否有任何理由(例如性能、精度)不使用这些函数而不是在查询中编写公式? (我不知道这些函数内部使用的是哪种算法)
我也不知道应该如何存储每个地方的坐标。
我见过的大多数示例都假设坐标存储在单独的 lat
、lon
列中作为双精度或 FLOAT(10,6)
(如 this example by google),但 MySQL POINT
数据类型似乎也适合存储地理坐标。
这两种方法的优缺点是什么?
如何使用索引来加速这类查询?例如,我读过“spatial indexes”,但我认为它们只能用于限制结果,例如MBRContains()
,而不是实际按距离排序结果。
那么,我应该如何存储地点的坐标以及如何查询它们以按距离排序?
【问题讨论】:
如果可以避免,不要滚动自己的 GIS 功能,使用空间扩展。其中许多问题早于该扩展变得流行和/或存在的时间。如果您正在做大量 GIS 类型的工作,您可能会发现 MySQL 有点弱,而带有 PostGIS 的 Postgres 工作得更好。 出于兴趣,你有多少分?你只需要最接近的n吗?以我的经验,这种应用程序的方法在很大程度上取决于这些问题。为此,有很多索引和缓存技术。我在 5 毫秒内完成了 60000 多个属性搜索,但这些不是纯 MySQL 解决方案 - 所以我在这里帮不上忙 :) 【参考方案1】:除了 ST_Distance_Sphere,5.7 没有带来任何额外的东西。 (SPATIAL 已经实现。)
对于“数千”点,您拥有的代码可能是最好的。包括
INDEX(lat, lng),
INDEX(lng, lat)
除非您绵延数千英里 (kms),否则我不会担心地球的曲率。即使这样,代码和那个功能也应该足够好。
不要使用FLOAT(m,n)
,只使用FLOAT
。下面的链接给出了FLOAT
和其他表示形式的可用精度。
如果您的点太多以至于无法完全缓存表及其索引(数百万个点),则可以使用 this ,它使用了一些技巧来避免像上述解决方案那样的冗长扫描。由于PARTITION
的限制,lat/lng 表示为按比例缩放的整数。 (但这很容易在输入/输出中转换。)地球的曲率、极点和日期线都被处理了。
【讨论】:
我认为 GCDist 是正确的答案,所以支持 Rick。另一个相关问题是 GC 距离与行驶距离。例如,纽约州的诺斯波特和康涅狄格州的达里恩相距约 13 英里,但被长岛海峡隔开,行驶距离为 66 英里,包括穿过皇后区和布朗克斯区。它可能对您的要求有点过头了,但是使用 GC 距离作为屏幕,然后使用诸如 this 之类的地图工具将距离转换为行驶距离并重新排序会得到最好的结果。 @wordragon - 您可能可以通过 Google 的一些 API 获得“行驶距离”。但我怀疑它的扩展性不好,至少今年不会。接下来,您将需要“驾驶时间”。 Google 比我汽车的导航系统做得更好——因为它可以处理当前的交通状况。 我不是为了几千分而提议这样做的。在一个典型的“商店查找器”应用程序中寻找位置,比如 25 英里,您可以使用 GCDist 获取 25 英里内的位置(可能不是很多),然后计算结果的行驶距离,丢弃任何 > 25 英里,并对剩余部分进行分类。这种类型的大多数应用程序都会为您提供某种地图显示,因此您可能已经完成了一半的工作。 感谢瑞克的回答。我也根据发布此赏金后发现的工作解决方案发布了答案:)【参考方案2】:我使用的表格与我找到的邮政编码有关联。我使用haversine 公式来查找特定范围内的所有邮政编码。然后,我使用从该查询返回的邮政编码列表,并使用这些邮政编码查找所有业务。也许该解决方案对您有用。这很容易实现。只要您知道邮政编码,这也使您不必知道每个企业的经纬度。
【讨论】:
这可能是一个不错的解决方案,但 OP 声明“因为我一次只需要对一个城市内的地点进行排序”所以在我看来邮政编码赢了在这里帮不了大忙 我一定忽略了那部分。在这种情况下,他只需要按城市查询商店,可能会显着缩小结果范围,然后从那里运行半正弦公式。他必须知道每项业务的经纬度。【参考方案3】:使用ST_DISTANCE_SPHERE
或MBRContains
获取范围内的点或点之间的距离——比使用不能使用索引且不是为查询距离而构建的Haversine 公式要快得多,因为MySql 在范围查询方面很慢。参考mysql documentation.
Haversine 公式可能适用于小型应用程序,并且大多数旧答案都引用该解决方案,因为旧版本的 MySql innodb 没有空间索引。
大致的方法如下——下面是我在Java中的工作代码——希望你可以根据你的需要为PHP定制它
首先将传入的数据保存为数据库中的一个点(请注意坐标公式使用经度、纬度约定)
GeometryFactory factory = new GeometryFactory();
Point point = factory.createPoint(new Coordinate(officeDto.getLongitude(), officeDto.getLatitude()));//IMP:Longitude,Latitude
officeDb.setLocation(point);
创建空间索引在 mysql 中使用以下内容
在办公室(位置)上创建空间索引位置;
您可能会收到错误“空间索引的所有部分都必须不为空”。这是因为只有当字段不为空时才能创建空间索引 - 在这种情况下,将字段转换为非空
最后,从您的代码中调用自定义函数 ST_DISTANCE_SPHERE,如下所示。
SELECT st_distance_sphere( office.getLocation , project.getLocation)
as distance FROM ....
注意:office.getLocation 和 project.getLocation 都返回 POINT 类型。本机 SQL 方法如下文档中的
ST_Distance_Sphere(g1, g2 [, radius])
返回球体上两点和/或多点之间的最小球面距离,以米为单位,如果任何几何参数为 NULL 或为空,则返回 NULL。
【讨论】:
以上是关于使用空间分析函数和数据类型在 MySQL 中按距离排序的主要内容,如果未能解决你的问题,请参考以下文章