PHP/MySQL中的地理搜索(距离)(性能)

Dex*_*ter 12 php mysql gis performance distance

我有一个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扩展已经完成了我正在寻找的东西?

或者是否可以使用我可以用来进行计算的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
Run Code Online (Sandbox Code Playgroud)

谢谢!

Pat*_*ick 15

如果从不同的角度解决问题该怎么办?

直线10公里是:

  1. 在纬度上等于~1'(分钟)
  2. 经度等于~6'(分钟)

以此为基础,进行一些快速数学运算,并在查询中添加该WHERE子句,删除通过添加缓冲区创建的"框"之外的任何位置,假设为1'lat&6'long

gps缓冲区圈

使用此图片:

  1. 您要搜索的GPS位置(34°12'34.0", - 85°1'1.0")[34.2094444444,-85.0169444444]
  2. 您可以找到最小/最大纬度/经度

    2A.Min Latitude - 34.1927777778,-85.0169444444

    2B.Min Minitude - 34.2094444444,-85.1169444444

    2C.最大纬度 - 34.2261111111,-85.0169444444

    2D.Max Longitude - 34.2094444444,-84.9169444444

  3. 使用每个方向的最小值和最大值运行查询

    SELECT *
    
    FROM geoloc
    
    WHERE
    
    lat >= 34.1927777 AND
    
    lat <= 34.2261111 AND
    
    long >= -85.1169444 AND
    
    long <= -84.9169444;
    
    Run Code Online (Sandbox Code Playgroud)

您可以将距离计算与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;
}
Run Code Online (Sandbox Code Playgroud)

UPDATE

我忘了提,我的距离函数将以英尺为单位返回距离.


Mar*_*ker 15

计算边界框以选择SQL查询的WHERE子句中的行的子集,以便您只对该行子集执行昂贵的距离计算,而不是对表中的整个200k记录执行.该方法在此被描述文章活字(用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);
Run Code Online (Sandbox Code Playgroud)

  • @epitaph - 如果您正在使用带有WHERE子句的边界框,该边界框过滤掉明显超出范围的Lat/Long记录(特别是如果您有lat/long的索引),那么您选择基于边界框__before__进行任何Haversine/Vincenty /距离计算...所以你不计算200k记录的距离,只是为了落在边界框内的子集.如果你尝试测试它,它会使它明显加快. (4认同)