哪种方法可以更快地使用 PHP/Laravel 从 MySQL/MariaDB 获取所有 POI
Posted
技术标签:
【中文标题】哪种方法可以更快地使用 PHP/Laravel 从 MySQL/MariaDB 获取所有 POI【英文标题】:Which approach is faster for getting all POIs from MySQL/MariaDB with PHP/Laravel 【发布时间】:2018-12-28 00:36:33 【问题描述】:如果我错了,请纠正我。
用户在我的网站上创建了三种获取最近房屋的方法:
-
要创建一个包含两列(纬度、经度)的表,它们都是浮动的,然后说:
这里是:
$latitude = 50;
$longitude = 60;
SELECT * FROM my_table
WHERE (latitude <= $latitude+10 AND latitude >= $latitude-10)
AND (longitude <= $longitude+10 AND longitude >= $longitude-10)
这里的 10 表示例如 1 公里。
在这种方法中,我们还可以使用 harvesine 公式。
将这些列(纬度、经度)合并到一列名为点的点为 POINT 类型,然后再次逐行搜索。
要将多个点(用户创建的房屋坐标)分类为一个国家(即城市)的一个部分的类别,如果查询带有 $latitude 和 $longitude 以查看最近的房屋,我会检查它们存储在哪个类别中,以免搜索所有行,而仅搜索此查询(坐标)所属的部分。
我猜方法 1 很慢,因为表格的每一行的条件,如果我使用 harvesine 公式,也会很慢。
如果我使用 ST_Distance,它似乎又变慢了,因为它只是有很多计算。
但是,如果我使用方法 3,检查每个部分的特定点用户似乎比检查所有行更快。我知道如何为每个家设置点,但我不知道如何在另一个表中创建多个家位置作为一个部分。
顺便说一句,新版本的 mysql 和 MariaDB 空间索引在 InnoDB 中受支持。
我的问题:
方法 1 真的很慢吗,或者其他 ST_* 函数是否与此方法相同,以使用其中提到的那些公式一一检查所有行?哪个更快?
除了简单的条件之外,方法 2 是否可以加快速度?我的意思是当使用 POINT 类型而不是 float 并使用 ST_* 函数而不是自己做时,它是否会做出任何改变?我想知道算法是否不同。
如果方法 3 是这三种方法中最快的,我如何对点进行分类以避免搜索表中的所有行?
如何使用空间索引使其尽可能快?
如果存在任何其他方法并且我没有提及,您能否告诉我如何仅通过 php/Laravel 中的 MySQL/MariaDB 中的坐标获得最近的房屋?
谢谢大家
【问题讨论】:
查看我回答的最后一部分here 数据集中有多少个“家”?世界上有几十亿,但我怀疑是否有关于其中大多数的数据。 @RickJames 现在可能有 2、3 千,但它正在增长,可能会达到数百万。 @PaulSpiegel 您的链接很好,但我在这里又遇到了一些问题:1. 如果我在我的家表中只使用空间索引,这是否会帮助我没有第三个城市表,每个城市都有边界被搜索? 2. 你说的是 MBRWithin 或 MBRContains,但是 st_contains、st_within 和 st_distance_sphere 呢? 3. 你说'在循环中增加多边形的大小,直到它包含至少 5 个位置。但是如果数百次都找不到怎么办?循环不会对性能产生不良影响吗? 4. 按 st_distance_sphere 搜索最近的不是一个好主意吗?还不够快吗? @kodfire 仅使用 st_distance 函数需要检查所有“数百万”行。 那个太贵了。正如我在回答中所讨论的那样,增加边界框(非空间)或多边形(空间)将保持努力。 【参考方案1】:边界框和Haversine
在您的简短SELECT
中,您使用的是“边界框”方法,其中在地图上绘制了一个粗略的正方形。但是,它有几个缺陷。
cos()
来解决此问题。
拥有这些有助于边界框,它会显着过滤行,然后可选的半正弦测试会围绕测试的范围。
INDEX(latitude)
INDEX(longitude)
这种方法具有“中等”性能——一个索引将与边界框一起使用,从而快速将候选者限制在围绕边界的东西(或南北)条纹地球。但这可能仍然是很多候选人。
通过过滤掉大部分行,Haversine 调用的数量还不错;不用担心函数的性能。
如果您有 100 万个房屋,则包含 5 个房屋(加上一些未通过半正弦检查的房屋)的最终边界框可能会涉及数千行——因为只使用了两个索引之一。这仍然比获取所有百万行并使用距离函数检查每一行要好得多。
POINT 和 SPATIAL 索引
切换到POINT
需要切换到SPATIAL
索引。在这种模式下,ST_Distance_Sphere()
可用,而不是 hasrsine。 (注意:该功能仅存在于最近的版本中。)
通过过滤掉大部分行,对ST_Distance
或ST_Distance_Sphere
的调用次数还不错;不用担心函数的性能。
SPATIAL
搜索使用 R-Trees。我对他们在您的查询中的表现没有很好的感觉。
方法 3
从另一个分类点开始,你增加了复杂性。您还需要检查相邻区域以查看附近是否有点。如果没有更多细节,我无法判断相对表现。
我的方法
我有一些复杂的代码可以扩展到任意多个点。由于您的数据集可能足够小,可以缓存在 RAM 中,因此对您来说可能有点过头了。 http://mysql.rjweb.org/doc.php/latlng
对于只有 100 万个家庭,上面的这对索引可能“足够好”,因此您无需求助于“我的算法”。我的算法将只触及大约 20 行以获得所需的 5 行——无论总行数如何。
其他说明
如果同时存储 lat/lng 和 POINT
,表格会很庞大;如果尝试混合边界框和ST
函数,请记住这一点。
【讨论】:
我根据可能拥有一百万个家庭进行了几次更新。【参考方案2】:您使用哪个公式计算距离并不重要。更重要的是您必须阅读、处理和排序的行数。在最好的情况下,您可以在 WHERE 子句中使用条件索引来限制处理的行数。您可以尝试对您的位置进行分类 - 但这取决于您的数据的性质,如果这会运作良好。您还需要找出要使用的“类别”。更通用的解决方案是使用 SPATIAL INDEX 和 ST_Within() 函数。
现在让我们运行一些测试..
在我的数据库(MySQL 5.7.18)中,我有下表:
CREATE TABLE `cities` (
`cityId` MEDIUMINT(9) UNSIGNED NOT NULL AUTO_INCREMENT,
`country` CHAR(2) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`city` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`accentCity` VARCHAR(100) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`region` CHAR(2) NULL DEFAULT NULL COLLATE 'utf8mb4_unicode_ci',
`population` INT(10) UNSIGNED NULL DEFAULT NULL,
`latitude` DECIMAL(10,7) NOT NULL,
`longitude` DECIMAL(10,7) NOT NULL,
`geoPoint` POINT NOT NULL,
PRIMARY KEY (`cityId`),
SPATIAL INDEX `geoPoint` (`geoPoint`)
) COLLATE='utf8mb4_unicode_ci' ENGINE=InnoDB
数据来自Free World Cities Database,包含 3173958 (3.1M) 行。
注意geoPoint
是多余的,等于POINT(longitude, latitude)
。
考虑到用户位于伦敦的某个地方
set @lon = 0.0;
set @lat = 51.5;
并且您想从cities
表中找到最近的位置。
一个“微不足道”的查询是
select c.cityId, c.accentCity, st_distance_sphere(c.geoPoint, point(@lon, @lat)) as dist
from cities c
order by dist
limit 1
结果是
988204 Blackwall 1085.8212159861014
执行时间:~ 4.970 秒
如果您使用不太复杂的函数ST_Distance()
,您将获得相同的结果,执行时间约为 4.580 秒 - 差别不大。
请注意,您不需要在表格中存储地理点。您也可以使用(point(c.longitude, c.latitude)
而不是c.geoPoint
。令我惊讶的是,它甚至更快(ST_Distance
约为 3.6 秒,ST_Distance_Sphere
约为 4.0 秒)。如果我根本没有geoPoint
列,它可能会更快。但这仍然无关紧要,因为您不希望用户等待,所以如果您可以做得更好,请登录以获得响应。
现在让我们看看如何将 SPATIAL INDEX 与ST_Within()
一起使用。
您需要定义一个包含最近位置的多边形。一个简单的方法是使用 ST_Buffer(),它会生成一个有 32 个点的多边形,几乎是一个圆*。
set @point = point(@lon, @lat);
set @radius = 0.1;
set @polygon = ST_Buffer(@point, @radius);
select c.cityId, c.accentCity, st_distance_sphere(c.geoPoint, point(@lon, @lat)) as dist
from cities c
where st_within(c.geoPoint, @polygon)
order by dist
limit 1
结果是一样的。执行时间约为 0.000 秒(这是我的客户 (HeidiSQL) 所说的)。
* 请注意,@radius
以度数表示,因此多边形将更像椭圆而不是圆形。但是在我的测试中,我总是得到与简单而缓慢的解决方案相同的结果。在我在生产代码中使用它之前,我会调查更多的边缘情况。
现在您需要为您的应用程序/数据找到最佳半径。如果它太小 - 你可能得不到任何结果,或者错过最近的点。如果它太大 - 您可能需要处理太多行。
这里是给定测试用例的一些数字:
@radius = 0.001:无结果 @radius = 0.01:恰好一个位置(有点幸运)- 执行时间 ~ 0.000 秒 @radius = 0.1:55 个位置 - 执行时间 ~ 0.000 秒 @radius = 1.0:2183 个位置 - 执行时间 ~ 0.030 秒【讨论】:
1.什么是“988204 Blackwall 1085.8212159861014”? 2. 你能告诉我索引是如何工作的吗?据我了解,它就像数组索引一样,它不会计算某个列是否大于某个值然后显示这个但它直接从我们想要的点获取行。但是当 POINT 类型的列中有大量数据时,如何在需要计算 I 和最近点之间的情况下直接得到呢? 3. 您已经为 geoPoint 使用了 SPATIAL INDEX,但是如果我们对lat
ant lon
列都有简单的索引呢?
4.通过这些结果,我得到了仅仅通过使用 SPATIAL INDEX 它变得更快,对吧? 5. 如果我们不使用 ST_BUFFER 并使用来自移动设备的四个点 (x1, y1, x2, y2) 作为多边形呢?
6.最后这个场景足够快:我们有一个名为sections
的表。在此表中,我们有 id
、cat_id
和 geopoint
。当用户首先提交他的家时,我们会得到他的家在哪个部分,例如它在您提到的地方(伦敦),例如伦敦属于第三类所以cat_id
是 3 所以在homes
表中我们有id
、cat_id
和lat
和lon
(正如你提到的它比geoPoint
快),当用户想要找到最近的家时,他会提供他当前的点(纬度,经度)和 sql首先会在sections
表中找到他属于哪个类别,然后会获取
homes
cat_id
表中的房屋,以免搜索所有记录。所以我。这种方法够快吗?二、如果是,哪些应该获得索引属性?如果可能的话,这应该有什么改变才能更快?谢谢:)
@lmao 询问距离单位here。 ST_Distance_Sphere
返回以米为单位的结果。 ST_Distance
返回与用于点的单位相同的结果。在纬度和经度的情况下,它是度数。见demo。以上是关于哪种方法可以更快地使用 PHP/Laravel 从 MySQL/MariaDB 获取所有 POI的主要内容,如果未能解决你的问题,请参考以下文章