我有一个MySQL表(MyISAM),包含我选择的大约200k个纬度/长对条目,基于来自另一个纬度/长对的对距离(大圆公式)。 (例如,半径10公里范围内的所有条目,大约在50.281852,2.504883)
我的问题是此查询大约需要0.28秒。只运行那些200k条目(每天继续获得更多)。虽然0,28秒。通常很好,这个查询经常运行,因为它支持我的web-app的主要功能,并且通常它是更大查询的一部分。
有没有办法加快速度?显而易见,MySQL必须每次都运行所有200k条目,并为每个条目执行大圆公式。我在stackoverflow上读到了关于geohashing,R-Trees之类的东西,但我认为这不是我想要的方式。部分是因为我从未成为数学的忠实粉丝,但主要是因为我认为这个问题已经由比我更聪明的人在图书馆/扩展/等中解决了。经过广泛测试并定期更新。
MySQL似乎有空间扩展,但是它没有提供距离功能。我应该查看另一个数据库来放置这个坐标对吗? PostgreSQL似乎有一个相当成熟的Spatial扩展。你对此有所了解吗?或者PostgreSQL也只是使用大圆公式来获取某个区域内的所有条目?
是否有专门的独立产品或mysql-extension已经完成了我正在寻找的东西?
或者是否有可以用来进行计算的PHP库?使用APC我可以很容易地将lat-long对装入内存(那些200k条目大约需要5MB),然后在PHP内部运行查询。然而,这种方法的问题是,我有一个MySQL查询,如SELECT .. FROM .. WHERE id in(id1,id2,..),所有结果都可以达到几千。 MySQL如何处理像这样的查询?然后(因为这是一个数字运算任务)在PHP中这样做会足够快吗?
任何其他想法我应该/不应该做什么?
对于completenes,这里是示例查询,删除了任何不相关的部分(正如我所说的,通常这是我加入多个表的更大查询的一部分):
SELECT id, 6371 * acos( sin( radians( 52.4042924 ) ) * sin( radians( lat ) ) + cos( radians( 50.281852 ) ) * cos( radians( lat ) ) * cos( radians( 2.504883 ) - radians( lon ) ) ) AS dst
FROM geoloc
HAVING dst <10
ORDER BY dst ASC
谢谢!
答案 0 :(得分:15)
如果从不同的角度解决问题该怎么办?
直线10公里是:
使用此作为基础,进行一些快速数学运算并在查询中添加WHERE
子句,删除通过添加缓冲区创建的“框”之外的任何位置,假设为1'拉特&amp; 6'长
使用此图片:
您可以找到最小/最大纬度/经度
2a上。最低纬度 - 34.1927777778,-85.0169444444
2B。最低经度 - 34.2094444444,-85.1169444444
2c中。最大纬度 - 34.2261111111,-85.0169444444
2d上。最大经度 - 34.2094444444,-84.9169444444
使用每个方向的最小值和最大值运行查询
SELECT *
FROM geoloc
WHERE
lat >= 34.1927777 AND
lat <= 34.2261111 AND
long >= -85.1169444 AND
long <= -84.9169444;
您可以将距离计算与SQL查询集成,也可以在拉取数据后使用PHP库/类来运行距离检查。无论哪种方式,您都将计算次数减少了很多。
我使用以下功能计算两个US84 GPS位置之间的距离。传递两个参数,每个参数是一个数组,第一个元素是纬度,第二个元素是经度。我相信它具有几英尺的精度,除了最坚固的核心GPS-ophiles之外,它应该足够了。另外,我相信这会使用Haversine距离公式。
$ distance = calculateGPSDistance(array(34.32343,-86.342343),array(34.433223,-96.0032344));
function calculateGPSDistance($site1, $site2)
{
$distance = 0;
$earthMeanRadius = 2.0891 * pow(10, 7);
$deltaLatitude = deg2rad($site2[0] - $site1[0]);
$deltaLongitude = deg2rad($site2[1] - $site1[1]);
$a = sin($deltaLatitude / 2) * sin($deltaLatitude / 2) + cos(deg2rad($site1[0])) *
cos(deg2rad($site2[0])) * sin($deltaLongitude / 2) * sin($deltaLongitude / 2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
$distance = $earthMeanRadius * $c;
return $distance;
}
<强>更新强>
我忘了提一下,我的距离函数会以英尺为单位返回距离。
答案 1 :(得分:15)
计算一个边界框以选择SQL查询的WHERE子句中的行子集,这样您只需对该行子集执行昂贵的距离计算,而不是对表中的整个200k记录执行。该方法在此article on Movable Type中描述(使用PHP代码示例)。然后,您可以在针对该子集的查询中包含Haversine计算,以计算实际距离,并在该点计算HAVING子句。
这是帮助您提高性能的边界框,因为这意味着您只需对数据的一小部分进行昂贵的距离计算。这实际上与Patrick建议的方法相同,但Movable Type链接对该方法有广泛的解释,以及可用于构建边界框和SQL查询的PHP代码。
修改强>
如果你认为hasrsine不够准确,那么还有Vincenty公式。
// Vincenty formula to calculate great circle distance between 2 locations expressed as Lat/Long in KM
function VincentyDistance($lat1,$lat2,$lon1,$lon2){
$a = 6378137 - 21 * sin($lat1);
$b = 6356752.3142;
$f = 1/298.257223563;
$p1_lat = $lat1/57.29577951;
$p2_lat = $lat2/57.29577951;
$p1_lon = $lon1/57.29577951;
$p2_lon = $lon2/57.29577951;
$L = $p2_lon - $p1_lon;
$U1 = atan((1-$f) * tan($p1_lat));
$U2 = atan((1-$f) * tan($p2_lat));
$sinU1 = sin($U1);
$cosU1 = cos($U1);
$sinU2 = sin($U2);
$cosU2 = cos($U2);
$lambda = $L;
$lambdaP = 2*M_PI;
$iterLimit = 20;
while(abs($lambda-$lambdaP) > 1e-12 && $iterLimit>0) {
$sinLambda = sin($lambda);
$cosLambda = cos($lambda);
$sinSigma = sqrt(($cosU2*$sinLambda) * ($cosU2*$sinLambda) + ($cosU1*$sinU2-$sinU1*$cosU2*$cosLambda) * ($cosU1*$sinU2-$sinU1*$cosU2*$cosLambda));
//if ($sinSigma==0){return 0;} // co-incident points
$cosSigma = $sinU1*$sinU2 + $cosU1*$cosU2*$cosLambda;
$sigma = atan2($sinSigma, $cosSigma);
$alpha = asin($cosU1 * $cosU2 * $sinLambda / $sinSigma);
$cosSqAlpha = cos($alpha) * cos($alpha);
$cos2SigmaM = $cosSigma - 2*$sinU1*$sinU2/$cosSqAlpha;
$C = $f/16*$cosSqAlpha*(4+$f*(4-3*$cosSqAlpha));
$lambdaP = $lambda;
$lambda = $L + (1-$C) * $f * sin($alpha) * ($sigma + $C*$sinSigma*($cos2SigmaM+$C*$cosSigma*(-1+2*$cos2SigmaM*$cos2SigmaM)));
}
$uSq = $cosSqAlpha*($a*$a-$b*$b)/($b*$b);
$A = 1 + $uSq/16384*(4096+$uSq*(-768+$uSq*(320-175*$uSq)));
$B = $uSq/1024 * (256+$uSq*(-128+$uSq*(74-47*$uSq)));
$deltaSigma = $B*$sinSigma*($cos2SigmaM+$B/4*($cosSigma*(-1+2*$cos2SigmaM*$cos2SigmaM)- $B/6*$cos2SigmaM*(-3+4*$sinSigma*$sinSigma)*(-3+4*$cos2SigmaM*$cos2SigmaM)));
$s = $b*$A*($sigma-$deltaSigma);
return $s/1000;
}
echo VincentyDistance($lat1,$lat2,$lon1,$lon2);
答案 2 :(得分:0)
你可以尝试一个四核。这是一个空间索引,减少了维度。它将地图细分为图块,但您可以使用它来存储点。你可以下载我的php类hilbert-curve @ phpclasses.org。它还包括z曲线和摩尔曲线。重要的是知道它使用墨卡托投影。您可以查找Bing地图平铺。它解释了如何使用四核。您需要x,y坐标和z(缩放或深度)值。然后它会给你一个四核。
答案 3 :(得分:0)
到目前为止,我所做的就像上面描述的@Mark一样。对于小型网站我认为是一个可行的解决方案,但对我的情况来说并不是那么好(200k条目位于一个给定点周围的100x100平方公里的盒子里面。我使用的是Mark的相同技巧,但性能太差了.5个用户/第二次查询附近的lat / lon点几个小时,查询开始需要10到15秒;这发生在之后我调整了my.cnf中的mySQL设置。甚至不想想想当全世界有200万个参赛作品会发生什么。
所以,现在是第2步的时间:Hilbert curve。 它应该通过在一列(hilbert_number)上仅使用一个索引来解决(lat,lon)列上的B树索引的问题,这是浪费的(在范围内扫描,正在使用B树索引的一部分)。 hilbert_number是根据Hilbert曲线上的点的纬度/经度坐标计算的数字。
但第二个问题是,测试固定点与之前结果子集到Haversine公式之间的距离仍然存在。那部分可能很慢。因此,我正在考虑以某种方式更直接地测试距离,将所有内容放在希尔伯特曲线上,并将一些位掩码应用于该结果子集,而不是应用Haversine公式。我只是不知道我该怎么做......
无论如何,我用来减少结果子集中点数的另一个技巧是使用两个边界框,并在子集中仅包含灰色/白色点以进行进一步的Haversine测试:
我现在需要做的是切换到希尔伯特数,看看它的行为方式。但我怀疑这会增加10倍的性能!