【问题标题】:Join operation in Haversine formula在 Haversine 公式中加入运算
【发布时间】:2017-03-05 02:36:03
【问题描述】:

我在PHP中实现Haversine公式如下

$result=mysqli_query($mysqli,"SELECT *,( 6371 * acos( cos( radians({$lat}) ) * cos( radians( `latitude` ) ) * cos( radians( `longitude` ) -radians({$lon}) ) +sin( radians({$lat}) ) * sin( radians( `latitude` ) ) ) ) AS distance FROM `places` HAVING distance <= {$radius} ORDER BY distance ASC") or die(mysqli_error($mysqli));

在 Haversine 提取循环中,我有一个查询,它遍历 hasrsine 的结果以选择与 hasrsine 公式返回的 ID 匹配的记录。查询如下。

 while($row = mysqli_fetch_assoc($result)) 

    {
   $rest_time=$row['id'];

$result1=mysqli_query($mysqli,"SELECT * FROM my_friends  WHERE personal_id='".$personal_id."' AND id='".$rest_time."'") or die(mysqli_error($mysqli)); 

//Some operations here
    }

如何执行 Join 操作来将这些查询混合成一个查询?从优化的角度来看,如果第二个表有 50k 个用户,而第一个表有近 1000 条记录,这样做是否明智?

【问题讨论】:

  • 警告:当使用mysqli 时,您应该使用parameterized queriesbind_param 将用户数据添加到您的查询中。 请勿使用字符串插值或连接来完成此操作,因为您创建了严重的SQL injection bug切勿$_POST$_GET 数据直接放入查询中,如果有人试图利用您的错误,这可能会非常有害。
  • @tadman 您认为查询中出现的变量尚未经过清理,但情况并非如此。
  • @WalterTross 当您假设任何事情时,您就会遇到问题。这太危险了,不能掉以轻心。事情必须明显逃脱,否则没有证据证明它们是。
  • @tadman 你写了“......你已经创建了一个严重的 SQL 注入错误”。除了我将其称为漏洞而不是错误这一事实之外,您还应该写“除非您可以保证您使用的变量是数字或转义字符串”。在一家我们确实保证(并且没有使用mysqli)的公司工作多年,我认为在这种情况下直接指出一个错误是一种过激行为。但是,既然mysqli 是标准,那么您应该不鼓励字符串连接和插值这一事实当然是正确的。
  • 我使用了参数化查询和 bind_param。我只是向你展示公式。这次讨论以完全不同的方式进行。

标签: php mysql join optimization haversine


【解决方案1】:

您在此处对所有行执行的任何操作都会因记录数过多而变慢。

您需要做的是利用索引。要使用索引,它必须是一个简单的查询,而不是 the result of a function(目前是这样)。

你通过半径搜索所做的就是围绕一个点做一个圆,通过在圆之前使用一些三角函数,我们可以得出以下结论

其中 S1 是内部最大的正方形,S2 是外部最小的正方形。

现在我们可以计算出这两个正方形的尺寸,S2 之外的任何东西都被索引命中,S1 内部的任何东西都被索引命中,只剩下现在需要使用查找的小区域缓慢的方法。

如果您需要与该点的距离,请忽略 S1 部分(因为圆内的所有内容都需要 hasrsine 函数)作为此处的注释,而圆内的所有内容都需要它,但并非每个点都在距离内,所以两个WHERE 子句仍然需要

所以让我们使用单位圆来计算这些点

function getS1S2($latitude, $longitude, $kilometer)
{
    $radiusOfEarthKM  = 6371;
    $latitudeRadians  = deg2rad($latitude);
    $longitudeRadians = deg2rad($longitude);
    $distance         = $kilometer / $radiusOfEarthKM;

    $deltaLongitude = asin(sin($distance) / cos($latitudeRadians));

    $bounds = new \stdClass();

    // these are the outer bounds of the circle (S2)
    $bounds->minLat  = rad2deg($latitudeRadians  - $distance);
    $bounds->maxLat  = rad2deg($latitudeRadians  + $distance);
    $bounds->minLong = rad2deg($longitudeRadians - $deltaLongitude);
    $bounds->maxLong = rad2deg($longitudeRadians + $deltaLongitude);

    // and these are the inner bounds (S1)
    $bounds->innerMinLat  = rad2deg($latitudeRadians  + $distance       * cos(5 * M_PI_4));
    $bounds->innerMaxLat  = rad2deg($latitudeRadians  + $distance       * sin(M_PI_4));
    $bounds->innerMinLong = rad2deg($longitudeRadians + $deltaLongitude * sin(5 * M_PI_4));
    $bounds->innerMaxLong = rad2deg($longitudeRadians + $deltaLongitude * cos(M_PI_4));

    return $bounds;
}

现在你的查询变成了

SELECT 
  *
FROM
  `places` 
HAVING p.nlatitude BETWEEN {$bounds->minLat} 
  AND {$bounds->maxLat} 
  AND p.nlongitude BETWEEN {$bounds->minLong} 
  AND {$bounds->maxLong} 
  AND (
    (
      p.nlatitude BETWEEN {$bounds->innerMinLat} 
      AND {$bounds->innerMaxLat} 
      AND p.nlongitude BETWEEN {$bounds->innerMinLong} 
      AND {$bounds->innerMaxLong}
    ) 
    OR (
      6371 * ACOS(
        COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
          RADIANS(`longitude`) - RADIANS({ $lon })
        ) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
      )
    )
  )) <= {$radius} 
ORDER BY distance ASC 

重要

以上文字为可读性,请确保这些值正确转义/最好参数化

这样就可以利用索引,让连接在更快的时间内发生

添加加入成为

SELECT 
  *
FROM
  `places` p
  INNER JOIN my_friends f ON f.id = p.id
WHERE   p.latitude BETWEEN {$bounds->minLat} 
  AND {$bounds->maxLat} 
  AND p.longitude BETWEEN {$bounds->minLong} 
  AND {$bounds->maxLong} 
  AND (
    (
      p.latitude BETWEEN {$bounds->innerMinLat} 
      AND {$bounds->innerMaxLat} 
      AND p.longitude BETWEEN {$bounds->innerMinLong} 
      AND {$bounds->innerMaxLong}
    ) 
    OR (
      6371 * ACOS(
        COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
          RADIANS(`longitude`) - RADIANS({ $lon })
        ) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
      )
    )
  )  <= {$radius} 
  AND f.personal_id = {$personal_id}
ORDER BY distance ASC 

重要

以上文字为可读性,请确保这些值正确转义/最好参数化

假设你有正确的索引,这个查询应该保持快速并允许你进行连接。

查看上面的代码,我不确定personal_id 来自哪里,所以保持原样

如果需要查询距离,可以去掉S1方块

    (
      p.latitude BETWEEN {$bounds->innerMinLat} 
      AND {$bounds->innerMaxLat} 
      AND p.longitude BETWEEN {$bounds->innerMinLong} 
      AND {$bounds->innerMaxLong}
    ) 

然后移动OR的第二部分

  6371 * ACOS(
    COS(RADIANS({ $lat })) * COS(RADIANS(`latitude`)) * COS(
      RADIANS(`longitude`) - RADIANS({ $lon })
    ) + SIN(RADIANS({ $lat })) * SIN(RADIANS(`latitude`))
  )

回到选择,它仍然使用 S2。

我还要确保删除查询 6371 中的“幻数”是地球的半径,以千米为单位

【讨论】:

  • 考虑一下我不想加入表格。我正在循环查询结果,就像您的回答一样。但是,我也需要拥有“距离”属性,就像我可以使用 row['distance'] 获取一样。我怎样才能从你的查询中得到呢?
  • 只需将原始查询中的相同计算添加到选择中即可。问题是关于加入吗?
  • 那么如果我去掉s1 Square,然后将计算部分移到初始选择*,那么它还会使用边界吗?
  • @exussem 为了使用距离,将haversine“移动”到选择而不是添加不是更好吗?
  • haversine 将对所有行运行,然后删除较小的正方形优化。使用 Pythagoras 的效果相当好且速度很快。
【解决方案2】:

这种的情况下,将第一个查询作为派生子查询放在第二个中:

SELECT  p.*, f.*    -- Select only the columns you need, not all
    FROM  
    (
        SELECT  *,
                ( 6371 * acos( cos( radians({$lat}) ) * cos( radians( `latitude` ) )
                  * cos( radians( `longitude` ) -radians({$lon}) )
                  +sin( radians({$lat}) ) * sin( radians( `latitude` ) ) )
                ) AS distance
            FROM  `places`
            HAVING  distance <= {$radius}
            ORDER BY  distance ASC"
            LIMIT 10               -- Didn't you forget this??
    ) AS p
    JOIN  my_friends AS f  ON f.personal_id p.personal_id
      AND  id='".$rest_time."'"     -- Huh??

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-09-12
    • 2013-01-11
    • 1970-01-01
    • 1970-01-01
    • 2016-11-09
    • 1970-01-01
    • 2016-04-03
    • 2013-01-22
    相关资源
    最近更新 更多